Skip to content

Instantly share code, notes, and snippets.

@finagin
Created January 18, 2023 09:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save finagin/058d2657b9b8789863597e282b86b27e to your computer and use it in GitHub Desktop.
Save finagin/058d2657b9b8789863597e282b86b27e to your computer and use it in GitHub Desktop.
#!/usr/bin/env php
<?php
namespace {
use Finagin\Phar\Builder;
Builder::make()
->setCliEntryPoint('cli.php')
->setWebEntryPoint('web.php')
->setOutput('./app.phar')
->addSource('src')
->addSource('vendor')
->build();
}
namespace Finagin\Phar {
use FilesystemIterator;
use Phar;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RuntimeException;
use Traversable;
class Builder
{
const STUB_HEADER = "#!/usr/bin/env php\n";
private $outputFile;
private $cliEntryPoint = null;
private $webEntryPoint = null;
private $sources = [];
public static function make(): self
{
return new static;
}
/**
* Determine if a given string starts with a given substring.
*
* @param string $haystack
* @param string|string[] $needles
*
* @return bool
*/
protected final static function startsWith(string $haystack, $needles): bool
{
foreach ((array)$needles as $needle) {
if ((string)$needle !== '' && strncmp($haystack, $needle, strlen($needle)) === 0) {
return true;
}
}
return false;
}
protected function realPath(string $path, bool $isDir = false): string
{
$filePath = realpath($path);
if (! $filePath) {
throw new RuntimeException(sprintf(
'%s not found: %s',
$isDir ? 'Directory' : 'File',
$path
));
}
return $isDir
? $filePath.DIRECTORY_SEPARATOR
: $filePath;
}
protected function relativePath(string $path): string
{
$quoted = preg_quote(__DIR__.DIRECTORY_SEPARATOR, '/');
return preg_replace('/^(?:'.$quoted.')+/u', '', $path);
}
public function setOutput(string $filename): self
{
if (! static::startsWith($filename, '/')) {
$filename = __DIR__.DIRECTORY_SEPARATOR.$filename;
}
list('dirname' => $dir, 'basename' => $file) = pathinfo($filename);
$this->outputFile = $this->realPath($dir, true).$file;
return $this;
}
public function setCliEntryPoint(string $filename): self
{
$this->cliEntryPoint = $this->realPath($filename);
return $this;
}
public function setWebEntryPoint(string $filename): self
{
$this->webEntryPoint = $this->realPath($filename);
return $this;
}
/**
* @param bool $relative
*
* @return string
*/
public function getCliEntryPoint(bool $relative = false)
{
return $relative && $this->cliEntryPoint !== null
? $this->relativePath($this->cliEntryPoint)
: $this->cliEntryPoint;
}
public function getEntryPoints(): array
{
return [
$this->getCliEntryPoint(),
$this->getWebEntryPoint(),
$this->getCliEntryPoint(true),
$this->getWebEntryPoint(true),
];
}
/**
* @param bool $relative
*
* @return string|null
*/
public function getWebEntryPoint(bool $relative = false)
{
return $relative && $this->webEntryPoint !== null
? $this->relativePath($this->webEntryPoint)
: $this->webEntryPoint;
}
public function cleanup()
{
if (file_exists($this->outputFile)) {
unlink($this->outputFile);
}
}
protected function checkPharSupport()
{
if (! extension_loaded('phar')) {
throw new RuntimeException('Phar extension not loaded');
}
if (! Phar::canWrite()) {
function f(string $string, ...$codes): string
{
return sprintf("\033[%sm%s\033[0m", implode(';', $codes), $string);
}
die(implode('', [
f('Error: ', 1, 31),
f('Phar extension is loaded, but cannot write to phar files.', 31),
"\n\n",
f('Try rerun with "', 33),
f('-d phar.readonly=0', 1),
f('"', 33),
"\n\n",
f('Example:', 33),
"\n\n\t",
f(implode(' ', array_merge([PHP_BINARY, '-d', 'phar.readonly=0'], $_SERVER['argv'])), 1),
"\n\n",
]));
}
}
public function build(bool $cleanup = true)
{
$this->checkPharSupport();
if ($cleanup) {
$this->cleanup();
}
$phar = new Phar($this->outputFile);
$phar->startBuffering();
list($cliReal, $webReal, $cliRelative, $webRelative) = $this->getEntryPoints();
$stub = $phar->createDefaultStub($cliRelative, $webRelative);
$cliReal && $phar->addFile($cliReal, $cliRelative);
$webReal && $phar->addFile($webReal, $webRelative);
$phar->buildFromIterator($this->getDirectoryIterator(), __DIR__.DIRECTORY_SEPARATOR);
$phar->setStub(static::STUB_HEADER.$stub);
$phar->stopBuffering();
$phar->compressFiles(Phar::GZ);
if (! chmod($this->outputFile, 0770)) {
throw new RuntimeException(sprintf(
'Failed to set executable permission for %s',
$this->outputFile
));
}
}
public function addSource(string $dir): self
{
if (! static::startsWith($dir, '/')) {
$dir = __DIR__.DIRECTORY_SEPARATOR.$dir;
}
$this->sources[] = $this->realPath($dir, true);
return $this;
}
private function getDirectoryIterator(): Traversable
{
foreach ($this->sources as $source) {
yield from new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(
$source,
$__ = FilesystemIterator::SKIP_DOTS
| FilesystemIterator::UNIX_PATHS
)
);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment