From: Michael Wallner Date: Tue, 24 Mar 2015 16:58:24 +0000 (+0100) Subject: major refactoring under the hood X-Git-Tag: v3.0.0~19 X-Git-Url: https://git.m6w6.name/?p=pharext%2Fpharext;a=commitdiff_plain;h=861260c111bff72f60665393660b6f5375559510 major refactoring under the hood --- diff --git a/LICENSE b/LICENSE index 3f7d0f6..ab8ebb2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2015, Michael Wallner . +Copyright (c) 2015, Michael Wallner . All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/Makefile b/Makefile index 9697343..433b800 100644 --- 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 ... " diff --git a/bin/pharext b/bin/pharext index 8e38598..dbde37d 100755 Binary files a/bin/pharext and b/bin/pharext differ diff --git a/build/create-phar.php b/build/create-phar.php index db9013f..4836ceb 100644 --- a/build/create-phar.php +++ b/build/create-phar.php @@ -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 ", 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"); diff --git a/src/pharext/Cli/Command.php b/src/pharext/Cli/Command.php index e19ce93..0cc0bb4 100644 --- a/src/pharext/Cli/Command.php +++ b/src/pharext/Cli/Command.php @@ -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 \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); } } } diff --git a/src/pharext/Command.php b/src/pharext/Command.php index b174f7d..61b8810 100644 --- a/src/pharext/Command.php +++ b/src/pharext/Command.php @@ -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 index 0000000..bf3d87b --- /dev/null +++ b/src/pharext/Exception.php @@ -0,0 +1,17 @@ +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; } /** diff --git a/src/pharext/Installer.php b/src/pharext/Installer.php index 7866b65..4b90c49 100644 --- a/src/pharext/Installer.php +++ b/src/pharext/Installer.php @@ -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); } } } diff --git a/src/pharext/Openssl/PrivateKey.php b/src/pharext/Openssl/PrivateKey.php index 35c596a..481fd86 100644 --- a/src/pharext/Openssl/PrivateKey.php +++ b/src/pharext/Openssl/PrivateKey.php @@ -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; } } } diff --git a/src/pharext/Packager.php b/src/pharext/Packager.php index d3e4000..ba7de89 100644 --- a/src/pharext/Packager.php +++ b/src/pharext/Packager.php @@ -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()); - } - } - } } } diff --git a/src/pharext/SourceDir/Git.php b/src/pharext/SourceDir/Git.php index 531aa65..8e35bf9 100644 --- a/src/pharext/SourceDir/Git.php +++ b/src/pharext/SourceDir/Git.php @@ -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"; } diff --git a/src/pharext/SourceDir/Pecl.php b/src/pharext/SourceDir/Pecl.php index d21a0e0..0b7055a 100644 --- a/src/pharext/SourceDir/Pecl.php +++ b/src/pharext/SourceDir/Pecl.php @@ -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; } diff --git a/src/pharext/SourceDir/Pharext.php b/src/pharext/SourceDir/Pharext.php index 2675a78..8ca0901 100644 --- a/src/pharext/SourceDir/Pharext.php +++ b/src/pharext/SourceDir/Pharext.php @@ -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 index 0000000..473e1a6 --- /dev/null +++ b/src/pharext/Task.php @@ -0,0 +1,11 @@ +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 index 0000000..24b22da --- /dev/null +++ b/src/pharext/Task/Askpass.php @@ -0,0 +1,40 @@ +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 index 0000000..aabf9c8 --- /dev/null +++ b/src/pharext/Task/BundleGenerator.php @@ -0,0 +1,28 @@ +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 index 0000000..3effbfb --- /dev/null +++ b/src/pharext/Task/Configure.php @@ -0,0 +1,54 @@ +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 index 0000000..b2f954f --- /dev/null +++ b/src/pharext/Task/Extract.php @@ -0,0 +1,41 @@ +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 index 0000000..709a34a --- /dev/null +++ b/src/pharext/Task/GitClone.php @@ -0,0 +1,36 @@ +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 index 0000000..9e71565 --- /dev/null +++ b/src/pharext/Task/Make.php @@ -0,0 +1,65 @@ +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 index 0000000..08a1d94 --- /dev/null +++ b/src/pharext/Task/PeclFixup.php @@ -0,0 +1,48 @@ +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 index 0000000..d6f2788 --- /dev/null +++ b/src/pharext/Task/PharBuild.php @@ -0,0 +1,82 @@ +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 index 0000000..d090e28 --- /dev/null +++ b/src/pharext/Task/PharCompress.php @@ -0,0 +1,66 @@ +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 index 0000000..19094c1 --- /dev/null +++ b/src/pharext/Task/PharRename.php @@ -0,0 +1,54 @@ +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 index 0000000..739c587 --- /dev/null +++ b/src/pharext/Task/PharSign.php @@ -0,0 +1,48 @@ +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 index 0000000..ab70534 --- /dev/null +++ b/src/pharext/Task/Phpize.php @@ -0,0 +1,55 @@ +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 index 0000000..4f090ef --- /dev/null +++ b/src/pharext/Task/StreamFetch.php @@ -0,0 +1,75 @@ +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; + } +} diff --git a/src/pharext/Tempdir.php b/src/pharext/Tempdir.php index 2de174d..eb59263 100644 --- a/src/pharext/Tempdir.php +++ b/src/pharext/Tempdir.php @@ -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); } diff --git a/src/pharext/Tempfile.php b/src/pharext/Tempfile.php index c890cd9..e720551 100644 --- a/src/pharext/Tempfile.php +++ b/src/pharext/Tempfile.php @@ -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 index 0000000..b5e7520 --- /dev/null +++ b/src/pharext/Tempname.php @@ -0,0 +1,29 @@ +name = sys_get_temp_dir() . "/" . uniqid($prefix) . $suffix; + } + + /** + * @return string + */ + public function __toString() { + return (string) $this->name; + } +} diff --git a/src/pharext_install.tpl.php b/src/pharext_install.tpl.php index 04d84b2..f7a356d 100644 --- a/src/pharext_install.tpl.php +++ b/src/pharext_install.tpl.php @@ -1,7 +1,7 @@ /** - * Generated by pharext v at . + * Generated by pharext v at . */ namespace pharext;