init
authorMichael Wallner <mike@php.net>
Wed, 4 Mar 2015 14:32:06 +0000 (15:32 +0100)
committerMichael Wallner <mike@php.net>
Wed, 4 Mar 2015 14:32:06 +0000 (15:32 +0100)
17 files changed:
.gitignore [new file with mode: 0644]
LICENSE [new file with mode: 0644]
Makefile [new file with mode: 0644]
README.md [new file with mode: 0644]
bin/pharext [new file with mode: 0755]
build/create-phar.php [new file with mode: 0644]
composer.json [new file with mode: 0644]
src/pharext/CliArgs.php [new file with mode: 0644]
src/pharext/Command.php [new file with mode: 0644]
src/pharext/FilteredSourceDir.php [new file with mode: 0644]
src/pharext/GitSourceDir.php [new file with mode: 0644]
src/pharext/Installer.php [new file with mode: 0644]
src/pharext/Packager.php [new file with mode: 0644]
src/pharext/PeclSourceDir.php [new file with mode: 0644]
src/pharext/SourceDir.php [new file with mode: 0644]
src/pharext_installer.php [new file with mode: 0644]
src/pharext_packager.php [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..c1b53ea
--- /dev/null
@@ -0,0 +1,5 @@
+.buildpath
+.project
+.settings/
+*~
+*.tmp
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..3f7d0f6
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,23 @@
+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.
+
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..3f00b50
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,15 @@
+#
+# 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
+       
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..9b098a6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,123 @@
+# 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)
+
diff --git a/bin/pharext b/bin/pharext
new file mode 100755 (executable)
index 0000000..61c9b84
Binary files /dev/null and b/bin/pharext differ
diff --git a/build/create-phar.php b/build/create-phar.php
new file mode 100644 (file)
index 0000000..c03ba8e
--- /dev/null
@@ -0,0 +1,27 @@
+<?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);
+}
diff --git a/composer.json b/composer.json
new file mode 100644 (file)
index 0000000..8f8ab64
--- /dev/null
@@ -0,0 +1,11 @@
+{
+       "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"
+       }
+}
diff --git a/src/pharext/CliArgs.php b/src/pharext/CliArgs.php
new file mode 100644 (file)
index 0000000..f93ad7b
--- /dev/null
@@ -0,0 +1,326 @@
+<?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);
+       }
+       /**@-*/
+}
diff --git a/src/pharext/Command.php b/src/pharext/Command.php
new file mode 100644 (file)
index 0000000..f7b0c74
--- /dev/null
@@ -0,0 +1,36 @@
+<?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);
+}
diff --git a/src/pharext/FilteredSourceDir.php b/src/pharext/FilteredSourceDir.php
new file mode 100644 (file)
index 0000000..859df5c
--- /dev/null
@@ -0,0 +1,97 @@
+<?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
diff --git a/src/pharext/GitSourceDir.php b/src/pharext/GitSourceDir.php
new file mode 100644 (file)
index 0000000..f18f97f
--- /dev/null
@@ -0,0 +1,70 @@
+<?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();
+       }
+}
diff --git a/src/pharext/Installer.php b/src/pharext/Installer.php
new file mode 100644 (file)
index 0000000..d8c7feb
--- /dev/null
@@ -0,0 +1,154 @@
+<?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);
+       }
+}
diff --git a/src/pharext/Packager.php b/src/pharext/Packager.php
new file mode 100644 (file)
index 0000000..3408c75
--- /dev/null
@@ -0,0 +1,187 @@
+<?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");
+               } 
+       }
+}
diff --git a/src/pharext/PeclSourceDir.php b/src/pharext/PeclSourceDir.php
new file mode 100644 (file)
index 0000000..3844519
--- /dev/null
@@ -0,0 +1,101 @@
+<?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();
+       }
+}
diff --git a/src/pharext/SourceDir.php b/src/pharext/SourceDir.php
new file mode 100644 (file)
index 0000000..fbdd2f6
--- /dev/null
@@ -0,0 +1,26 @@
+<?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();
+}
diff --git a/src/pharext_installer.php b/src/pharext_installer.php
new file mode 100644 (file)
index 0000000..90c9f50
--- /dev/null
@@ -0,0 +1,11 @@
+<?php
+/**
+ * The installer sub-stub for extension phars
+ */
+
+function __autoload($c) {
+       return include strtr($c, "\\_", "//") . ".php";
+}
+
+$installer = new pharext\Installer();
+$installer->run($argc, $argv);
diff --git a/src/pharext_packager.php b/src/pharext_packager.php
new file mode 100644 (file)
index 0000000..d0636ee
--- /dev/null
@@ -0,0 +1,16 @@
+<?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();