major refactoring under the hood
authorMichael Wallner <mike@php.net>
Tue, 24 Mar 2015 16:58:24 +0000 (17:58 +0100)
committerMichael Wallner <mike@php.net>
Tue, 24 Mar 2015 16:58:24 +0000 (17:58 +0100)
33 files changed:
LICENSE
Makefile
bin/pharext
build/create-phar.php
src/pharext/Cli/Command.php
src/pharext/Command.php
src/pharext/Exception.php [new file with mode: 0644]
src/pharext/ExecCmd.php
src/pharext/Installer.php
src/pharext/Openssl/PrivateKey.php
src/pharext/Packager.php
src/pharext/SourceDir/Git.php
src/pharext/SourceDir/Pecl.php
src/pharext/SourceDir/Pharext.php
src/pharext/Task.php [new file with mode: 0644]
src/pharext/Task/Activate.php [new file with mode: 0644]
src/pharext/Task/Askpass.php [new file with mode: 0644]
src/pharext/Task/BundleGenerator.php [new file with mode: 0644]
src/pharext/Task/Configure.php [new file with mode: 0644]
src/pharext/Task/Extract.php [new file with mode: 0644]
src/pharext/Task/GitClone.php [new file with mode: 0644]
src/pharext/Task/Make.php [new file with mode: 0644]
src/pharext/Task/PeclFixup.php [new file with mode: 0644]
src/pharext/Task/PharBuild.php [new file with mode: 0644]
src/pharext/Task/PharCompress.php [new file with mode: 0644]
src/pharext/Task/PharRename.php [new file with mode: 0644]
src/pharext/Task/PharSign.php [new file with mode: 0644]
src/pharext/Task/Phpize.php [new file with mode: 0644]
src/pharext/Task/StreamFetch.php [new file with mode: 0644]
src/pharext/Tempdir.php
src/pharext/Tempfile.php
src/pharext/Tempname.php [new file with mode: 0644]
src/pharext_install.tpl.php

diff --git a/LICENSE b/LICENSE
index 3f7d0f629e10fada8b38dd9c1804cb251a457ef9..ab8ebb29d35c7ab8c2e88e8e2ef0b572e84b9145 100644 (file)
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2015, Michael Wallner <mike@iworks.at>.
+Copyright (c) 2015, Michael Wallner <mike@php.net>.
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without 
index 9697343b7b9c36c639a650257bd802f2cf94c7c3..433b800792fb551cda94849949f610b551782e34 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -9,7 +9,6 @@ bin/pharext: src/* src/pharext/* src/pharext/*/*
        @for file in $?; do php -l $$file | sed -ne '/^No syntax errors/!p' && exit $${PIPESTATUS[0]}; done
        @echo "Creating bin/pharext ... "
        php -d phar.readonly=0 build/create-phar.php
-       chmod +x $@
 
 test:
        @echo "Running tests ... "
index 8e385989c2d14bcf8cf95ab55ebd041a9b6a2db7..dbde37d8ae89529d271f2a087833d8c9252d2def 100755 (executable)
Binary files a/bin/pharext and b/bin/pharext differ
index db9013faa4a8f72876a5d2274c411ee3129f18be..4836cebf6e30438cf85f2dec5ab1cf1b1b08c7bc 100644 (file)
@@ -4,47 +4,28 @@
  * Creates bin/pharext, invoked through the Makefile
  */
 
-$pkgname = __DIR__."/../bin/pharext";
-$tmpname = __DIR__."/pharext.phar";
+set_include_path(dirname(__DIR__)."/src");
+spl_autoload_register(function($c) {
+       return include strtr($c, "\\_", "//") . ".php";
+});
 
-if (file_exists($tmpname)) {
-       if (!unlink($tmpname)) {
-               fprintf(STDERR, "%s\n", error_get_last()["message"]);
-               exit(3);
-       }
-}
+require_once __DIR__."/../src/pharext/Version.php";
 
-$package = new \Phar($tmpname, 0, "pharext.phar");
+$file = (new pharext\Task\PharBuild(null, [
+       "header" => sprintf("pharext v%s (c) Michael Wallner <mike@php.net>", pharext\VERSION),
+       "version" => pharext\VERSION,
+       "name" => "pharext",
+       "date" => date("Y-m-d"),
+       "stub" => "pharext_packager.php",
+       "license" => file_get_contents(__DIR__."/../LICENSE")
+], false))->run();
 
 if (getenv("SIGN")) {
-       shell_exec("stty -echo");
-       printf("Password: ");
-       $password = fgets(STDIN, 1024);
-       printf("\n");
-       shell_exec("stty echo");
-       if (substr($password, -1) == "\n") {
-               $password = substr($password, 0, -1);
-       }
-       
-       $pkey = openssl_pkey_get_private("file://".__DIR__."/pharext.key", $password);
-       if (!is_resource($pkey)) {
-               $this->error("Could not load private key %s/pharext.key", __DIR__);
-               exit(3);
-       }
-       if (!openssl_pkey_export($pkey, $key)) {
-               $this->error(null);
-               exit(3);
-       }
-       
-       $package->setSignatureAlgorithm(Phar::OPENSSL, $key);
+       $pass = (new pharext\Task\Askpass)->run();
+       $sign = new pharext\Task\PharSign($file, __DIR__."/pharext.key", $pass);
+       $pkey = $sign->run();
+       $pkey->exportPublicKey(__DIR__."/../bin/pharext.pubkey");
 }
 
-$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);
-}
+/* we do not need the extra logic of Task\PharRename */
+rename($file, __DIR__."/../bin/pharext");
index e19ce93a444ffd3d4d87a2be90ed589f897e86a8..0cc0bb40f2b8649cc06de21d577c7b76728f2a44 100644 (file)
@@ -4,7 +4,21 @@ namespace pharext\Cli;
 
 use pharext\Cli\Args as CliArgs;
 
-require_once "pharext/Version.php";
+use Phar;
+
+if (!function_exists("array_column")) {
+       function array_column(array $array, $col, $idx = null) {
+               $result = [];
+               foreach ($array as $el) {
+                       if (isset($idx)) {
+                               $result[$el[$idx]] = $el[$col];
+                       } else {
+                               $result[] = $el[$col];
+                       }
+               }
+               return $result;
+       }
+}
 
 trait Command
 {
@@ -22,12 +36,37 @@ trait Command
                return $this->args;
        }
 
+       /**
+        * Retrieve metadata of the currently running phar
+        * @param string $key
+        * @return mixed
+        */
+       public function metadata($key = null) {
+               $running = new Phar(Phar::running(false));
+
+               if ($key === "signature") {
+                       $sig = $running->getSignature();
+                       return sprintf("%s signature of %s\n%s", 
+                               $sig["hash_type"],
+                               $this->metadata("name"),
+                               chunk_split($sig["hash"], 64, "\n"));
+               }
+
+               $metadata = $running->getMetadata();
+               if (isset($key)) {
+                       return $metadata[$key];
+               }
+               return $metadata;
+       }
+
        /**
         * Output pharext vX.Y.Z header
         */
