php-pharlite/src/Builder.php

231 lines
7.1 KiB
PHP

<?php
namespace PharLite;
class Builder
{
const PHAR_TYPE_APP = "app";
const PHAR_TYPE_WEB = "web";
const PHAR_TYPE_LIB = "lib";
/** @var string */
protected $path;
protected $manifest;
protected $composer;
protected $output;
protected $pharType = self::PHAR_TYPE_LIB;
protected $pharOpts = [];
protected $index;
protected $bins;
private static $default_options = [
'install' => 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 = '<?php require_once __DIR__."/vendor/autoload.php";';
} elseif (count($bins)>1) {
$indexFile = '<?php $bin=basename($argv[0],".phar"); switch($bin) {';
for ($n = 1; $n<count($bins); $n++) {
$indexFile.= 'case '.var_export(basename($bins[$n]),true).': require_once __DIR__."/".'.var_export($bins[$n],true).'; break;';
}
$indexFile.= 'case '.var_export(basename($bins[0]),true).': default: require_once __DIR__."/".'.var_export($bins[0],true).'; break;';
$indexFile.= '}';
$index = "bootstrap.php";
foreach ($bins as $bin) {
$str = file_get_contents($bin);
$str = $this->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, "<?"));
}
}
return $str;
}
private function writeMetadata(\Phar $phar, array $metadata=[])
{
$metadata = array_merge([
'phar.generator' => "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);
}
}