From fd5b3ddb8feb9a334f49ffc1577d2445a9c4c846 Mon Sep 17 00:00:00 2001 From: Christopher Vagnetoft Date: Fri, 6 Apr 2018 01:22:42 +0200 Subject: [PATCH] Initial commit --- .gitignore | 2 + README.md | 87 ++++++++++++++++ bin/pharlite | 83 +++++++++++++++ composer.json | 13 +++ src/Builder.php | 230 +++++++++++++++++++++++++++++++++++++++++ src/Manifest.php | 53 ++++++++++ src/ManifestObject.php | 46 +++++++++ 7 files changed, 514 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100755 bin/pharlite create mode 100644 composer.json create mode 100644 src/Builder.php create mode 100644 src/Manifest.php create mode 100644 src/ManifestObject.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ff72e2d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/composer.lock +/vendor diff --git a/README.md b/README.md new file mode 100644 index 0000000..0b935dd --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +PharLite: Easy PHAR generator +============================= + +Make sure PHP can write PHARs in your `php.ini` before trying anything else. + + +`composer.json` options +----------------------- + + { + ... + "extra": { + "phar": { + "include": [ ... ], + "exclude": [ ... ], + "index": "web/index.php", + "output": "pharname.phar", + "strip": "none", + "metadata": { ... }, + "preload": [ ... ], + } + } + } + +### `include` and `exclude` + +Specify additional files or paths to include or exclude. + +### `index`: Create a web phar + +Specifying this option will automatically opt to create a web phar. The value should +point to a valid index file or router. + +### `output`: Output filename + +Defines the output filename. Defaults to `output.phar` + +### `strip`: Trim file sizes + +Valid values are `none`, `all` or an array of patterns to match. + + "strip": "all" Strip all files + "strip": "none" Include everything as is (default) + "strip": [ "src/*.php" ] All *.php in src/ and subdirectories + +Stripping will remove comments and whitepace in files, which makes them smaller +but can confuse code that dynamically parses f.ex. docblock comments for routing +info etc. + +### `metadata`: Phar metadata + +Define additional metadata to write to the phar. The following keys are automatically +written: + +| Key | Type | Description | +|:----------------------|:---------:|:------------------------------------------- +| `phar.generator` | string | The string `PharLite/x.y (PHP/x.y)` +| `phar.type` | string | One of `app`, `library` or `web` +| `package.name` | string | The composer package name +| `package.version` | string | The composer package version + +### `preload`: Files to preload + +If defined, this should be an array of files to include after `composer.json` in the +stub. + + +Building +-------- + +To build a phar from a composer project, just run `pharlite`. It will build one of +the following: + +* **Library phar** - A single file containing a composer library, complete with + autoloaders and all that magic. +* **Application phar** - An executable file containing an application and its + dependencies. +* **Web phar** - A complete website in a single file + +If more than one `bin` is defined in the `composer.json`, the first one will be +the default one, but all defined bins will be available by symlinking the phar +to the respective names. + + "bin": [ "bin/foo", "bin/bar" ] + + output.phar, foo.phar, foo, hello.phar -> calls bin/foo + bar.phar, bar -> calls bin/bar diff --git a/bin/pharlite b/bin/pharlite new file mode 100755 index 0000000..404ba8a --- /dev/null +++ b/bin/pharlite @@ -0,0 +1,83 @@ +#!/usr/bin/env php +$value) { + switch ($option) { + case 'h': + case 'help': + show_app_usage(); + return false; + case 'd': + case 'dir': + case 'directory': + $parsed['dir'] = $value; + break; + case 'o': + case 'output': + $parsed['output'] = $value; + break; + case 'i': + case 'install': + $parsed['install'] = true; + } + } + + return $parsed; +} + +$opts = parse_opts(); +if (false === parse_opts()) { + exit(1); +} + +$path = getcwd(); + +$builder = new PharLite\Builder($path, $opts); +$builder->build(); \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..d03dcc0 --- /dev/null +++ b/composer.json @@ -0,0 +1,13 @@ +{ + "autoload": { + "psr-4": { + "PharLite\\": "src/" + } + }, + "require": { + "symfony/finder": "^4.0" + }, + "bin": [ + "bin/pharlite" + ] +} diff --git a/src/Builder.php b/src/Builder.php new file mode 100644 index 0000000..e957317 --- /dev/null +++ b/src/Builder.php @@ -0,0 +1,230 @@ + false, + ]; + + public function __construct($path=null, array $opts=[]) + { + if (!$path) { + $path = getcwd(); + } + + if (!file_exists($path."/composer.json")) { + throw new \RuntimeException("Could not find a composer.json in {$path}"); + } + + $this->opts = array_merge( + self::$default_options, + $opts + ); + + } + + + private function readComposer() + { + print_info("Parsing composer.json..."); + + $json = file_get_contents("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]); + $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"; + } else { + if (isset($composer->bin) && is_array($composer->bin)) { + $this->pharType = self::PHAR_TYPE_APP; + } + $this->output = "output.phar"; + } + print_info("Generating output %s", $this->output); + + $this->composer = $composer; + } + + private function installComposer() + { + print_info("Installing dependencies..."); + passthru("composer install --no-interaction --optimize-autoloader"); + } + + private function buildManifest() + { + print_info("Creating manifest..."); + + $manifest = new Manifest($this->path); + // Always include the vendor directory + print_info(" ← vendor/"); + $manifest->addDirectory("vendor"); + // Include everything from the autoload + if (isset($this->composer->autoload)) { + foreach ((array)$this->composer->autoload as $type=>$autoloaders) { + if ($type == "files") { + foreach ($autoloaders as $file) { + print_info(" ← %s", $file); + $manifest->addFile($file); + } + continue; + } + foreach ($autoloaders as $prefix=>$path) { + print_info(" ← %s", $path); + $manifest->addDirectory($path); + } + } + } + if (array_key_exists('include', $this->pharOpts)) { + foreach ((array)$this->pharOpts['include'] as $path) { + print_info(" ← %s", $path); + if (substr($path,-1,1)==DIRECTORY_SEPARATOR) { + $manifest->addDirectory($path); + } else { + $manifest->addFile($path); + } + } + } + + $this->manifest = $manifest; + } + + private function generateBootstrap(\Phar $phar, $type, $index=null, array $bins=[]) + { + print_info("Generating bootstrap stub..."); + + if (!$index && count($bins)==0) { + $indexFile = '1) { + $indexFile = 'stripShebang($str); + $phar->addFromString($bin, $str); + } + } else { + $indexFile = file_get_contents($index); + $indexFile = $this->stripShebang($indexFile); + $index = "bootstrap.php"; + } + + $stub = "#!/usr/bin/env php\n".$phar->createDefaultStub($index); + + $phar->addFromString($index, $indexFile); + $phar->setStub($stub); + + } + + private function stripShebang($str) + { + if (strpos($str, '#!') === 0) { + if (strpos($str, '#!/usr/bin/env php') === 0) { + $str = substr($str,strpos($str, " "PharLite/".PHARLITE_VERSION." (PHP/".PHP_VERSION.")", + 'phar.type' => $this->pharType, + 'package.name' => isset($this->composer->name)?$this->composer->name:null, + 'package.version' => isset($this->composer->version)?$this->composer->version:null, + ], $metadata); + + print_info("Writing metadata:\n%s", json_encode($metadata, JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT)); + + $phar->setMetadata($metadata); + } + + public function build() + { + $this->readComposer(); + + if ($this->opts['install']) { + $this->installComposer(); + } + + $this->buildManifest(); + + print_info("Adding files..."); + + $phar = new \Phar($this->output); + + $total = count($this->manifest); + $pcf = 100 / $total; + $bw = 40; + $pbf = $bw / $total; + $index = 0; + foreach ($this->manifest as $object) { + // printf(" %s: %s\n", $object->getFilename(), $object->getLocalName()); + $object->addToPhar($phar); + if (posix_isatty(STDOUT) && ($index++ % 25 == 0)) { + $pc = $pcf * $index; + $pb = round($pbf * $index); + printf("\r[%s%s] %.1f%%", str_repeat("=",$pb), str_repeat(" ",$bw-$pb), $pc); + } + } + if (posix_isatty(STDOUT)) { + printf("\r\e[2K"); + } + + print_info("Added %d files!", $index); + + $this->generateBootstrap($phar, $this->pharType, $this->index, $this->bins); + $this->writeMetadata($phar, (array)(array_key_exists('metadata',$this->pharOpts)?$this->pharOpts['metadata']:[])); + + chmod($this->output, 0777); + + } + +} diff --git a/src/Manifest.php b/src/Manifest.php new file mode 100644 index 0000000..17b8770 --- /dev/null +++ b/src/Manifest.php @@ -0,0 +1,53 @@ +root = $root; + } + + public function addDirectory($path, callable $filter=null) + { + $path = rtrim($path, DIRECTORY_SEPARATOR); + $obj = (new Finder)->files()->in($path); + foreach ($obj as $o) { + if (is_callable($filter) && (false === $filter($o))) { + continue; + } + $path = $o->getPathname(); + $this->addFile($path); + } + } + + public function addFile($file) + { + if (strpos($file, $this->root) === 0) { + $local = substr($file, 0, strlen($this->root)); + } else { + $local = $file; + } + $this->objects[] = new ManifestObject($file, $local); + } + + public function getIterator() + { + return new \ArrayIterator($this->objects); + } + + public function count() + { + return count($this->objects); + } + +} diff --git a/src/ManifestObject.php b/src/ManifestObject.php new file mode 100644 index 0000000..8d5616d --- /dev/null +++ b/src/ManifestObject.php @@ -0,0 +1,46 @@ +filename = $filename; + $this->localname = $localname; + } + + public function getFilename() + { + return $this->filename; + } + + public function getLocalName() + { + return $this->localname; + } + + public function addToPhar(\Phar $phar) + { + $phar->addFile( + $this->getFilename(), + $this->getLocalName() + ); + } + + public function addFiltered(\Phar $phar, callable $filter) + { + $body = file_get_contents($this->filename); + $body = call_user_func($filter, $body); + $phar->addFromString($this->getLocalName(), $body); + } +} \ No newline at end of file