353 lines
12 KiB
PHP
353 lines
12 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;
|
|
|
|
protected $dependencies = [];
|
|
|
|
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
|
|
);
|
|
$this->path = $path;
|
|
|
|
}
|
|
|
|
|
|
private function readComposer()
|
|
{
|
|
print_info("Parsing composer.json...");
|
|
|
|
$json = file_get_contents($this->path."/composer.json");
|
|
|
|
$composer = json_decode($json);
|
|
|
|
if (isset($composer->require)) {
|
|
foreach ($composer->require as $package=>$constraint) {
|
|
if ($package == "php") {
|
|
$this->dependencies[$package] = $constraint;
|
|
print_info(" ! require php: %s", $constraint);
|
|
} elseif (strpos($package, "ext-") === 0) {
|
|
$this->dependencies[$package] = $constraint;
|
|
print_info(" ! require ext: %s %s", $package, $constraint);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!isset($composer->bin) || !is_array($composer->bin)) {
|
|
print_warn("No executable defined, building a library phar...");
|
|
} elseif (count($composer->bin)>1) {
|
|
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 ($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";
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
private function installComposer()
|
|
{
|
|
print_info("Installing dependencies...");
|
|
passthru("composer install --no-interaction --optimize-autoloader");
|
|
}
|
|
|
|
private function buildManifest()
|
|
{
|
|
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");
|
|
// 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=null)
|
|
{
|
|
print_info("Generating bootstrap stub...");
|
|
|
|
$bins = (array)$bins;
|
|
|
|
$indexFile = "<?php\n";
|
|
|
|
$depcheck = null;
|
|
foreach ($this->dependencies as $dep=>$constraint) {
|
|
if ($dep == "php") {
|
|
|
|
} elseif (strpos($dep, "ext-") === 0) {
|
|
$ext = substr($dep, 4);
|
|
$depcheck.= 'if (!extension_loaded("' . $ext . '")) { @fwrite(STDERR, "Fatal: Missing required extension ' . $ext . '.\n"); exit(2); }' . PHP_EOL;
|
|
}
|
|
}
|
|
|
|
if (!$index && count($bins)==0) {
|
|
$indexFile.= 'require_once __DIR__."/vendor/autoload.php";';
|
|
} elseif (count($bins)>1) {
|
|
$indexFile.= '$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 {
|
|
if (count($bins)) {
|
|
$index = array_shift($bins);
|
|
}
|
|
$indexFile = file_get_contents($index);
|
|
$indexFile = $this->stripShebang($indexFile);
|
|
$index = "bootstrap.php";
|
|
}
|
|
|
|
$indexFile = str_replace("<?php", "<?php ".$depcheck, $indexFile);
|
|
|
|
$stub = "#!/usr/bin/env php\n".$phar->createDefaultStub($index);
|
|
$phar->setStub($stub);
|
|
|
|
if ($index) {
|
|
$phar->addFromString($index, $indexFile);
|
|
}
|
|
|
|
}
|
|
|
|
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...");
|
|
|
|
$tempName = uniqid("phar").".phar";
|
|
$phar = new \Phar($tempName);
|
|
|
|
$total = count($this->manifest);
|
|
$pcf = 100 / $total;
|
|
$bw = 40;
|
|
$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);
|
|
$index++;
|
|
$totalBytes += $object->getFilesize();
|
|
if (posix_isatty(STDOUT) && (microtime(true)>$t+.2)) {
|
|
$t = microtime(true);
|
|
$pc = $pcf * $index;
|
|
$pb = round($pbf * $index);
|
|
$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)) {
|
|
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']:[]));
|
|
|
|
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");
|
|
|
|
}
|
|
|
|
}
|