diff --git a/bin/pharlite b/bin/pharlite index dcd3b96..cdb0ee8 100755 --- a/bin/pharlite +++ b/bin/pharlite @@ -14,12 +14,14 @@ if (posix_isatty(STDOUT)) { define("FMT_ERR", "\e[31;1m"); define("FMT_WARN", "\e[33;1m"); define("FMT_INFO", "\e[32m"); + define("FMT_NOTICE", "\e[33m"); define("FMT_AFTER", "\e[0m"); } else { define("FMT_ERR", ""); define("FMT_WARN", ""); define("FMT_INFO", ""); define("FMT_AFTER", ""); + define("FMT_NOTICE", ""); } function print_error($fmt, ...$arg) { @@ -34,24 +36,33 @@ function print_info($fmt, ...$arg) { fprintf(STDOUT, FMT_INFO.$fmt.FMT_AFTER.PHP_EOL, ...$arg); } +function print_notice($fmt, ...$arg) { + fprintf(STDOUT, FMT_NOTICE.$fmt.FMT_AFTER.PHP_EOL, ...$arg); +} + function show_app_usage() { - printf("usage\n"); + printf("options:\n"); + printf(" -I,--init Initialize a new configuration\n"); } function parse_opts() { $opts = getopt( - "ho:d:i", + "ho:d:iI", [ "help", "directory:", "dir:", "output:", - "install" + "install", + "init" ] ); - $parsed = []; + $parsed = [ + 'install' => false, + 'init' => false + ]; foreach ($opts as $option=>$value) { switch ($option) { @@ -71,6 +82,10 @@ function parse_opts() { case 'i': case 'install': $parsed['install'] = true; + break; + case 'I': + case 'init': + $parsed['init'] = true; } } @@ -78,11 +93,15 @@ function parse_opts() { } $opts = parse_opts(); -if (false === parse_opts()) { +if (false === $opts) { exit(1); } $path = getcwd(); $builder = new PharLite\Builder($path, $opts); -$builder->build(); +if ($opts['init']) { + $builder->initConfig(); +} else { + $builder->build(); +} diff --git a/composer.json b/composer.json index b5e75b6..e753b50 100644 --- a/composer.json +++ b/composer.json @@ -19,5 +19,10 @@ }, "bin": [ "bin/pharlite" - ] + ], + "extra": { + "phar": { + "output": "pharlite" + } + } } diff --git a/src/Builder.php b/src/Builder.php index 5d44103..5193619 100644 --- a/src/Builder.php +++ b/src/Builder.php @@ -44,6 +44,7 @@ class Builder self::$default_options, $opts ); + $this->path = $path; } @@ -52,26 +53,24 @@ class Builder { print_info("Parsing composer.json..."); - $json = file_get_contents("composer.json"); + $json = file_get_contents($this->path."/composer.json"); $composer = json_decode($json); if (!isset($composer->bin) || !is_array($composer->bin)) { print_warn("No executable defined, building a library phar..."); } elseif (count($composer->bin)>1) { - print_info("More than one executable defined, using the first one as default"); - print_info(" → Using executable %s", $composer->bin[0]); + print_notice("More than one executable defined, using the first one as default"); + print_info(" → Using default executable %s", $composer->bin[0]); + for ($n = 1; $n < count($composer->bin); $n++) { + print_info(" → Adding alternate executable %s", $composer->bin[$n]); + } $this->bins = $composer->bin; } else { print_info(" → Using executable %s", $composer->bin[0]); $this->bins = $composer->bin; } - if (isset($composer->extra) && isset($composer->extra->phar)) { - print_info(" → Found extra phar configuration"); - $this->pharOpts = (array)$composer->extra->phar; - } - if ($this->index) { $this->pharType = self::PHAR_TYPE_WEB; $this->output = basename($this->index,".php").".phar"; @@ -81,6 +80,20 @@ class Builder } $this->output = "output.phar"; } + + if (isset($composer->extra)) { + if (isset($composer->extra->phar)) { + print_info(" → Found extra phar configuration"); + $this->pharOpts = (array)$composer->extra->phar; + } + if (array_key_exists('output', $this->pharOpts)) { + $this->output = $this->pharOpts['output']; + } + if (array_key_exists('stub', $this->pharOpts)) { + // set a custom stub + } + } + print_info("Generating output %s", $this->output); $this->composer = $composer; @@ -96,7 +109,17 @@ class Builder { print_info("Creating manifest..."); + $excluded = (array)( + array_key_exists('exclude',$this->pharOpts) + ?$this->pharOpts['exclude'] + :[] + ); + + $manifest = new Manifest($this->path); + + $manifest->setExcluded($excluded); + // Always include the vendor directory print_info(" ← vendor/"); $manifest->addDirectory("vendor"); @@ -130,10 +153,12 @@ class Builder $this->manifest = $manifest; } - private function generateBootstrap(\Phar $phar, $type, $index=null, array $bins=[]) + private function generateBootstrap(\Phar $phar, $type, $index=null, ?array $bins=null) { print_info("Generating bootstrap stub..."); + $bins = (array)$bins; + if (!$index && count($bins)==0) { $indexFile = '1) { @@ -159,10 +184,12 @@ class Builder } $stub = "#!/usr/bin/env php\n".$phar->createDefaultStub($index); - - $phar->addFromString($index, $indexFile); $phar->setStub($stub); + if ($index) { + $phar->addFromString($index, $indexFile); + } + } private function stripShebang($str) @@ -201,20 +228,30 @@ class Builder print_info("Adding files..."); - $phar = new \Phar($this->output); + $tempName = uniqid("phar").".phar"; + $phar = new \Phar($tempName); $total = count($this->manifest); $pcf = 100 / $total; $bw = 40; - $pbf = $bw / $total; + $tw = intval(exec("tput cols")); + $pbf = $tw / $total; $index = 0; + $spinner = [ "/", "-", "\\", "|" ]; + $s = 0; $t = microtime(true); + $totalBytes = 0; foreach ($this->manifest as $object) { // printf(" %s: %s\n", $object->getFilename(), $object->getLocalName()); $object->addToPhar($phar); - if (posix_isatty(STDOUT) && ($index++ % 25 == 0)) { + $index++; + $totalBytes += $object->getFilesize(); + if (posix_isatty(STDOUT) && (microtime(true)>$t+.2)) { + $t = microtime(true); $pc = $pcf * $index; $pb = round($pbf * $index); - printf("\r[%s%s] %.1f%%", str_repeat("=",$pb), str_repeat(" ",$bw-$pb), $pc); + $bar = str_pad(sprintf(" %4.1f%% %d files, %.1fKiB %s ", $pc, $index, $totalBytes/1024, dirname($object->getFilename())), $tw); + echo "\r\e[30;42m".substr($bar, 0, $pb)."\e[0m".substr($bar, $pb); + //printf("\r%4.1f%% [%s%s%s] %d files, %.1fKiB", $pc, str_repeat("=",$pb), $spinner[$s=(($s+1)%4)], str_repeat(" ",$bw-$pb), $index, $totalBytes/1024); } } if (posix_isatty(STDOUT)) { @@ -226,8 +263,62 @@ class Builder $this->generateBootstrap($phar, $this->pharType, $this->index, $this->bins); $this->writeMetadata($phar, (array)(array_key_exists('metadata',$this->pharOpts)?$this->pharOpts['metadata']:[])); + if (file_exists($this->output)) { + unlink($this->output); + } + rename($tempName, $this->output); chmod($this->output, 0777); } + public function initConfig() + { + print_info("Configuring pharlite"); + + $askString = function($prompt, $default=null) { + return readline($prompt." [".$default."]? ")?:null; + }; + + $composer = json_decode(file_get_contents($this->path."/composer.json")); + + if (!isset($composer->extra)) { + $composer->extra = (object)[]; + } + if (!isset($composer->extra->phar)) { + $composer->extra->phar = (object)[]; + } + + if ($output = $askString('Output filename', @$composer->extra->phar->output)) { + $composer->extra->phar->output = $output; + } + if ($index = $askString('Index file (for web phar)', @$composer->extra->phar->index)) { + $composer->extra->phar->index = $index; + } + if ($stub = $askString('Loader stub', @$composer->extra->phar->stub)) { + $composer->extra->phar->stub = $stub; + } + + if (empty($composer->bin)) { + print_warn("You have no bins defined in your composer.json. Specify at least one bin to create an application phar."); + } + + if (file_exists($this->path."/composer.bak")) { + unlink($this->path."/composer.bak"); + } + rename($this->path."/composer.json", $this->path."/composer.bak"); + file_put_contents($this->path."/composer.json", json_encode($composer, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)."\n"); + + exec("composer validate --no-check-lock -q", $output, $retval); + if ($retval == 0) { + print_info("Updated composer.json"); + unlink($this->path."/composer.bak"); + return; + } + + print_error("The composer.json file isn't valid after being modified. Reverting..."); + unlink($this->path."/composer.json"); + rename($this->path."/composer.bak", $this->path."/composer.json"); + + } + } diff --git a/src/Manifest.php b/src/Manifest.php index 17b8770..55bfe14 100644 --- a/src/Manifest.php +++ b/src/Manifest.php @@ -12,11 +12,18 @@ class Manifest implements \IteratorAggregate, \Countable /** @var ManifestObject[] The included objects */ protected $objects = []; + protected $excluded = []; + public function __construct($root) { $this->root = $root; } + public function setExcluded(array $patterns=[]) + { + $this->excluded = $patterns; + } + public function addDirectory($path, callable $filter=null) { $path = rtrim($path, DIRECTORY_SEPARATOR); @@ -32,11 +39,19 @@ class Manifest implements \IteratorAggregate, \Countable public function addFile($file) { + // Don't add the file if it matches an excluded pattern + if ($this->excluded) foreach ($this->excluded as $p) { + if (fnmatch($p, $file)) return; + } + + // Figure out local path name if (strpos($file, $this->root) === 0) { $local = substr($file, 0, strlen($this->root)); } else { $local = $file; } + + // Add object to manifest $this->objects[] = new ManifestObject($file, $local); } diff --git a/src/ManifestObject.php b/src/ManifestObject.php index 8d5616d..887102d 100644 --- a/src/ManifestObject.php +++ b/src/ManifestObject.php @@ -13,28 +13,44 @@ class ManifestObject protected $localname; + protected $stripped = false; + public function __construct($filename, $localname=null) { $this->filename = $filename; $this->localname = $localname; } - public function getFilename() + public function setStripped(bool $stripped) + { + $this->stripped = $stripped; + } + + public function getStripped():bool + { + return $this->stripped; + } + + public function getFilename():string { return $this->filename; } - public function getLocalName() + public function getLocalName():string { return $this->localname; } public function addToPhar(\Phar $phar) { - $phar->addFile( - $this->getFilename(), - $this->getLocalName() - ); + if ($this->stripped) { + // strip and add + } else { + $phar->addFile( + $this->getFilename(), + $this->getLocalName() + ); + } } public function addFiltered(\Phar $phar, callable $filter) @@ -43,4 +59,9 @@ class ManifestObject $body = call_user_func($filter, $body); $phar->addFromString($this->getLocalName(), $body); } -} \ No newline at end of file + + public function getFilesize() + { + return filesize($this->filename); + } +}