--- /dev/null
+.buildpath
+.project
+.settings/
+*~
+*.tmp
--- /dev/null
+Copyright (c) 2015, Michael Wallner <mike@iworks.at>.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
--- /dev/null
+#
+# build bin/pharext
+#
+
+all: bin/pharext
+
+bin/pharext: src/* src/pharext/*
+ php -d phar.readonly=0 build/create-phar.php
+ chmod +x $@
+
+clean:
+ rm bin/pharext*
+
+.PHONY: all clean
+
--- /dev/null
+# pharext
+
+Distribute your PHP extension as self-installable phar executable
+
+## About
+
+### Disclaimer
+
+You don't need this package to install any `*.ext.phar` extension packages,
+just run them with php:
+
+ $ ./pecl_http-2.4.0dev.ext.phar
+
+Or, if the execute permission bit got lost somehow:
+
+ $ php pecl_http-2.4.0dev.ext.phar
+
+Command help:
+
+ $ ./pecl_http-2.4.0dev.ext.phar -h
+
+Yields:
+
+ Usage:
+
+ $ ./pecl_http-2.4.0dev.ext.phar [-h|-v|-q|-s] [-p|-n|-c <arg>]
+
+ -h|--help Display help
+ -v|--verbose More output
+ -q|--quiet Less output
+ -p|--prefix <arg> PHP installation directory [/usr]
+ -n|--common-name <arg> PHP common program name, e.g. php5 [php]
+ -c|--configure <arg> Additional extension configure flags
+ -s|--sudo [<arg>] Installation might need increased privileges [sudo -S %s]
+
+If your installation destination needs escalated permissions, have a look at the `--sudo` option:
+
+ $ ./pecl_http-2.4.0dev.ext.phar --sudo
+ Running phpize ... OK
+ Running configure ... OK
+ Running make ... OK
+ Running install ... Password:············
+ Installing shared extensions: /usr/lib/php/extensions/no-debug-non-zts-20121212/
+ Installing header files: /usr/include/php/
+ OK
+
+### Prerequisites
+
+The usual tools you need to build a PHP extension:
+* php, phpize and php-config
+* make, cc and autotools
+A network connection is not needed.
+
+### Not implemented
+
+* Dependencies
+* Package description files
+
+## Installation for extension maintainers
+
+ $ composer require m6w6/pharext
+
+### Prerequisites:
+
+* make
+* php + phar
+
+## Usage
+
+ $ ./bin/pharext --pecl --source ../pecl_http.git
+
+Yields:
+
+ Creating phar ./pecl_http-2.4.0dev.ext.phar.54f6e987ae00f.tmp ... OK
+ Finalizing ./pecl_http-2.4.0dev.ext.phar ... OK
+
+Note that the PECL source can infer package name and release version from the package.xml.
+
+Another example using `git ls-files`:
+
+ $ ./bin/pharext -v -g -s ../raphf.git --name raphf --release 1.0.5
+
+Yields:
+
+ Creating phar ./raphf-1.0.5.ext.phar.54f6ebd71f13b.tmp ...
+ Packaging .gitignore
+ Packaging CREDITS
+ Packaging Doxyfile
+ Packaging LICENSE
+ Packaging TODO
+ Packaging config.m4
+ Packaging config.w32
+ Packaging package.xml
+ Packaging php_raphf.c
+ Packaging php_raphf.h
+ Packaging raphf.png
+ Packaging tests/http001.phpt
+ Packaging tests/http002.phpt
+ Packaging tests/http003.phpt
+ Packaging tests/http004.phpt
+ OK
+ Finalizing ./raphf-1.0.5.ext.phar ... OK
+
+Command help:
+
+ $ ./bin/pharext --help
+
+Yields:
+
+ Usage:
+
+ $ ./bin/pharext [-h|-v|-q|-g|-p] -s <arg> -n <arg> -r <arg> [-d <arg>]
+
+ -h|--help Display this help
+ -v|--verbose More output
+ -q|--quiet Less output
+ -s|--source <arg> Extension source directory (REQUIRED)
+ -g|--git Use `git ls-files` instead of the standard ignore filter
+ -p|--pecl Use PECL package.xml instead of the standard ignore filter
+ -d|--dest <arg> Destination directory [.]
+ -n|--name <arg> Extension name (REQUIRED)
+ -r|--release <arg> Extension release version (REQUIRED)
+
--- /dev/null
+<?php
+
+/**
+ * Creates bin/pharext, invoked through the Makefile
+ */
+
+$pkguniq = uniqid();
+$pkgname = __DIR__."/../bin/pharext";
+$tmpname = "$pkgname.$pkguniq.phar.tmp";
+
+if (file_exists($tmpname)) {
+ if (!unlink($tmpname)) {
+ fprintf(STDERR, "%s\n", error_get_last()["message"]);
+ exit(3);
+ }
+}
+
+$package = new \Phar($tmpname, 0, "pharext.phar");
+$package->buildFromDirectory(dirname(__DIR__)."/src", "/^.*\.php$/");
+$package->setDefaultStub("pharext_packager.php");
+$package->setStub("#!/usr/bin/php -dphar.readonly=0\n".$package->getStub());
+unset($package);
+
+if (!rename($tmpname, $pkgname)) {
+ fprintf(STDERR, "%s\n", error_get_last()["message"]);
+ exit(4);
+}
--- /dev/null
+{
+ "name": "m6w6/pharext",
+ "description": "Package PHP extensions as self-installing PHARs",
+ "keywords": ["ext", "extension", "phar", "package", "install"],
+ "type": "project",
+ "license": "BSD-2-Clause",
+ "bin": ["bin/pharext"],
+ "scripts": {
+ "pre-install-cmd": "make -s"
+ }
+}
--- /dev/null
+<?php
+
+namespace pharext;
+
+/**
+ * Command line arguments
+ */
+class CliArgs implements \ArrayAccess
+{
+ /**
+ * Optional option
+ */
+ const OPTIONAL = 0x000;
+
+ /**
+ * Required Option
+ */
+ const REQUIRED = 0x001;
+
+ /**
+ * Only one value, even when used multiple times
+ */
+ const SINGLE = 0x000;
+
+ /**
+ * Aggregate an array, when used multiple times
+ */
+ const MULTI = 0x010;
+
+ /**
+ * Option takes no argument
+ */
+ const NOARG = 0x000;
+
+ /**
+ * Option requires an argument
+ */
+ const REQARG = 0x100;
+
+ /**
+ * Option takes an optional argument
+ */
+ const OPTARG = 0x200;
+
+ /**
+ * Option halts processing
+ */
+ const HALT = 0x10000000;
+
+ /**
+ * Original option spec
+ * @var array
+ */
+ private $orig;
+
+ /**
+ * Compiled spec
+ * @var array
+ */
+ private $spec = [];
+
+ /**
+ * Parsed args
+ * @var array
+ */
+ private $args = [];
+
+ /**
+ * Compile the original spec
+ * @param array $spec
+ */
+ public function __construct(array $spec = null) {
+ $this->compile($spec);
+ }
+
+ /**
+ * Compile the original spec
+ * @param array $spec
+ * @return pharext\CliArgs self
+ */
+ public function compile(array $spec = null) {
+ $this->orig = $spec;
+ $this->spec = [];
+ foreach ((array) $spec as $arg) {
+ $this->spec["-".$arg[0]] = $arg;
+ $this->spec["--".$arg[1]] = $arg;
+ }
+ return $this;
+ }
+
+ /**
+ * Parse command line arguments according to the compiled spec.
+ *
+ * The Generator yields any parsing errors.
+ * Parsing will stop when all arguments are processed or the first option
+ * flagged CliArgs::HALT was encountered.
+ *
+ * @param int $argc
+ * @param array $argv
+ * @return Generator
+ */
+ public function parse($argc, array $argv) {
+ for ($i = 0; $i < $argc; ++$i) {
+ $o = $argv[$i];
+
+ if (!isset($this->spec[$o])) {
+ yield sprintf("Unknown option %s", $argv[$i]);
+ } elseif (!$this->optAcceptsArg($o)) {
+ $this[$o] = true;
+ } elseif ($i+1 < $argc && !isset($this->spec[$argv[$i+1]])) {
+ $this[$o] = $argv[++$i];
+ } elseif ($this->optNeedsArg($o)) {
+ yield sprintf("Option --%s needs an argument", $this->optLongName($o));
+ } else {
+ // OPTARG
+ $this[$o] = $this->optDefaultArg($o);
+ }
+
+ if ($this->optHalts($o)) {
+ return;
+ }
+ }
+ }
+
+ /**
+ * Validate that all required options were given.
+ *
+ * The Generator yields any validation errors.
+ *
+ * @return Generator
+ */
+ public function validate() {
+ $required = array_filter($this->orig, function($spec) {
+ return $spec[3] & self::REQUIRED;
+ });
+ foreach ($required as $req) {
+ if (!isset($this[$req[0]])) {
+ yield sprintf("Option --%s is required", $req[1]);
+ }
+ }
+ }
+
+ /**
+ * Output command line help message
+ * @param string $prog
+ */
+ public function help($prog) {
+ printf("\nUsage:\n\n $ %s", $prog);
+ $flags = [];
+ $required = [];
+ $optional = [];
+ foreach ($this->orig as $spec) {
+ if ($spec[3] & self::REQARG) {
+ if ($spec[3] & self::REQUIRED) {
+ $required[] = $spec;
+ } else {
+ $optional[] = $spec;
+ }
+ } else {
+ $flags[] = $spec;
+ }
+ }
+
+ if ($flags) {
+ printf(" [-%s]", implode("|-", array_column($flags, 0)));
+ }
+ foreach ($required as $req) {
+ printf(" -%s <arg>", $req[0]);
+ }
+ if ($optional) {
+ printf(" [-%s <arg>]", implode("|-", array_column($optional, 0)));
+ }
+ printf("\n\n");
+ foreach ($this->orig as $spec) {
+ printf(" -%s|--%s %s", $spec[0], $spec[1], ($spec[3] & self::REQARG) ? "<arg> " : (($spec[3] & self::OPTARG) ? "[<arg>]" : " "));
+ printf("%s%s %s", str_repeat(" ", 16-strlen($spec[1])), $spec[2], ($spec[3] & self::REQUIRED) ? "(REQUIRED)" : "");
+ if (isset($spec[4])) {
+ printf(" [%s]", $spec[4]);
+ }
+ printf("\n");
+ }
+ printf("\n");
+ }
+
+ /**
+ * Retreive the default argument of an option
+ * @param string $o
+ * @return mixed
+ */
+ private function optDefaultArg($o) {
+ $o = $this->opt($o);
+ if (isset($this->spec[$o][4])) {
+ return $this->spec[$o][4];
+ }
+ return null;
+ }
+
+ /**
+ * Retrieve the help message of an option
+ * @param string $o
+ * @return string
+ */
+ private function optHelp($o) {
+ $o = $this->opt($o);
+ if (isset($this->spec[$o][2])) {
+ return $this->spec[$o][2];
+ }
+ return "";
+ }
+
+ /**
+ * Check whether an option is flagged for halting argument processing
+ * @param string $o
+ * @return boolean
+ */
+ private function optHalts($o) {
+ $o = $this->opt($o);
+ return $this->spec[$o][3] & self::HALT;
+ }
+
+ /**
+ * Check whether an option needs an argument
+ * @param string $o
+ * @return boolean
+ */
+ private function optNeedsArg($o) {
+ $o = $this->opt($o);
+ return $this->spec[$o][3] & self::REQARG;
+ }
+
+ /**
+ * Check wether an option accepts any argument
+ * @param string $o
+ * @return boolean
+ */
+ private function optAcceptsArg($o) {
+ $o = $this->opt($o);
+ return $this->spec[$o][3] & 0xf00;
+ }
+
+ /**
+ * Check whether an option can be used more than once
+ * @param string $o
+ * @return boolean
+ */
+ private function optIsMulti($o) {
+ $o = $this->opt($o);
+ return $this->spec[$o][3] & self::MULTI;
+ }
+
+ /**
+ * Retreive the long name of an option
+ * @param string $o
+ * @return string
+ */
+ private function optLongName($o) {
+ $o = $this->opt($o);
+ return $this->spec[$o][1];
+ }
+
+ /**
+ * Retreive the short name of an option
+ * @param string $o
+ * @return string
+ */
+ private function optShortName($o) {
+ $o = $this->opt($o);
+ return $this->spec[$o][0];
+ }
+
+ /**
+ * Retreive the canonical name (--long-name) of an option
+ * @param string $o
+ * @return string
+ */
+ private function opt($o) {
+ if ($o{0} !== '-') {
+ if (strlen($o) > 1) {
+ $o = "-$o";
+ }
+ $o = "-$o";
+ }
+ return $o;
+ }
+
+ /**@+
+ * Implements ArrayAccess and virtual properties
+ */
+ function offsetExists($o) {
+ $o = $this->opt($o);
+ return isset($this->args[$o]);
+ }
+ function __isset($o) {
+ return $this->offsetExists($o);
+ }
+ function offsetGet($o) {
+ $o = $this->opt($o);
+ if (isset($this->args[$o])) {
+ return $this->args[$o];
+ }
+ return $this->optDefaultArg($o);
+ }
+ function __get($o) {
+ return $this->offsetGet($o);
+ }
+ function offsetSet($o, $v) {
+ if ($this->optIsMulti($o)) {
+ $this->args["-".$this->optShortName($o)][] = $v;
+ $this->args["--".$this->optLongName($o)][] = $v;
+ } else {
+ $this->args["-".$this->optShortName($o)] = $v;
+ $this->args["--".$this->optLongName($o)] = $v;
+ }
+ }
+ function __set($o, $v) {
+ $this->offsetSet($o, $v);
+ }
+ function offsetUnset($o) {
+ unset($this->args["-".$this->optShortName($o)]);
+ unset($this->args["--".$this->optLongName($o)]);
+ }
+ function __unset($o) {
+ $this->offsetUnset($o);
+ }
+ /**@-*/
+}
--- /dev/null
+<?php
+
+namespace pharext;
+
+/**
+ * Command interface
+ */
+interface Command
+{
+ /**
+ * Retrieve command line arguments
+ * @return pharext\CliArgs
+ */
+ public function getArgs();
+
+ /**
+ * Print info
+ * @param string $fmt
+ * @param string ...$args
+ */
+ public function info($fmt);
+
+ /**
+ * Print error
+ * @param string $fmt
+ * @param string ...$args
+ */
+ public function error($fmt);
+
+ /**
+ * Execute the command
+ * @param int $argc command line argument count
+ * @param array $argv command line argument list
+ */
+ public function run($argc, array $argv);
+}
--- /dev/null
+<?php
+
+namespace pharext;
+
+/**
+ * Generic filtered source directory
+ */
+class FilteredSourceDir extends \FilterIterator implements SourceDir
+{
+ /**
+ * The Packager command
+ * @var pharext\Command
+ */
+ private $cmd;
+
+ /**
+ * Base directory
+ * @var string
+ */
+ private $path;
+
+ /**
+ * Exclude filters
+ * @var array
+ */
+ private $filter = [".git/*", ".hg/*"];
+
+ /**
+ * @inheritdoc
+ * @see \pharext\SourceDir::__construct()
+ */
+ public function __construct(Command $cmd, $path) {
+ $this->cmd = $cmd;
+ $this->path = $path;
+ parent::__construct(
+ new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator($path,
+ \FilesystemIterator::KEY_AS_PATHNAME |
+ \FilesystemIterator::CURRENT_AS_FILEINFO |
+ \FilesystemIterator::SKIP_DOTS
+ )
+ )
+ );
+ foreach ([".gitignore", ".hgignore"] as $ignore) {
+ if (file_exists("$path/$ignore")) {
+ $this->filter = array_merge($this->filter,
+ array_map(function($pat) {
+ $pat = trim($pat);
+ if (substr($pat, -1) == '/') {
+ $pat .= '*';
+ }
+ return $pat;
+ }, file("$path/$ignore",
+ FILE_IGNORE_NEW_LINES |
+ FILE_SKIP_EMPTY_LINES
+ ))
+ );
+ }
+ }
+ }
+
+ /**
+ * @inheritdoc
+ * @see \pharext\SourceDir::getBaseDir()
+ */
+ public function getBaseDir() {
+ return $this->path;
+ }
+
+ /**
+ * Implements FilterIterator
+ * @see FilterIterator::accept()
+ */
+ public function accept() {
+ $fn = $this->key();
+ if (is_dir($fn)) {
+ if ($this->cmd->getArgs()->verbose) {
+ $this->info("Excluding %s\n", $fn);
+ }
+ return false;
+ }
+ $pl = strlen($this->path) + 1;
+ $pn = substr($this->key(), $pl);
+ foreach ($this->filter as $pat) {
+ if (fnmatch($pat, $pn)) {
+ if ($this->cmd->getArgs()->verbose) {
+ $this->info("Excluding %s\n", $pn);
+ }
+ return false;
+ }
+ }
+ if ($this->cmd->getArgs()->verbose) {
+ $this->info("Packaging %s\n", $pn);
+ }
+ return true;
+ }
+}
\ No newline at end of file
--- /dev/null
+<?php
+
+namespace pharext;
+
+/**
+ * Extension source directory which is a git repo
+ */
+class GitSourceDir implements \IteratorAggregate, SourceDir
+{
+ /**
+ * The Packager command
+ * @var pharext\Command
+ */
+ private $cmd;
+
+ /**
+ * Base directory
+ * @var string
+ */
+ private $path;
+
+ /**
+ * @inheritdoc
+ * @see \pharext\SourceDir::__construct()
+ */
+ public function __construct(Command $cmd, $path) {
+ $this->cmd = $cmd;
+ $this->path = $path;
+ }
+
+ /**
+ * @inheritdoc
+ * @see \pharext\SourceDir::getBaseDir()
+ */
+ public function getBaseDir() {
+ return $this->path;
+ }
+
+ /**
+ * Generate a list of files by `git ls-files`
+ * @return Generator
+ */
+ private function generateFiles() {
+ $pwd = getcwd();
+ chdir($this->path);
+ if (($pipe = popen("git ls-files", "r"))) {
+ while (!feof($pipe)) {
+ if (strlen($file = trim(fgets($pipe)))) {
+ if ($this->cmd->getArgs()->verbose) {
+ $this->cmd->info("Packaging %s\n", $file);
+ }
+ if (!($realpath = realpath($file))) {
+ $this->cmd->error("File %s does not exist\n", $file);
+ }
+ yield $realpath;
+ }
+ }
+ pclose($pipe);
+ }
+ chdir($pwd);
+ }
+
+ /**
+ * Implements IteratorAggregate
+ * @see IteratorAggregate::getIterator()
+ */
+ public function getIterator() {
+ return $this->generateFiles();
+ }
+}
--- /dev/null
+<?php
+
+namespace pharext;
+
+use Phar;
+
+/**
+ * The extension install command executed by the extension phar
+ */
+class Installer implements Command
+{
+ /**
+ * Command line arguments
+ * @var pharext\CliArgs
+ */
+ private $args;
+
+ /**
+ * Create the command
+ */
+ public function __construct() {
+ $this->args = new CliArgs([
+ ["h", "help", "Display help",
+ CliArgs::OPTIONAL|CliArgs::SINGLE|CliArgs::NOARG],
+ ["v", "verbose", "More output",
+ CliArgs::OPTIONAL|CliArgs::SINGLE|CliArgs::NOARG],
+ ["q", "quiet", "Less output",
+ CliArgs::OPTIONAL|CliArgs::SINGLE|CliArgs::NOARG],
+ ["p", "prefix", "PHP installation directory",
+ CliArgs::OPTIONAL|CliArgs::SINGLE|CliArgs::REQARG,
+ "/usr"],
+ ["n", "common-name", "PHP common program name, e.g. php5",
+ CliArgs::OPTIONAL|CliArgs::SINGLE|CliArgs::REQARG,
+ "php"],
+ ["c", "configure", "Additional extension configure flags",
+ CliArgs::OPTIONAL|CliArgs::MULTI|CliArgs::REQARG],
+ ["s", "sudo", "Installation might need increased privileges",
+ CliArgs::OPTIONAL|CliArgs::SINGLE|CliArgs::OPTARG,
+ "sudo -S %s"]
+ ]);
+ }
+
+ /**
+ * @inheritdoc
+ * @see \pharext\Command::run()
+ */
+ public function run($argc, array $argv) {
+ $prog = array_shift($argv);
+ foreach ($this->args->parse(--$argc, $argv) as $error) {
+ $this->error("%s\n", $error);
+ }
+
+ if ($this->args["help"]) {
+ $this->args->help($prog);
+ exit;
+ }
+
+ foreach ($this->args->validate() as $error) {
+ $this->error("%s\n", $error);
+ }
+
+ if (isset($error)) {
+ if (!$this->args["quiet"]) {
+ $this->args->help($prog);
+ }
+ exit(1);
+ }
+
+ $this->installPackage();
+ }
+
+ /**
+ * @inheritdoc
+ * @see \pharext\Command::getArgs()
+ */
+ public function getArgs() {
+ return $this->args;
+ }
+
+ /**
+ * @inheritdoc
+ * @see \pharext\Command::info()
+ */
+ public function info($fmt) {
+ if (!$this->args->quiet) {
+ vprintf($fmt, array_slice(func_get_args(), 1));
+ }
+ }
+
+ /**
+ * @inheritdoc
+ * @see \pharext\Command::error()
+ */
+ public function error($fmt) {
+ if (!$this->args->quiet) {
+ vfprintf(STDERR, "ERROR: $fmt", array_slice(func_get_args(), 1));
+ }
+ }
+
+ /**
+ * Extract the phar to a temporary directory
+ */
+ private function extract() {
+ if (!$file = Phar::running(false)) {
+ $this->error("Did your run the ext.phar?\n");
+ exit(3);
+ }
+ $temp = sys_get_temp_dir()."/".basename($file, ".ext.phar");
+ is_dir($temp) or mkdir($temp, 0750, true);
+ $phar = new Phar($file);
+ $phar->extractTo($temp, null, true);
+ chdir($temp);
+ }
+
+ /**
+ * Execute a system command
+ * @param string $name pretty name
+ * @param string $command full command
+ * @param bool $sudo whether the command may need escalated privileges
+ */
+ private function exec($name, $command, $sudo = false) {
+ $this->info("Running %s ...%s", $this->args->verbose ? $command : $name, $this->args->verbose ? "\n" : " ");
+ if ($sudo && isset($this->args->sudo)) {
+ if ($proc = proc_open(sprintf($this->args->sudo, $command)." 2>&1", [STDIN,STDOUT,STDERR], $pipes)) {
+ $retval = proc_close($proc);
+ } else {
+ $retval = -1;
+ }
+ } elseif ($this->args->verbose) {
+ passthru($command ." 2>&1", $retval);
+ } else {
+ exec($command ." 2>&1", $output, $retval);
+ }
+ if ($retval) {
+ $this->error("Command %s failed with (%s)\n", $command, $retval);
+ if (isset($output) && !$this->args->quiet) {
+ printf("%s\n", implode("\n", $output));
+ }
+ exit(2);
+ }
+ $this->info("OK\n");
+ }
+
+ /**
+ * Prepares, configures, builds and installs the extension
+ */
+ private function installPackage() {
+ $this->extract();
+ $this->exec("phpize", "{$this->args->prefix}/bin/{$this->args->{'common-name'}}ize");
+ $this->exec("configure", "./configure --with-php-config={$this->args->prefix}/bin/{$this->args->{'common-name'}}-config ". implode(" ", (array) $this->args->configure));
+ $this->exec("make", "make -sj3");
+ $this->exec("install", "make -s install", true);
+ }
+}
--- /dev/null
+<?php
+
+namespace pharext;
+
+use Phar;
+
+/**
+ * The extension packaging command executed by bin/pharext
+ */
+class Packager implements Command
+{
+ /**
+ * Command line arguments
+ * @var pharext\CliArgs
+ */
+ private $args;
+
+ /**
+ * Extension source directory
+ * @var pharext\SourceDir
+ */
+ private $source;
+
+ /**
+ * Create the command
+ */
+ public function __construct() {
+ $this->args = new CliArgs([
+ ["h", "help", "Display this help",
+ CliArgs::OPTIONAL|CliArgs::SINGLE|CliArgs::NOARG|CliArgs::HALT],
+ ["v", "verbose", "More output",
+ CliArgs::OPTIONAL|CliArgs::SINGLE|CliArgs::NOARG],
+ ["q", "quiet", "Less output",
+ CliArgs::OPTIONAL|CliArgs::SINGLE|CliArgs::NOARG],
+ ["s", "source", "Extension source directory",
+ CliArgs::REQUIRED|CliArgs::SINGLE|CliArgs::REQARG],
+ ["g", "git", "Use `git ls-files` instead of the standard ignore filter",
+ CliArgs::OPTIONAL|CliArgs::SINGLE|CliArgs::NOARG],
+ ["p", "pecl", "Use PECL package.xml instead of the standard ignore filter",
+ CliArgs::OPTIONAL|CliArgs::SINGLE|CliArgs::NOARG],
+ ["d", "dest", "Destination directory",
+ CliArgs::OPTIONAL|CliArgs::SINGLE|CliArgs::REQARG,
+ "."],
+ ["n", "name", "Extension name",
+ CliArgs::REQUIRED|CliArgs::SINGLE|CliArgs::REQARG],
+ ["r", "release", "Extension release version",
+ CliArgs::REQUIRED|CliArgs::SINGLE|CliArgs::REQARG],
+ ["z", "gzip", "Create additional PHAR compressed with gzip",
+ CliArgs::OPTIONAL|CliArgs::SINGLE|CliArgs::NOARG],
+ ["Z", "bzip", "Create additional PHAR compressed with bzip",
+ CliArgs::OPTIONAL|CliArgs::SINGLE|CliArgs::NOARG],
+]);
+ }
+
+ /**
+ * @inheritdoc
+ * @see \pharext\Command::run()
+ */
+ public function run($argc, array $argv) {
+ $prog = array_shift($argv);
+ foreach ($this->args->parse(--$argc, $argv) as $error) {
+ $this->error("%s\n", $error);
+ }
+
+ if ($this->args["help"]) {
+ $this->args->help($prog);
+ exit;
+ }
+
+ if ($this->args["source"]) {
+ if ($this->args["pecl"]) {
+ $this->source = new PeclSourceDir($this, $this->args["source"]);
+ } elseif ($this->args["git"]) {
+ $this->source = new GitSourceDir($this, $this->args["source"]);
+ } else {
+ $this->source = new FilteredSourceDir($this, $this->args["source"]);
+ }
+ }
+
+ foreach ($this->args->validate() as $error) {
+ $this->error("%s\n", $error);
+ }
+
+ if (isset($error)) {
+ if (!$this->args["quiet"]) {
+ $this->args->help($prog);
+ }
+ exit(1);
+ }
+
+ $this->createPackage();
+ }
+
+ /**
+ * @inheritdoc
+ * @see \pharext\Command::getArgs()
+ */
+ public function getArgs() {
+ return $this->args;
+ }
+
+ /**
+ * @inheritdoc
+ * @see \pharext\Command::info()
+ */
+ public function info($fmt) {
+ if (!$this->args->quiet) {
+ vprintf($fmt, array_slice(func_get_args(), 1));
+ }
+ }
+
+ /**
+ * @inheritdoc
+ * @see \pharext\Command::error()
+ */
+ public function error($fmt) {
+ if (!$this->args->quiet) {
+ vfprintf(STDERR, "ERROR: $fmt", array_slice(func_get_args(), 1));
+ }
+ }
+
+ /**
+ * Traverses all pharext source files to bundle
+ * @return Generator
+ */
+ private function bundle() {
+ foreach (scandir(__DIR__) as $entry) {
+ if (fnmatch("*.php", $entry)) {
+ yield "pharext/$entry" => __DIR__."/$entry";
+ }
+ }
+ }
+
+ /**
+ * Creates the extension phar
+ */
+ private function createPackage() {
+ $pkguniq = uniqid();
+ $pkgtemp = sys_get_temp_dir() ."/{$pkguniq}.phar";
+ $pkgdesc = "{$this->args->name}-{$this->args->release}";
+
+ $this->info("Creating phar %s ...%s", $pkgtemp, $this->args->verbose ? "\n" : " ");
+ try {
+ $package = new Phar($pkgtemp, 0, "ext.phar");
+ $package->startBuffering();
+ $package->buildFromIterator($this->source, $this->source->getBaseDir());
+ $package->buildFromIterator($this->bundle());
+ $package->addFile(__DIR__."/../pharext_installer.php", "pharext_installer.php");
+ $package->setDefaultStub("pharext_installer.php");
+ $package->setStub("#!/usr/bin/php -dphar.readonly=1\n".$package->getStub());
+ $package->stopBuffering();
+
+ chmod($pkgtemp, 0770);
+ if ($this->args->verbose) {
+ $this->info("Created executable phar %s\n", $pkgtemp);
+ } else {
+ $this->info("OK\n");
+ }
+ if ($this->args->gzip) {
+ $this->info("Compressing with gzip ... ");
+ $package->compress(Phar::GZ);
+ $this->info("OK\n");
+ }
+ if ($this->args->bzip) {
+ $this->info("Compressing with bzip ... ");
+ $package->compress(Phar::BZ2);
+ $this->info("OK\n");
+ }
+
+ unset($package);
+ } catch (\Exception $e) {
+ $this->error("%s\n", $e->getMessage());
+ exit(4);
+ }
+
+ foreach (glob($pkgtemp."*") as $pkgtemp) {
+ $pkgfile = str_replace($pkguniq, "{$pkgdesc}-ext", $pkgtemp);
+ $pkgname = $this->args->dest ."/". basename($pkgfile);
+ $this->info("Finalizing %s ... ", $pkgname);
+ if (!rename($pkgtemp, $pkgname)) {
+ $this->error("%s\n", error_get_last()["message"]);
+ exit(5);
+ }
+ $this->info("OK\n");
+ }
+ }
+}
--- /dev/null
+<?php
+
+namespace pharext;
+
+/**
+ * A PECL extension source directory containing a v2 package.xml
+ */
+class PeclSourceDir implements \IteratorAggregate, SourceDir
+{
+ /**
+ * The Packager command
+ * @var pharext\Packager
+ */
+ private $cmd;
+
+ /**
+ * The package.xml
+ * @var SimpleXmlElement
+ */
+ private $sxe;
+
+ /**
+ * The base directory
+ * @var string
+ */
+ private $path;
+
+ /**
+ * @inheritdoc
+ * @see \pharext\SourceDir::__construct()
+ */
+ public function __construct(Command $cmd, $path) {
+ $sxe = simplexml_load_file("$path/package.xml", null, 0, "http://pear.php.net/dtd/package-2.0");
+ $sxe->registerXPathNamespace("pecl", "http://pear.php.net/dtd/package-2.0");
+
+ $args = $cmd->getArgs();
+ if (!isset($args->name)) {
+ $name = (string) $sxe->xpath("/pecl:package/pecl:name")[0];
+ foreach ($args->parse(2, ["--name", $name]) as $error) {
+ $cmd->error("%s\n", $error);
+ }
+ }
+
+ if (!isset($args->release)) {
+ $release = (string) $sxe->xpath("/pecl:package/pecl:version/pecl:release")[0];
+ foreach ($args->parse(2, ["--release", $release]) as $error) {
+ $cmd->error("%s\n", $error);
+ }
+ }
+
+ $this->cmd = $cmd;
+ $this->sxe = $sxe;
+ $this->path = $path;
+ }
+
+ /**
+ * @inheritdoc
+ * @see \pharext\SourceDir::getBaseDir()
+ */
+ public function getBaseDir() {
+ return $this->path;
+ }
+
+ /**
+ * Compute the path of a file by parent dir nodes
+ * @param \SimpleXMLElement $ele
+ * @return string
+ */
+ private function dirOf($ele) {
+ $path = "";
+ while (($ele = current($ele->xpath(".."))) && $ele->getName() == "dir") {
+ $path = trim($ele["name"], "/") ."/". $path ;
+ }
+ return trim($path, "/");
+ }
+
+ /**
+ * Generate a list of files from the package.xml
+ * @return Generator
+ */
+ private function generateFiles() {
+ foreach ($this->sxe->xpath("//pecl:file") as $file) {
+ $path = $this->path ."/". $this->dirOf($file) ."/". $file["name"];
+ if ($this->cmd->getArgs()->verbose) {
+ $this->cmd->info("Packaging %s\n", $path);
+ }
+ if (!($realpath = realpath($path))) {
+ $this->cmd->error("File %s does not exist", $path);
+ }
+ yield $realpath;
+ }
+ }
+
+ /**
+ * Implements IteratorAggregate
+ * @see IteratorAggregate::getIterator()
+ */
+ public function getIterator() {
+ return $this->generateFiles();
+ }
+}
--- /dev/null
+<?php
+
+namespace pharext;
+
+/**
+ * Source directory interface
+ */
+interface SourceDir extends \Traversable
+{
+ /**
+ * Read the source directory
+ *
+ * Note: Best practices are for others, but if you want to follow them, do
+ * not put constructors in interfaces. Keep your complaints, I warned you.
+ *
+ * @param Command $cmd
+ * @param string $path
+ */
+ public function __construct(Command $cmd, $path);
+
+ /**
+ * Retrieve the base directory
+ * @return string
+ */
+ public function getBaseDir();
+}
--- /dev/null
+<?php
+/**
+ * The installer sub-stub for extension phars
+ */
+
+function __autoload($c) {
+ return include strtr($c, "\\_", "//") . ".php";
+}
+
+$installer = new pharext\Installer();
+$installer->run($argc, $argv);
--- /dev/null
+<?php
+
+/**
+ * The packager stub for bin/pharext
+ */
+
+#Phar::mapPhar("pharext.phar");
+
+function __autoload($c) {
+ return include /*"phar://pharext.phar/".*/strtr($c, "\\_", "//") . ".php";
+}
+
+$packager = new pharext\Packager();
+$packager->run($argc, $argv);
+
+__HALT_COMPILER();