-       function header() {
-               printf("pharext v%s (c) Michael Wallner <mike@php.net>\n\n", 
-                       \pharext\VERSION);
+       public function header() {
+               if (!headers_sent()) {
+                       /* only display header, if we didn't generate any output yet */
+                       printf("%s\n\n", $this->metadata("header"));
+               }
        }
        
        /**
@@ -50,6 +89,22 @@ trait Command
                }
        }
 
+       /**
+        * @inheritdoc
+        * @see \pharext\Command::warn()
+        */
+       public function warn($fmt) {
+               if (!$this->args->quiet) {
+                       if (!isset($fmt)) {
+                               $fmt = "%s\n";
+                               $arg = error_get_last()["message"];
+                       } else {
+                               $arg = array_slice(func_get_args(), 1);
+                       }
+                       vfprintf(STDERR, "Warning: $fmt", $arg);
+               }
+       }
+
        /**
         * @inheritdoc
         * @see \pharext\Command::error()
@@ -138,11 +193,11 @@ trait Command
                        } elseif (is_dir("$dir/$entry")) {
                                $this->rm("$dir/$entry");
                        } elseif (!unlink("$dir/$entry")) {
-                               $this->error(null);
+                               $this->warn(null);
                        }
                }
                if (!rmdir($dir)) {
-                       $this->error(null);
+                       $this->warn(null);
                }
        }
 }
index b174f7df2afee1a78c60d0a3b121e6df70d5a2a9..61b8810adcffd5bb2c65dd116a493743d7e81538 100644 (file)
@@ -27,13 +27,20 @@ interface Command
         */
        public function info($fmt);
        
+       /**
+        * Print warning
+        * @param string $fmt
+        * @param string ...$args
+        */
+       public function warn($fmt);
+
        /**
         * Print error
         * @param string $fmt
         * @param string ...$args
         */
        public function error($fmt);
-       
+
        /**
         * Execute the command
         * @param int $argc command line argument count
diff --git a/src/pharext/Exception.php b/src/pharext/Exception.php
new file mode 100644 (file)
index 0000000..bf3d87b
--- /dev/null
@@ -0,0 +1,17 @@
+<?php
+
+namespace pharext;
+
+class Exception extends \Exception
+{
+       public function __construct($message = null, $code = 0, $previous = null) {
+               if (!isset($message)) {
+                       $last_error = error_get_last();
+                       $message = $last_error["message"];
+                       if (!$code) {
+                               $code = $last_error["type"];
+                       }
+               }
+               parent::__construct($message, $code, $previous);
+       }
+}
index 056f0b28c5dc3947a3024a05e5c8d10e725c1aa4..3ea63d00438f2887c8f69ce9c5ee1e3e008adf3f 100644 (file)
@@ -2,6 +2,9 @@
 
 namespace pharext;
 
+/**
+ * Execute system command
+ */
 class ExecCmd
 {
        /**
@@ -41,11 +44,6 @@ class ExecCmd
        public function __construct($command, $verbose = false) {
                $this->command = $command;
                $this->verbose = $verbose;
-               
-               /* interrupt output stream */
-               if ($verbose) {
-                       printf("\n");
-               }
        }
        
        /**
@@ -65,7 +63,7 @@ class ExecCmd
        private function suExec($command, &$output, &$status) {
                if (!($proc = proc_open($command, [STDIN,["pipe","w"],["pipe","w"]], $pipes))) {
                        $status = -1;
-                       throw new \Exception("Failed to run {$command}");
+                       throw new Exception("Failed to run {$command}");
                }
                $stdout = $pipes[1];
                $passwd = 0;
@@ -89,7 +87,7 @@ class ExecCmd
        /**
         * Run the command
         * @param array $args
-        * @throws \Exception
+        * @throws \pharext\Exception
         */
        public function run(array $args = null) {
                $exec = escapeshellcmd($this->command);
@@ -112,7 +110,7 @@ class ExecCmd
                }
                
                if ($this->status) {
-                       throw new \Exception("Command {$this->command} failed ({$this->status})");
+                       throw new Exception("Command {$this->command} failed ({$this->status})");
                }
        }
        
@@ -121,7 +119,7 @@ class ExecCmd
         * @return int
         */
        public function getStatus() {
-               return $status;
+               return $this->status;
        }
        
        /**
index 7866b65a160711009ee1e6ee32ae91bdebdc7c46..4b90c49a0ae65ea7a133730c17e48242007261ef 100644 (file)
@@ -2,10 +2,12 @@
 
 namespace pharext;
 
-use Phar;
 use pharext\Cli\Args as CliArgs;
 use pharext\Cli\Command as CliCommand;
 
+use Phar;
+use SplObjectStorage;
+
 /**
  * The extension install command executed by the extension phar
  */
@@ -13,18 +15,6 @@ class Installer implements Command
 {
        use CliCommand;
        
-       /**
-        * The temporary directory we should operate in
-        * @var string
-        */
-       private $tmp;
-
-       /**
-        * The directory we came from
-        * @var string
-        */
-       private $cwd;
-
        /**
         * Create the command
         */
@@ -51,11 +41,22 @@ class Installer implements Command
                ]);
        }
        
-       /**
-        * Cleanup temp directory
-        */
-       public function __destruct() {
-               $this->cleanup();
+       private function extract(Phar $phar) {
+               $this->debug("Extracting %s ...\n", basename($phar->getPath()));
+               return (new Task\Extract($phar))->run($this->args->verbose);
+       }
+
+       private function hooks(SplObjectStorage $phars) {
+               $hooks = [];
+               foreach ($phars as $phar) {
+                       if (isset($phar["pharext_install.php"])) {
+                               $callable = include $phar["pharext_install.php"];
+                               if (is_callable($callable)) {
+                                       $hooks[] = $callable($this);
+                               }
+                       }
+               }
+               return $hooks;
        }
 
        /**
@@ -63,169 +64,111 @@ class Installer implements Command
         * @see \pharext\Command::run()
         */
        public function run($argc, array $argv) {
-               $this->cwd = getcwd();
-               $this->tmp = new Tempdir(basename(Phar::running(false)));
-
+               $list = new SplObjectStorage();
                $phar = new Phar(Phar::running(false));
+               $temp = $this->extract($phar);
+
                foreach ($phar as $entry) {
-                       if (fnmatch("*.ext.phar*", $entry->getBaseName())) {
-                               $temp = new Tempdir($entry->getBaseName());
-                               $phar->extractTo($temp, $entry->getFilename(), true);
-                               $phars[(string) $temp] = new Phar($temp."/".$entry->getFilename());
+                       $dep_file = $entry->getBaseName();
+                       if (fnmatch("*.ext.phar*", $dep_file)) {
+                               $dep_phar = new Phar("$temp/$dep_file");
+                               $list[$dep_phar] = $this->extract($dep_phar);
                        }
                }
-               $phars[(string) $this->tmp] = $phar;
+               /* the actual ext.phar at last */
+               $list[$phar] = $temp;
 
-               foreach ($phars as $phar) {
-                       if (isset($phar["pharext_install.php"])) {
-                               $callable = include $phar["pharext_install.php"];
-                               if (is_callable($callable)) {
-                                       $recv[] = $callable($this);
-                               }
-                       }
-               }
-               
+               /* installer hooks */
+               $hook = $this->hooks($list);
+
+               /* standard arg stuff */
                $errs = [];
                $prog = array_shift($argv);
                foreach ($this->args->parse(--$argc, $argv) as $error) {
                        $errs[] = $error;
                }
-               
+
                if ($this->args["help"]) {
                        $this->header();
                        $this->help($prog);
                        exit;
                }
-               
+
                foreach ($this->args->validate() as $error) {
                        $errs[] = $error;
                }
-               
+
                if ($errs) {
                        if (!$this->args["quiet"]) {
                                $this->header();
                        }
                        foreach ($errs as $err) {
                                $this->error("%s\n", $err);
-                       } 
+                       }
                        if (!$this->args["quiet"]) {
                                $this->help($prog);
                        }
                        exit(1);
                }
-               
-               if (isset($recv)) {
-                       foreach ($recv as $r) {
-                               $r($this);
-                       }
-               }
-               foreach ($phars as $temp => $phar) {
-                       $this->installPackage($phar, $temp);
-               }
-       }
 
-       /**
-        * Prepares, configures, builds and installs the extension
-        */
-       private function installPackage(Phar $phar, $temp) {
-               $this->info("Installing %s ... \n", basename($phar->getAlias()));
-               try {
-                       $phar->extractTo($temp, null, true);
-               } catch (\Exception $e) {
-                       $this->error("%s\n", $e->getMessage());
-                       exit(3);
+               /* post process hooks */
+               foreach ($hook as $callback) {
+                       if (is_callable($callback)) {
+                               $callback($this);
+                       }
                }
 
-               if (!chdir($temp)) {
-                       $this->error(null);
-                       exit(4);
+               /* install packages */
+               foreach ($list as $phar) {
+                       $this->info("Installing %s ...\n", basename($phar->getPath()));
+                       $this->install($list[$phar]);
+                       $this->activate($list[$phar]);
+                       $this->cleanup($list[$phar]);
+                       $this->info("Successfully installed %s!\n", basename($phar->getPath()));
                }
-               
-               $this->build();
-               $this->activate();
-               $this->cleanup($temp);
        }
        
        /**
         * Phpize + trinity
         */
-       private function build() {
+       private function install($temp) {
                try {
                        // phpize
-                       $this->info("Runnin phpize ... ");
-                       $cmd = new ExecCmd($this->php("ize"), $this->args->verbose);
-                       $cmd->run();
-                       $this->info("OK\n");
-                               
+                       $this->info("Running phpize ...\n");
+                       $phpize = new Task\Phpize($temp, $this->args->prefix, $this->args->{"common-name"});
+                       $phpize->run($this->args->verbose);
+
                        // configure
-                       $this->info("Running configure ... ");
-                       $args = ["--with-php-config=". $this->php("-config")];
-                       if ($this->args->configure) {
-                               $args = array_merge($args, $this->args->configure);
-                       }
-                       $cmd = new ExecCmd("./configure", $this->args->verbose);
-                       $cmd->run($args);
-                       $this->info("OK\n");
+                       $this->info("Running configure ...\n");
+                       $configure = new Task\Configure($temp, $this->args->configure, $this->args->prefix, $this->args{"common-name"});
+                       $configure->run($this->args->verbose);
                                
                        // make
-                       $this->info("Running make ... ");
-                       $cmd = new ExecCmd("make", $this->args->verbose);
-                       if ($this->args->verbose) {
-                               $cmd->run(["-j3"]);
-                       } else {
-                               $cmd->run(["-j3", "-s"]);
-                       }
-                       $this->info("OK\n");
-               
+                       $this->info("Running make ...\n");
+                       $make = new Task\Make($temp);
+                       $make->run($this->args->verbose);
+
                        // install
-                       $this->info("Running make install ... ");
-                       if (isset($this->args->sudo)) {
-                               $cmd->setSu($this->args->sudo);
-                       }
-                       if ($this->args->verbose) {
-                               $cmd->run(["install"]);
-                       } else {
-                               $cmd->run(["install", "-s"]);
-                       }
-                       $this->info("OK\n");
+                       $this->info("Running make install ...\n");
+                       $sudo = isset($this->args->sudo) ? $this->args->sudo : null;
+                       $install = new Task\Make($temp, ["install"], $sudo);
+                       $install->run($this->args->verbose);
                
                } catch (\Exception $e) {
                        $this->error("%s\n", $e->getMessage());
-                       $this->error("%s\n", $cmd->getOutput());
+                       exit(2);
                }
        }
 
-       /**
-        * Perform any cleanups
-        */
-       private function cleanup($temp = null) {
-               if (!isset($temp)) {
-                       $temp = $this->tmp;
-               }
+       private function cleanup($temp) {
                if (is_dir($temp)) {
-                       chdir($this->cwd);
-                       $this->info("Cleaning up %s ...\n", $temp);
                        $this->rm($temp);
+               } elseif (file_exists($temp)) {
+                       unlink($temp);
                }
        }
 
-       /**
-        * Construct a command from prefix common-name and suffix
-        * @param type $suffix
-        * @return string
-        */
-       private function php($suffix) {
-               $cmd = $this->args["common-name"] . $suffix;
-               if (isset($this->args->prefix)) {
-                       $cmd = $this->args->prefix . "/bin/" . $cmd;
-               }
-               return $cmd;
-       }
-
-       /**
-        * Activate extension in php.ini
-        */
-       private function activate() {
+       private function activate($temp) {
                if ($this->args->ini) {
                        $files = [realpath($this->args->ini)];
                } else {
@@ -233,60 +176,18 @@ class Installer implements Command
                        $files[] = php_ini_loaded_file();
                }
 
-               $extension = basename(current(glob("modules/*.so")));
-               $pattern = preg_quote($extension);
-
-               foreach ($files as $index => $file) {
-                       $temp = new Tempfile("phpini");
-                       foreach (file($file) as $line) {
-                               if (preg_match("/^\s*extension\s*=\s*[\"']?{$pattern}[\"']?\s*(;.*)?\$/", $line)) {
-                                       // already there
-                                       $this->info("Extension already activated\n");
-                                       return;
-                               }
-                               fwrite($temp->getStream(), $line);
-                       }
-               }
-
-               // not found, add extension line to the last process file
-               if (isset($temp, $file)) {
-                       fprintf($temp->getStream(), "extension=%s\n", $extension);
-                       $temp->closeStream();
+               $sudo = isset($this->args->sudo) ? $this->args->sudo : null;
 
-                       $path = $temp->getPathname();
-                       $stat = stat($file);
-
-                       try {
-                               $this->info("Running INI owner transfer ... ");
-                               $ugid = sprintf("%d:%d", $stat["uid"], $stat["gid"]);
-                               $cmd = new ExecCmd("chown", $this->args->verbose);
-                               if (isset($this->args->sudo)) {
-                                       $cmd->setSu($this->args->sudo);
-                               }
-                               $cmd->run([$ugid, $path]);
-                               $this->info("OK\n");
-                               
-                               $this->info("Running INI permission transfer ... ");
-                               $perm = decoct($stat["mode"] & 0777);
-                               $cmd = new ExecCmd("chmod", $this->args->verbose);
-                               if (isset($this->args->sudo)) {
-                                       $cmd->setSu($this->args->sudo);
-                               }
-                               $cmd->run([$perm, $path]);
-                               $this->info("OK\n");
-       
-                               $this->info("Running INI activation ... ");
-                               $cmd = new ExecCmd("mv", $this->args->verbose);
-                               if (isset($this->args->sudo)) {
-                                       $cmd->setSu($this->args->sudo);
-                               }
-                               $cmd->run([$path, $file]);
-                               $this->info("OK\n");
-                       } catch (\Exception $e) {
-                               $this->error("%s\n", $e->getMessage());
-                               $this->error("%s\n", $cmd->getOutput());
-                               exit(5);
+               try {
+                       $this->info("Running INI activation ...\n");
+                       $activate = new Task\Activate($temp, $files, $sudo);
+                       if (!$activate->run($this->args->verbose)) {
+                               $this->info("Extension already activated ...\n");
                        }
+               } catch (\Exception $e) {
+                       $this->error("%s\n", $e->getMessage());
+                       $this->error("%s\n", $output);
+                       exit(3);
                }
        }
 }
index 35c596acf57b9ba5fb7379dec203e7fdbfcf07c8..481fd86aa85a8c6b90628dc1b8f3adb7322a1488 100644 (file)
@@ -2,6 +2,8 @@
 
 namespace pharext\Openssl;
 
+use pharext\Exception;
+
 class PrivateKey
 {
        /**
@@ -20,16 +22,16 @@ class PrivateKey
         * Read a private key
         * @param string $file
         * @param string $password
-        * @throws \Exception
+        * @throws \pharext\Exception
         */
        function __construct($file, $password) {
                /* there appears to be a bug with refcount handling of this
                 * resource; when the resource is stored as property, it cannot be
-                * "coerced to a private key" on openssl_sign() alter in another method
+                * "coerced to a private key" on openssl_sign() later in another method
                 */
                $key = openssl_pkey_get_private("file://$file", $password);
                if (!is_resource($key)) {
-                       throw new \Exception("Could not load private key");
+                       throw new Exception("Could not load private key");
                }
                openssl_pkey_export($key, $this->key);
                $this->pub = openssl_pkey_get_details($key)["key"];
@@ -46,11 +48,11 @@ class PrivateKey
        /**
         * Export the public key to a file
         * @param string $file
-        * @throws \Exception
+        * @throws \pharext\Exception
         */
        function exportPublicKey($file) {
                if (!file_put_contents("$file.tmp", $this->pub) || !rename("$file.tmp", $file)) {
-                       throw new \Exception(error_get_last()["message"]);
+                       throw new Exception;
                }
        }
 }
index d3e4000a3f45c505829b336c8528f8a40e531553..ba7de89100a9200623db6b8d1912139802d59e2d 100644 (file)
@@ -3,7 +3,6 @@
 namespace pharext;
 
 use Phar;
-use PharData;
 use pharext\Cli\Args as CliArgs;
 use pharext\Cli\Command as CliCommand;
 
@@ -58,6 +57,10 @@ class Packager implements Command
                                CliArgs::OPTIONAL|CliArgs::SINGLE|CliArgs::REQARG],
                        [null, "signature", "Dump signature",
                                CliArgs::OPTIONAL|CliArgs::SINGLE|CliArgs::NOARG|CliArgs::HALT],
+                       [null, "license", "Show license",
+                               CliArgs::OPTIONAL|CliArgs::SINGLE|CliArgs::NOARG|CliArgs::HALT],
+                       [null, "version", "Show version",
+                               CliArgs::OPTIONAL|CliArgs::SINGLE|CliArgs::NOARG|CliArgs::HALT],
                ]);
        }
        
@@ -68,7 +71,7 @@ class Packager implements Command
                foreach ($this->cleanup as $cleanup) {
                        if (is_dir($cleanup)) {
                                $this->rm($cleanup);
-                       } else {
+                       } elseif (file_exists($cleanup)) {
                                unlink($cleanup);
                        }
                }
@@ -90,8 +93,16 @@ class Packager implements Command
                        $this->help($prog);
                        exit;
                }
-               if ($this->args["signature"]) {
-                       exit($this->signature($prog));
+               try {
+                       foreach (["signature", "license", "version"] as $opt) {
+                               if ($this->args[$opt]) {
+                                       printf("%s\n", $this->metadata($opt));
+                                       exit;
+                               }
+                       }
+               } catch (\Exception $e) {
+                       $this->error("%s\n", $e->getMessage());
+                       exit(2);
                }
 
                try {
@@ -119,10 +130,7 @@ class Packager implements Command
                
                if ($errs) {
                        if (!$this->args["quiet"]) {
-                               if (!headers_sent()) {
-                                       /* only display header, if we didn't generate any output yet */
-                                       $this->header();
-                               }
+                               $this->header();
                        }
                        foreach ($errs as $err) {
                                $this->error("%s\n", $err);
@@ -136,72 +144,29 @@ class Packager implements Command
                
                $this->createPackage();
        }
-
-       /**
-        * Dump program signature
-        * @param string $prog
-        * @return int exit code
-        */
-       function signature($prog) {
-               try {
-                       $sig = (new Phar(Phar::running(false)))->getSignature();
-                       printf("%s signature of %s\n%s", $sig["hash_type"], $prog, 
-                               chunk_split($sig["hash"], 64, "\n"));
-                       return 0;
-               } catch (\Exception $e) {
-                       $this->error("%s\n", $e->getMessage());
-                       return 2;
-               }
-       }
-
+       
        /**
         * Download remote source
         * @param string $source
         * @return string local source
         */
        private function download($source) {
-               $this->info("Fetching remote source %s ... ", $source);
-               if ($this->args["git"]) {
-                       $local = new Tempdir("gitclone");
-                       $cmd = new ExecCmd("git", $this->args->verbose);
-                       $cmd->run(["clone", $source, $local]);
-                       if (!$this->args->verbose) {
-                               $this->info("OK\n");
-                       }
+               $this->info("Fetching remote source %s ...\n", $source);
+               
+               if ($this->args->git) {
+                       $task = new Task\GitClone($source);
                } else {
-                       $context = stream_context_create([],["notification" => function($notification, $severity, $message, $code, $bytes_cur, $bytes_max) {
-                               switch ($notification) {
-                                       case STREAM_NOTIFY_CONNECT:
-                                               $this->debug("\n");
-                                               break;
-                                       case STREAM_NOTIFY_PROGRESS:
-                                               if ($bytes_max) {
-                                                       $bytes_pct = $bytes_cur/$bytes_max;
-                                                       $this->debug("\r %3d%% [%s>%s] ",
-                                                               $bytes_pct*100,
-                                                               str_repeat("=", round(70*$bytes_pct)), 
-                                                               str_repeat(" ", round(70*(1-$bytes_pct)))
-                                                       );
-                                               }
-                                               break;
-                                       case STREAM_NOTIFY_COMPLETED:
-                                               /* this is not generated, why? */
-                                               break;
-                               }
-                       }]);
-                       if (!$remote = fopen($source, "r", false, $context)) {
-                               $this->error(null);
-                               exit(2);
-                       }
-                       $local = new Tempfile("remote");
-                       if (!stream_copy_to_stream($remote, $local->getStream())) {
-                               $this->error(null);
-                               exit(2);
-                       }
-                       $local->closeStream();
-                       $this->info("OK\n");
+                       $task = new Task\StreamFetch($source, function($bytes_pct) {
+                               $this->debug(" %3d%% [%s>%s] \r",
+                                       floor($bytes_pct*100),
+                                       str_repeat("=", round(50*$bytes_pct)),
+                                       str_repeat(" ", round(50*(1-$bytes_pct)))
+                               );
+                       });
                }
-               
+               $local = $task->run($this->args->verbose);
+               $this->debug("\n");
+
                $this->cleanup[] = $local;
                return $local;
        }
@@ -212,11 +177,11 @@ class Packager implements Command
         * @return string extracted directory
         */
        private function extract($source) {
-               $dest = new Tempdir("local");
-               $this->debug("Extracting %s to %s ... ", $source, $dest);
-               $archive = new PharData($source);
-               $archive->extractTo($dest);
-               $this->debug("OK\n");
+               $this->debug("Extracting %s ...\n", $source);
+               
+               $task = new Task\Extract($source);
+               $dest = $task->run($this->args->verbose);
+               
                $this->cleanup[] = $dest;
                return $dest;
        }
@@ -229,134 +194,96 @@ class Packager implements Command
        private function localize($source) {
                if (!stream_is_local($source)) {
                        $source = $this->download($source);
+                       $this->cleanup[] = $source;
                }
+               $source = realpath($source);
                if (!is_dir($source)) {
                        $source = $this->extract($source);
-                       if ($this->args["pecl"]) {
-                               $this->debug("Sanitizing PECL dir ... ");
-                               $dirs = glob("$source/*", GLOB_ONLYDIR);
-                               $files = array_diff(glob("$source/*"), $dirs);
-                               $source = current($dirs);
-                               foreach ($files as $file) {
-                                       rename($file, "$source/" . basename($file));
-                               }
-                               $this->debug("OK\n");
+                       $this->cleanup[] = $source;
+                       
+                       if ($this->args->pecl) {
+                               $this->debug("Sanitizing PECL dir ...\n");
+                               $source = (new Task\PeclFixup($source))->run($this->args->verbose);
                        }
                }
                return $source;
        }
 
-       /**
-        * Traverses all pharext source files to bundle
-        * @return Generator
-        */
-       private function bundle() {
-               $rdi = new \RecursiveDirectoryIterator(__DIR__);
-               $rii = new \RecursiveIteratorIterator($rdi);
-               for ($rii->rewind(); $rii->valid(); $rii->next()) {
-                       yield "pharext/". $rii->getSubPathname() => $rii->key();
-                       
-               }
-       }
-
-       /**
-        * Ask for password on the console
-        * @param string $prompt
-        * @return string password
-        */
-       private function askpass($prompt = "Password:") {
-               system("stty -echo", $retval);
-               if ($retval) {
-                       $this->error("Could not disable echo on the terminal\n");
-               }
-               printf("%s ", $prompt);
-               $pass = fgets(STDIN, 1024);
-               system("stty echo");
-               if (substr($pass, -1) == "\n") {
-                       $pass = substr($pass, 0, -1);
-               }
-               return $pass;
-       }
-
        /**
         * Creates the extension phar
         */
        private function createPackage() {
-               $pkguniq = uniqid();
-               $pkgtemp = sprintf("%s/%s.phar", sys_get_temp_dir(), $pkguniq);
-               $pkgdesc = "{$this->args->name}-{$this->args->release}";
-       
-               $this->info("Creating phar %s ...%s", $pkgtemp, $this->args->verbose ? "\n" : " ");
                try {
-                       $package = new Phar($pkgtemp);
+                       $meta = array_merge($this->metadata(), [
+                               "date" => date("Y-m-d"),
+                               "name" => $this->args->name,
+                               "release" => $this->args->release,
+                               "license" => @file_get_contents(current(glob($this->source->getBaseDir()."/LICENSE*"))),
+                               "stub" => "pharext_installer.php",
+                       ]);
+                       $file = (new Task\PharBuild($this->source, $meta))->run();
 
                        if ($this->args->sign) {
-                               $this->info("\nUsing private key to sign phar ... \n");
-                               $privkey = new Openssl\PrivateKey(realpath($this->args->sign), $this->askpass());
-                               $privkey->sign($package);
+                               $this->info("Using private key to sign phar ...\n");
+                               $pass = (new Task\Askpass)->run($this->args->verbose);
+                               $sign = new Task\PharSign($file, $this->args->sign, $pass);
+                               $pkey = $sign->run($this->args->verbose);
                        }
 
-                       $package->startBuffering();
-                       $package->buildFromIterator($this->source, $this->source->getBaseDir());
-                       $package->buildFromIterator($this->bundle(__DIR__));
-                       $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();
+               } catch (\Exception $e) {
+                       $this->error("%s\n", $e->getMessage());
+                       exit(4);
+               }
+
+               if ($this->args->gzip) {
+                       try {
+                               $gzip = (new Task\PharCompress($file, Phar::GZ))->run();
+                               $move = new Task\PharRename($gzip, $this->args->dest, $this->args->name ."-". $this->args->release);
+                               $name = $move->run($this->args->verbose);
 
-                       if (!chmod($pkgtemp, 0777)) {
-                               $this->error(null);
-                       } elseif ($this->args->verbose) {
-                               $this->debug("Created executable phar %s\n", $pkgtemp);
-                       } else {
-                               $this->info("OK\n");
-                       }
-                       if ($this->args->gzip) {
-                               $this->info("Compressing with gzip ... ");
-                               try {
-                                       $package->compress(Phar::GZ)
-                                               ->setDefaultStub("pharext_installer.php");
-                                       $this->info("OK\n");
-                               } catch (\Exception $e) {
-                                       $this->error("%s\n", $e->getMessage());
+                               $this->info("Created gzipped phar %s\n", $name);
+
+                               if ($this->args->sign) {
+                                       $sign = new Task\PharSign($name, $this->args->sign, $pass);
+                                       $sign->run($this->args->verbose)->exportPublicKey($name.".pubkey");
                                }
+
+                       } catch (\Exception $e) {
+                               $this->warn("%s\n", $e->getMessage());
                        }
-                       if ($this->args->bzip) {
-                               $this->info("Compressing with bzip ... ");
-                               try {
-                                       $package->compress(Phar::BZ2)
-                                               ->setDefaultStub("pharext_installer.php");
-                                       $this->info("OK\n");
-                               } catch (\Exception $e) {
-                                       $this->error("%s\n", $e->getMessage());
+               }
+               
+               if ($this->args->bzip) {
+                       try {
+                               $bzip = (new Task\PharCompress($file, Phar::BZ2))->run();
+                               $move = new Task\PharRename($bzip, $this->args->dest, $this->args->name ."-". $this->args->release);
+                               $name = $move->run($this->args->verbose);
+
+                               $this->info("Created bzipped phar %s\n", $name);
+
+                               if ($this->args->sign) {
+                                       $sign = new Task\PharSign($name, $this->args->sign, $pass);
+                                       $sign->run($this->args->verbose)->exportPublicKey($name.".pubkey");
                                }
+
+                       } catch (\Exception $e) {
+                               $this->warn("%s\n", $e->getMessage());
+                       }
+               }
+
+               try {
+                       $move = new Task\PharRename($file, $this->args->dest, $this->args->name ."-". $this->args->release);
+                       $name = $move->run($this->args->verbose);
+
+                       $this->info("Created executable phar %s\n", $name);
+
+                       if (isset($pkey)) {
+                               $pkey->exportPublicKey($name.".pubkey");
                        }
 
-                       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(null);
-                               exit(5);
-                       }
-                       $this->info("OK\n");
-                       if ($this->args->sign && isset($privkey)) {
-                               $keyname = $this->args->dest ."/". basename($pkgfile) . ".pubkey";
-                               $this->info("Public Key %s ... ", $keyname);
-                               try {
-                                       $privkey->exportPublicKey($keyname);
-                                       $this->info("OK\n");
-                               } catch (\Exception $e) {
-                                       $this->error("%s", $e->getMessage());
-                               }
-                       }
-               } 
        }
 }
index 531aa6535f6e29c1b114945e973f8ad819968940..8e35bf9b3f50fb292501f6c02652f090397685e8 100644 (file)
@@ -55,7 +55,7 @@ class Git implements \IteratorAggregate, SourceDir
                                        }
                                        /* there may be symlinks, so no realpath here */
                                        if (!file_exists("$path/$file")) {
-                                               $this->cmd->error("File %s does not exist in %s\n", $file, $path);
+                                               $this->cmd->warn("File %s does not exist in %s\n", $file, $path);
                                        }
                                        yield "$path/$file";
                                }
index d21a0e0de313a5155a48bb88424ef377dea80769..0b7055a99da64e0afb162490a18944bbeffd030c 100644 (file)
@@ -3,6 +3,7 @@
 namespace pharext\SourceDir;
 
 use pharext\Command;
+use pharext\Exception;
 use pharext\SourceDir;
 
 /**
@@ -38,7 +39,7 @@ class Pecl implements \IteratorAggregate, SourceDir
                } elseif (realpath("$path/package.xml")) {
                        $sxe = simplexml_load_file("$path/package.xml");
                } else {
-                       throw new \Exception("Missing package.xml in $path");
+                       throw new Exception("Missing package.xml in $path");
                }
                $sxe->registerXPathNamespace("pecl", $sxe->getDocNamespaces()[""]);
                
@@ -46,14 +47,14 @@ class Pecl implements \IteratorAggregate, SourceDir
                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);
+                               $cmd->warn("%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);
+                               $cmd->warn("%s\n", $error);
                        }
                }
                
@@ -89,6 +90,7 @@ class Pecl implements \IteratorAggregate, SourceDir
         * @return string
         */
        private static function loadHook($configure, $dependencies) {
+               require_once "pharext/Version.php";
                return include __DIR__."/../../pharext_install.tpl.php";
        }
 
@@ -106,7 +108,7 @@ class Pecl implements \IteratorAggregate, SourceDir
                                                substr($b, strpos(".ext.phar", $b))
                                        );
                                });
-                               yield realpath($this->path."/".end($glob));
+                               yield end($glob);
                        } else {
                                unset($dependencies[$key]);
                        }
@@ -132,17 +134,17 @@ class Pecl implements \IteratorAggregate, SourceDir
        private function generateFiles() {
                foreach ($this->generateHooks() as $file => $hook) {
                        if ($this->cmd->getArgs()->verbose) {
-                               $this->cmd->info("Packaging %s\n", is_string($hook) ? $hook : $file);
+                               $this->cmd->info("Packaging %s\n", is_scalar($hook) ? $hook : $file);
                        }
                        yield $file => $hook;
                }
                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);
+                               $this->cmd->info("Packaging %s\n", substr($path, strlen($this->path)));
                        }
                        if (!($realpath = realpath($path))) {
-                               $this->cmd->error("File %s does not exist", $path);
+                               $this->cmd->warn("File %s does not exist", $path);
                        }
                        yield $realpath;
                }
index 2675a7899836d70d07b9e36b4b0d1ee637ad2150..8ca0901c24e696652e3ef0afa9f87b322e896f77 100644 (file)
@@ -3,6 +3,7 @@
 namespace pharext\SourceDir;
 
 use pharext\Command;
+use pharext\Exception;
 use pharext\SourceDir;
 
 /**
@@ -35,7 +36,7 @@ class Pharext implements \IteratorAggregate, SourceDir
                
                $callable = include "$path/pharext_package.php";
                if (!is_callable($callable)) {
-                       throw new \Exception("Package hook did not return a callable");
+                       throw new Exception("Package hook did not return a callable");
                }
                $this->iter = $callable($cmd, $path);
        }
diff --git a/src/pharext/Task.php b/src/pharext/Task.php
new file mode 100644 (file)
index 0000000..473e1a6
--- /dev/null
@@ -0,0 +1,11 @@
+<?php
+
+namespace pharext;
+
+/**
+ * Simple task interface
+ */
+interface Task
+{
+       public function run($verbose = false);
+}
diff --git a/src/pharext/Task/Activate.php b/src/pharext/Task/Activate.php
new file mode 100644 (file)
index 0000000..29341cc
--- /dev/null
@@ -0,0 +1,94 @@
+<?php
+
+namespace pharext\Task;
+
+use pharext\Exception;
+use pharext\ExecCmd;
+use pharext\Task;
+use pharext\Tempfile;
+
+/**
+ * PHP INI activation
+ */
+class Activate implements Task
+{
+       /**
+        * @var string
+        */
+       private $cwd;
+
+       /**
+        * @var array
+        */
+       private $inis;
+
+       /**
+        * @var string
+        */
+       private $sudo;
+
+       /**
+        * @param string $cwd working directory
+        * @param array $inis custom INI or list of loaded/scanned INI files
+        * @param string $sudo sudo command
+        * @throws \pharext\Exception
+        */
+       public function __construct($cwd, array $inis, $sudo = null) {
+               $this->cwd = $cwd;
+               $this->sudo = $sudo;
+               if (!$this->inis = $inis) {
+                       throw new Exception("No PHP INIs given");
+               }
+       }
+
+       /**
+        * @param bool $verbose
+        * @return boolean false, if extension was already activated
+        */
+       public function run($verbose = false) {
+               $extension = basename(current(glob("{$this->cwd}/modules/*.so")));
+               $pattern = preg_quote($extension);
+
+               foreach ($this->inis as $file) {
+                       $temp = new Tempfile("phpini");
+                       foreach (file($file) as $line) {
+                               if (preg_match("/^\s*extension\s*=\s*[\"']?{$pattern}[\"']?\s*(;.*)?\$/", $line)) {
+                                       return false;
+                               }
+                               fwrite($temp->getStream(), $line);
+                       }
+               }
+
+               /* not found; append to last processed file, which is the main by default */
+               fprintf($temp->getStream(), "extension=%s\n", $extension);
+               $temp->closeStream();
+
+               $path = $temp->getPathname();
+               $stat = stat($file);
+
+               // owner transfer
+               $ugid = sprintf("%d:%d", $stat["uid"], $stat["gid"]);
+               $cmd = new ExecCmd("chown", $verbose);
+               if (isset($this->sudo)) {
+                       $cmd->setSu($this->sudo);
+               }
+               $cmd->run([$ugid, $path]);
+
+               // permission transfer
+               $perm = decoct($stat["mode"] & 0777);
+               $cmd = new ExecCmd("chmod", $verbose);
+               if (isset($this->sudo)) {
+                       $cmd->setSu($this->sudo);
+               }
+               $cmd->run([$perm, $path]);
+
+               // rename
+               $cmd = new ExecCmd("mv", $verbose);
+               if (isset($this->sudo)) {
+                       $cmd->setSu($this->sudo);
+               }
+               $cmd->run([$path, $file]);
+
+               return true;
+       }
+}
diff --git a/src/pharext/Task/Askpass.php b/src/pharext/Task/Askpass.php
new file mode 100644 (file)
index 0000000..24b22da
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+
+namespace pharext\Task;
+
+use pharext\Exception;
+use pharext\Task;
+
+/**
+ * Ask password on console
+ */
+class Askpass implements Task
+{
+       /**
+        * @var string
+        */
+       private $prompt;
+
+       /**
+        * @param string $prompt
+        */
+       public function __construct($prompt = "Password:") {
+               $this->prompt = $prompt;
+       }
+
+       /**
+        * @param bool $verbose
+        * @return string
+        */
+       public function run($verbose = false) {
+               system("stty -echo");
+               printf("%s ", $this->prompt);
+               $pass = fgets(STDIN, 1024);
+               printf("\n");
+               system("stty echo");
+               if (substr($pass, -1) == "\n") {
+                       $pass = substr($pass, 0, -1);
+               }
+               return $pass;
+       }
+}
\ No newline at end of file
diff --git a/src/pharext/Task/BundleGenerator.php b/src/pharext/Task/BundleGenerator.php
new file mode 100644 (file)
index 0000000..aabf9c8
--- /dev/null
@@ -0,0 +1,28 @@
+<?php
+
+namespace pharext\Task;
+
+use pharext\Task;
+
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
+
+/**
+ * List all library files of pharext to bundle with a phar
+ */
+class BundleGenerator implements Task
+{
+       /**
+        * @param bool $verbose
+        * @return Generator
+        */
+       public function run($verbose = false) {
+               $rdi = new RecursiveDirectoryIterator(dirname(dirname(__DIR__)));
+               $rii = new RecursiveIteratorIterator($rdi);
+               for ($rii->rewind(); $rii->valid(); $rii->next()) {
+                       if (!$rii->isDot()) {
+                               yield $rii->getSubPathname() => $rii->key();
+                       }
+               }
+       }
+}
diff --git a/src/pharext/Task/Configure.php b/src/pharext/Task/Configure.php
new file mode 100644 (file)
index 0000000..3effbfb
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+
+namespace pharext\Task;
+
+use pharext\Exception;
+use pharext\ExecCmd;
+use pharext\Task;
+
+/**
+ * Runs extension's configure
+ */
+class Configure implements Task
+{
+       /**
+        * @var array
+        */
+       private $args;
+
+       /**
+        * @var string
+        */
+       private $cwd;
+
+       /**
+        * @param string $cwd working directory
+        * @param array $args configure args
+        * @param string $prefix install prefix, e.g. /usr/local
+        * @param string $common_name PHP programs common name, e.g. php5
+        */
+       public function __construct($cwd, array $args = null, $prefix = null, $common_name = "php") {
+               $this->cwd = $cwd;
+               $cmd = $common_name . "-config";
+               if (isset($prefix)) {
+                       $cmd = $prefix . "/bin/" . $cmd;
+               }
+               $this->args =  ["--with-php-config=$cmd"];
+               if ($args) {
+                       $this->args = array_merge($this->args, $args);
+               }
+       }
+
+       public function run($verbose = false) {
+               $pwd = getcwd();
+               if (!chdir($this->cwd)) {
+                       throw new Exception;
+               }
+               try {
+                       $cmd = new ExecCmd("./configure", $verbose);
+                       $cmd->run($this->args);
+               } finally {
+                       chdir($pwd);
+               }
+       }
+}
diff --git a/src/pharext/Task/Extract.php b/src/pharext/Task/Extract.php
new file mode 100644 (file)
index 0000000..b2f954f
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+
+namespace pharext\Task;
+
+use pharext\Task;
+use pharext\Tempdir;
+
+use Phar;
+use PharData;
+
+/**
+ * Extract a package archive
+ */
+class Extract implements Task
+{
+       /**
+        * @var Phar(Data)
+        */
+       private $source;
+
+       /**
+        * @param mixed $source archive location
+        */
+       public function __construct($source) {
+               if ($source instanceof Phar || $source instanceof PharData) {
+                       $this->source = $source;
+               } else {
+                       $this->source = new PharData($source);
+               }
+       }
+
+       /**
+        * @param bool $verbose
+        * @return \pharext\Tempdir
+        */
+       public function run($verbose = false) {
+               $dest = new Tempdir("extract");
+               $this->source->extractTo($dest);
+               return $dest;
+       }
+}
diff --git a/src/pharext/Task/GitClone.php b/src/pharext/Task/GitClone.php
new file mode 100644 (file)
index 0000000..709a34a
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+
+namespace pharext\Task;
+
+use pharext\ExecCmd;
+use pharext\Task;
+use pharext\Tempdir;
+
+/**
+ * Clone a git repo
+ */
+class GitClone implements Task
+{
+       /**
+        * @var string
+        */
+       private $source;
+
+       /**
+        * @param string $source git repo location
+        */
+       public function __construct($source) {
+               $this->source = $source;
+       }
+
+       /**
+        * @param bool $verbose
+        * @return \pharext\Tempdir
+        */
+       public function run($verbose = false) {
+               $local = new Tempdir("gitclone");
+               $cmd = new ExecCmd("git", $verbose);
+               $cmd->run(["clone", $this->source, $local]);
+               return $local;
+       }
+}
diff --git a/src/pharext/Task/Make.php b/src/pharext/Task/Make.php
new file mode 100644 (file)
index 0000000..9e71565
--- /dev/null
@@ -0,0 +1,65 @@
+<?php
+
+namespace pharext\Task;
+
+use pharext\ExecCmd;
+use pharext\Exception;
+use pharext\Task;
+
+/**
+ * Run make in the source dir
+ */
+class Make implements Task
+{
+       /**
+        * @var string
+        */
+       private $cwd;
+
+       /**
+        * @var array
+        */
+       private $args;
+
+       /**
+        * @var string
+        */
+       private $sudo;
+
+       /**
+        *
+        * @param string $cwd working directory
+        * @param array $args make's arguments
+        * @param string $sudo sudo command
+        */
+       public function __construct($cwd, array $args = null, $sudo = null) {
+               $this->cwd = $cwd;
+               $this->sudo = $sudo;
+               $this->args = $args;
+       }
+
+       /**
+        *
+        * @param bool $verbose
+        * @throws \pharext\Exception
+        */
+       public function run($verbose = false) {
+               $pwd = getcwd();
+               if (!chdir($this->cwd)) {
+                       throw new Exception;
+               }
+               try {
+                       $cmd = new ExecCmd("make", $verbose);
+                       if (isset($this->sudo)) {
+                               $cmd->setSu($this->sudo);
+                       }
+                       $args = $this->args;
+                       if (!$verbose) {
+                               $args = array_merge((array) $args, ["-s"]);
+                       }
+                       $cmd->run($args);
+               } finally {
+                       chdir($pwd);
+               }
+       }
+}
diff --git a/src/pharext/Task/PeclFixup.php b/src/pharext/Task/PeclFixup.php
new file mode 100644 (file)
index 0000000..08a1d94
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+
+namespace pharext\Task;
+
+use pharext\Exception;
+use pharext\Task;
+
+/**
+ * Fixup package.xml files in an extracted PECL dir
+ */
+class PeclFixup implements Task
+{
+       /**
+        * @var string
+        */
+       private $source;
+
+       /**
+        * @param string $source source directory
+        */
+       public function __construct($source) {
+               $this->source = $source;
+       }
+
+       /**
+        * @param bool $verbose
+        * @return string sanitized source location
+        * @throws \pahrext\Exception
+        */
+       public function run($verbose = false) {
+               $dirs = glob("{$this->source}/*", GLOB_ONLYDIR);
+               $files = array_diff(glob("{$this->source}/*"), $dirs);
+
+               if (count($dirs) !== 1 || !count($files)) {
+                       throw new Exception("Does not look like an extracted PECL dir: {$this->source}");
+               }
+
+               $dest = current($dirs);
+
+               foreach ($files as $file) {
+                       if (!rename($file, "$dest/" . basename($file))) {
+                               throw new Exception;
+                       }
+               }
+
+               return $dest;
+       }
+}
diff --git a/src/pharext/Task/PharBuild.php b/src/pharext/Task/PharBuild.php
new file mode 100644 (file)
index 0000000..d6f2788
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+
+namespace pharext\Task;
+
+use pharext\Exception;
+use pharext\SourceDir;
+use pharext\Task;
+use pharext\Tempname;
+
+use Phar;
+
+/**
+ * Build phar
+ */
+class PharBuild implements Task
+{
+       /**
+        * @var \pharext\SourceDir
+        */
+       private $source;
+
+       /**
+        * @var array
+        */
+       private $meta;
+
+       /**
+        * @var bool
+        */
+       private $readonly;
+
+       /**
+        * @param SourceDir $source extension source directory
+        * @param array $meta phar meta data
+        * @param bool $readonly whether the stub has -dphar.readonly=1 set
+        */
+       public function __construct(SourceDir $source = null, array $meta = null, $readonly = true) {
+               $this->source = $source;
+               $this->meta = $meta;
+               $this->readonly = $readonly;
+       }
+
+       /**
+        * @param bool $verbose
+        * @return \pharext\Tempname
+        * @throws \pharext\Exception
+        */
+       public function run($verbose = false) {
+               /* Phar::compress() and ::convert*() use strtok("."), ugh!
+                * so, be sure to not use any other dots in the filename
+                * except for .phar
+                */
+               $temp = new Tempname("", "-pharext.phar");
+
+               $phar = new Phar($temp);
+               $phar->startBuffering();
+
+               if ($this->meta) {
+                       $phar->setMetadata($this->meta);
+                       if (isset($this->meta["stub"])) {
+                               $phar->setDefaultStub($this->meta["stub"]);
+                               $phar->setStub("#!/usr/bin/php -dphar.readonly=" .
+                                       intval($this->readonly) ."\n".
+                                       $phar->getStub());
+                       }
+               }
+
+               $phar->buildFromIterator((new Task\BundleGenerator)->run());
+
+               if ($this->source) {
+                       $phar->buildFromIterator($this->source, $this->source->getBaseDir());
+               }
+
+               $phar->stopBuffering();
+
+               if (!chmod($temp, fileperms($temp) | 0111)) {
+                       throw new Exception;
+               }
+               
+               return $temp;
+       }
+}
\ No newline at end of file
diff --git a/src/pharext/Task/PharCompress.php b/src/pharext/Task/PharCompress.php
new file mode 100644 (file)
index 0000000..d090e28
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+
+namespace pharext\Task;
+
+use pharext\Task;
+
+use Phar;
+
+/**
+ * Clone a compressed copy of a phar
+ */
+class PharCompress implements Task
+{
+       /**
+        * @var string
+        */
+       private $file;
+
+       /**
+        * @var Phar
+        */
+       private $package;
+
+       /**
+        * @var int
+        */
+       private $encoding;
+
+       /**
+        * @var string
+        */
+       private $extension;
+
+       /**
+        * @param string $file path to the original phar
+        * @param int $encoding Phar::GZ or Phar::BZ2
+        */
+       public function __construct($file, $encoding) {
+               $this->file = $file;
+               $this->package = new Phar($file);
+               $this->encoding = $encoding;
+
+               switch ($encoding) {
+                       case Phar::GZ:
+                               $this->extension = ".gz";
+                               break;
+                       case Phar::BZ2:
+                               $this->extension = ".bz2";
+                               break;
+               }
+       }
+
+       /**
+        * @param bool $verbose
+        * @return string
+        */
+       public function run($verbose = false) {
+               $phar = $this->package->compress($this->encoding);
+               $meta = $phar->getMetadata();
+               if (isset($meta["stub"])) {
+                       /* drop shebang */
+                       $phar->setDefaultStub($meta["stub"]);
+               }
+               return $this->file . $this->extension;
+       }
+}
diff --git a/src/pharext/Task/PharRename.php b/src/pharext/Task/PharRename.php
new file mode 100644 (file)
index 0000000..19094c1
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+
+namespace pharext\Task;
+
+use pharext\Exception;
+use pharext\Task;
+
+/**
+ * Rename the phar archive
+ */
+class PharRename implements Task
+{
+       /**
+        * @var string
+        */
+       private $phar;
+
+       /**
+        * @var string
+        */
+       private $dest;
+
+       /**
+        * @var string
+        */
+       private $name;
+
+       /**
+        * @param string $phar path to phar
+        * @param string $dest destination dir
+        * @param string $name package name
+        */
+       public function __construct($phar, $dest, $name) {
+               $this->phar = $phar;
+               $this->dest = $dest;
+               $this->name = $name;
+       }
+
+       /**
+        * @param bool $verbose
+        * @return string path to renamed phar
+        * @throws \pharext\Exception
+        */
+       public function run($verbose = false) {
+               $extension = substr(strstr($this->phar, "-pharext.phar"), 8);
+               $name = sprintf("%s/%s.ext%s", $this->dest, $this->name, $extension);
+
+               if (!rename($this->phar, $name)) {
+                       throw new Exception;
+               }
+
+               return $name;
+       }
+}
diff --git a/src/pharext/Task/PharSign.php b/src/pharext/Task/PharSign.php
new file mode 100644 (file)
index 0000000..739c587
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+
+namespace pharext\Task;
+
+use pharext\Openssl;
+use pharext\Task;
+
+use Phar;
+
+/**
+ * Sign the phar with a private key
+ */
+class PharSign implements Task
+{
+       /**
+        * @var Phar
+        */
+       private $phar;
+
+       /**
+        * @var \pharext\Openssl\PrivateKey
+        */
+       private $pkey;
+
+       /**
+        *
+        * @param mixed $phar phar instance or path to phar
+        * @param string $pkey path to private key
+        * @param string $pass password for the private key
+        */
+       public function __construct($phar, $pkey, $pass) {
+               if ($phar instanceof Phar || $phar instanceof PharData) {
+                       $this->phar = $phar;
+               } else {
+                       $this->phar = new Phar($phar);
+               }
+               $this->pkey = new Openssl\PrivateKey($pkey, $pass);
+       }
+
+       /**
+        * @param bool $verbose
+        * @return \pharext\Openssl\PrivateKey
+        */
+       public function run($verbose = false) {
+               $this->pkey->sign($this->phar);
+               return $this->pkey;
+       }
+}
diff --git a/src/pharext/Task/Phpize.php b/src/pharext/Task/Phpize.php
new file mode 100644 (file)
index 0000000..ab70534
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+
+namespace pharext\Task;
+
+use pharext\Exception;
+use pharext\ExecCmd;
+use pharext\Task;
+
+/**
+ * Run phpize in the extension source directory
+ */
+class Phpize implements Task
+{
+       /**
+        * @var string
+        */
+       private $phpize;
+
+       /**
+        *
+        * @var string
+        */
+       private $cwd;
+
+       /**
+        * @param string $cwd working directory
+        * @param string $prefix install prefix, e.g. /usr/local
+        * @param string $common_name PHP program common name, e.g. php5
+        */
+       public function __construct($cwd, $prefix = null,  $common_name = "php") {
+               $this->cwd = $cwd;
+               $cmd = $common_name . "ize";
+               if (isset($prefix)) {
+                       $cmd = $prefix . "/bin/" . $cmd;
+               }
+               $this->phpize = $cmd;
+       }
+
+       /**
+        * @param bool $verbose
+        * @throws \pharext\Exception
+        */
+       public function run($verbose = false) {
+               $pwd = getcwd();
+               if (!chdir($this->cwd)) {
+                       throw new Exception;
+               }
+               try {
+                       $cmd = new ExecCmd($this->phpize, $verbose);
+                       $cmd->run();
+               } finally {
+                       chdir($pwd);
+               }
+       }
+}
diff --git a/src/pharext/Task/StreamFetch.php b/src/pharext/Task/StreamFetch.php
new file mode 100644 (file)
index 0000000..4f090ef
--- /dev/null
@@ -0,0 +1,75 @@
+<?php
+
+namespace pharext\Task;
+
+use pharext\Exception;
+use pharext\Task;
+use pharext\Tempfile;
+
+/**
+ * Fetch a remote archive
+ */
+class StreamFetch implements Task
+{
+       /**
+        * @var string
+        */
+       private $source;
+
+       /**
+        * @var callable
+        */
+       private $progress;
+
+       /**
+        * @param string $source remote file location
+        * @param callable $progress progress callback
+        */
+       public function __construct($source, callable $progress) {
+               $this->source = $source;
+               $this->progress = $progress;
+       }
+
+       private function createStreamContext() {
+               $progress = $this->progress;
+
+               return stream_context_create([],["notification" => function($notification, $severity, $message, $code, $bytes_cur, $bytes_max) use($progress) {
+                       switch ($notification) {
+                               case STREAM_NOTIFY_CONNECT:
+                                       $progress(0);
+                                       break;
+                               case STREAM_NOTIFY_PROGRESS:
+                                       $progress($bytes_max ? $bytes_cur/$bytes_max : .5);
+                                       break;
+                               case STREAM_NOTIFY_COMPLETED:
+                                       /* this is not generated, why? */
+                                       $progress(1);
+                                       break;
+                       }
+               }]);
+       }
+
+       /**
+        * @param bool $verbose
+        * @return \pharext\Task\Tempfile
+        * @throws \pharext\Exception
+        */
+       public function run($verbose = false) {
+               $context = $this->createStreamContext();
+
+               if (!$remote = fopen($this->source, "r", false, $context)) {
+                       throw new Exception;
+               }
+               
+               $local = new Tempfile("remote");
+               if (!stream_copy_to_stream($remote, $local->getStream())) {
+                       throw new Exception;
+               }
+               $local->closeStream();
+
+               /* STREAM_NOTIFY_COMPLETED is not generated, see above */
+               call_user_func($this->progress, 1);
+
+               return $local;
+       }
+}
index 2de174dfc8272889ab2d57677b7ea984eeaa39e5..eb592637e5ea921640bdce316549c676a285217f 100644 (file)
@@ -2,16 +2,19 @@
 
 namespace pharext;
 
+/**
+ * Create a temporary directory
+ */
 class Tempdir extends \SplFileInfo
 {
-       private $dir;
-       
+       /**
+        * @param string $prefix prefix to uniqid()
+        * @throws \pharext\Exception
+        */
        public function __construct($prefix) {
-               $temp = sprintf("%s/%s", sys_get_temp_dir(), uniqid($prefix));
-               if (!is_dir($temp)) {
-                       if (!mkdir($temp, 0700, true)) {
-                               throw new Exception("Could not create tempdir: ".error_get_last()["message"]);
-                       }
+               $temp = new Tempname($prefix);
+               if (!is_dir($temp) && !mkdir($temp, 0700, true)) {
+                       throw new Exception("Could not create tempdir: ".error_get_last()["message"]);
                }
                parent::__construct($temp);
        }
index c890cd92262b1ec163bb6d9dc19d5959343afa1e..e7205519ea784869a58c821d132609091a4720c3 100644 (file)
@@ -2,38 +2,56 @@
 
 namespace pharext;
 
+/**
+ * Create a new temporary file
+ */
 class Tempfile extends \SplFileInfo
 {
+       /**
+        * @var resource
+        */
        private $handle;
-       
-       function __construct($prefix) {
+
+       /**
+        * @param string $prefix uniqid() prefix
+        * @param string $suffix e.g. file extension
+        * @throws \pharext\Exception
+        */
+       public function __construct($prefix, $suffix = ".tmp") {
                $tries = 0;
-               /* PharData needs a dot in the filename, sure */
-               $temp = sys_get_temp_dir() . "/";
-               
                $omask = umask(077);
                do {
-                       $path = $temp.uniqid($prefix).".tmp";
+                       $path = new Tempname($prefix, $suffix);
                        $this->handle = fopen($path, "x");
                } while (!is_resource($this->handle) && $tries++ < 10);
                umask($omask);
                
                if (!is_resource($this->handle)) {
-                       throw new \Exception("Could not create temporary file");
+                       throw new Exception("Could not create temporary file");
                }
                
                parent::__construct($path);
        }
-       
-       function __destruct() {
+
+       /**
+        * Unlink the file
+        */
+       public function __destruct() {
                @unlink($this->getPathname());
        }
-       
-       function closeStream() {
+
+       /**
+        * Close the stream
+        */
+       public function closeStream() {
                fclose($this->handle);
        }
 
-       function getStream() {
+       /**
+        * Retrieve the stream resource
+        * @return resource
+        */
+       public function getStream() {
                return $this->handle;
        }
 }
diff --git a/src/pharext/Tempname.php b/src/pharext/Tempname.php
new file mode 100644 (file)
index 0000000..b5e7520
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+
+namespace pharext;
+
+/**
+ * A temporary file/directory name
+ */
+class Tempname
+{
+       /**
+        * @var string
+        */
+       private $name;
+
+       /**
+        * @param string $prefix uniqid() prefix
+        * @param string $suffix e.g. file extension
+        */
+       public function __construct($prefix, $suffix = null) {
+               $this->name = sys_get_temp_dir() . "/" . uniqid($prefix) . $suffix;
+       }
+
+       /**
+        * @return string
+        */
+       public function __toString() {
+               return (string) $this->name;
+       }
+}
index 04d84b257dff5c0e43736b2db797a06b2ff24bea..f7a356d8b27217c10a170212cf572d4b1e2ae674 100644 (file)
@@ -1,7 +1,7 @@
 <?='<?php'?>
 
 /**
- * Generated by pharext v<?=pharext\VERSION?> at <?=date("Y-m-d H:i:i T")?>.
+ * Generated by pharext v<?=pharext\VERSION?> at <?=date("Y-m-d H:i:s T")?>.
  */
 namespace pharext;