Skip to content

Commit

Permalink
issue #154 - PHAR file creation
Browse files Browse the repository at this point in the history
  • Loading branch information
pounard committed Jun 4, 2024
1 parent f4ffec1 commit 0a07d89
Show file tree
Hide file tree
Showing 7 changed files with 287 additions and 6 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
/.phpunit.result.cache
/.php-cs-fixer.cache
/composer.lock
/composer.phar
/phpstan.neon
/phpunit.xml
/vendor/
test_db*
test_db*
39 changes: 39 additions & 0 deletions bin/compile
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/bash

ROOTPATH="`dirname $0`"
ROOTPATH="`dirname $ROOTPATH`"
COMPOSER="$ROOTPATH/composer.phar"

echo " ==> Using composer $COMPOSER"
echo " ==> Will generate $ROOTPATH/db-tools.phar"

# Make a backup of the composer.json file.
cp "$ROOTPATH/composer.json" "$ROOTPATH/composer.json.dist"

if [ ! -e "$COMPOSER" ]; then
echo " ==> Download composer in $COMPOSER"
wget --quiet https://getcomposer.org/download/latest-stable/composer.phar -o "$COMPOSER"
fi

# Prepare composer, install without depdendencies.
echo " ==> Prepare environment"
rm -rf "$ROOTPATH/composer.lock"
rm -rf "$ROOTPATH/vendor"

# Install PHAR only tooling.
echo " ==> Require compile-only dependencies"
php "$COMPOSER" -n require --no-audit composer/pcre:'^3.1' seld/phar-utils:'^1.2'
php "$COMPOSER" -n -q config autoloader-suffix DbToolsPhar
php "$COMPOSER" -n install --no-dev
php "$COMPOSER" -n config autoloader-suffix --unset

# Compile PHAR file
echo " ==> Running compilation"
php -d phar.readonly=0 bin/compile.php
chmod +x "$ROOTPATH/db-tools.phar"

# Clean up environment
echo " ==> Cleaning up environment"
cp "$ROOTPATH/composer.json.dist" "$ROOTPATH/composer.json"
rm -rf "$ROOTPATH/composer.lock" "$ROOTPATH/composer.json.dist"
php "$COMPOSER" -n -q install
57 changes: 57 additions & 0 deletions bin/compile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

namespace MakinaCorpus\DbToolsBundle;

use MakinaCorpus\DbToolsBundle\Bridge\Standalone\PharCompiler;

/**
* Please run before running this:
* $ composer config autoloader-suffix DbToolsPhar
* $ composer install --no-dev
* $ composer config autoloader-suffix --unset
* $ php -d phar.readonly=0 bin/compile.php
*/

(static function (): void {
$autoloadFiles = [
__DIR__ . '/../vendor/autoload.php',
__DIR__ . '/../../../autoload.php',
];

$autoloaderFound = false;
foreach ($autoloadFiles as $autoloadFile) {
if (!\file_exists($autoloadFile)) {
continue;
}
require_once $autoloadFile;
$autoloaderFound = true;
}

if (!$autoloaderFound) {
if (\extension_loaded('phar') && \Phar::running() !== '') {
\fwrite(STDERR, 'The PHAR was built without dependencies!' . \PHP_EOL);
exit(1);
}
\fwrite(STDERR, 'vendor/autoload.php could not be found. Did you run `composer install`?' . \PHP_EOL);
exit(1);
}

$cwd = \getcwd();
\assert(\is_string($cwd));
\chdir(__DIR__.'/../');
$ts = \rtrim(\exec('git log -n1 --pretty=%ct HEAD'));
if (!\is_numeric($ts)) {
echo 'Could not detect date using "git log -n1 --pretty=%ct HEAD"'.\PHP_EOL;
exit(1);
}
\chdir($cwd);

\error_reporting(-1);
\ini_set('display_errors', '1');

$compiler = new PharCompiler();
$compiler->compile();
exit(1);
})();
8 changes: 5 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"makinacorpus/query-builder": "^1.6.1",
"psr/log": "^3.0",
"symfony/config": "^6.0|^7.0",
"symfony/console": "^6.0|^7.0",
"symfony/filesystem": "^6.0|^7.0",
"symfony/finder": "^6.0|^7.0",
"symfony/options-resolver": "^6.0|^7.0",
Expand All @@ -34,12 +35,13 @@
"symfony/validator": "^6.3|^7.0"
},
"suggest": {
"doctrine/doctrine-bundle": "For autoconfiguration in Symfony project context",
"symfony/console": "In order to use the standalone CLI tool or Symfony console commands",
"symfony/password-hasher": "In order to use the password hash anonymizer"
},
"conflict": {
"symfony/console": "<6.0|>=8.0",
"composer/pcre": "<3.1|>=4.0",
"doctrine/dbal": "<3.0|>=5.0",
"doctrine/orm": "<2.15|>=4.0",
"seld/phar-utils": "<1.2|>=2.0",
"symfony/password-hasher": "<6.0|>=8.0"
},
"autoload": {
Expand Down
3 changes: 3 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ parameters:
excludePaths:
- src/DependencyInjection/DbToolsConfiguration.php
checkMissingOverrideMethodAttribute: true
ignoreErrors:
- '#Instantiated class Seld\\PharUtils\\Timestamps not found.#'
- '#on an unknown class Seld\\PharUtils\\Timestamps.#'
2 changes: 0 additions & 2 deletions src/Bridge/Standalone/Bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,10 @@ public static function run(): void
*/
public static function createApplication(): Application
{
// @todo Test in PHAR context.
if (\class_exists(InstalledVersions::class)) {
$version = InstalledVersions::getVersion('makinacorpus/db-tools-bundle');
}
$version ??= 'cli';
\assert($version !== null);

$application = new Application('DbTools', $version);
$application->setCatchExceptions(true);
Expand Down
181 changes: 181 additions & 0 deletions src/Bridge/Standalone/PharCompiler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
<?php

declare(strict_types=1);

namespace MakinaCorpus\DbToolsBundle\Bridge\Standalone;

use Composer\Pcre\Preg;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Process\Process;
use Seld\PharUtils\Timestamps;

/**
* The Compiler class compiles composer into a phar.
*
* Heavily inspired from composer code, all credits to their authors.
*
* @see https://github.com/composer/composer/blob/main/src/Composer/Compiler.php
* @see https://getcomposer.org/
*/
class PharCompiler
{
private \DateTime $versionDate;

/**
* Creates the PHAR.
*
* @param ?string $pharFile
* Full target PHAR file name.
*/
public function compile(?string $pharFile = null): void
{
$pharFile ??= \dirname(__DIR__, 3) . '/db-tools.phar';

if (\file_exists($pharFile)) {
\unlink($pharFile);
}

$rootDir = \dirname(__DIR__, 3);

// Next line would fetch the current reference (commit hash or tag).
// $process = new Process(['git', 'log', '--pretty=%H', '-n1', 'HEAD'], $rootDir);

$process = new Process(['git', 'log', '-n1', '--pretty=%ci', 'HEAD'], $rootDir);
if ($process->run() !== 0) {
throw new \RuntimeException('Can\'t run git log. You must ensure to run compile from composer git repository clone and that git binary is available.');
}

$this->versionDate = new \DateTime(\trim($process->getOutput()));
$this->versionDate->setTimezone(new \DateTimeZone('UTC'));

$phar = new \Phar($pharFile, 0, 'db-tools.phar');
$phar->setSignatureAlgorithm(\Phar::SHA512);

$phar->startBuffering();

$finderSort = static fn ($a, $b): int => \strcmp(\strtr($a->getRealPath(), '\\', '/'), \strtr($b->getRealPath(), '\\', '/'));

// Local package sources.
$finder = new Finder();
$finder->files()
->ignoreVCS(true)
->name('*.php')
->notName('Compiler.php')
->notName('ClassLoader.php')
->notName('InstalledVersions.php')
->in($rootDir.'/src')
->sort($finderSort)
;
foreach ($finder as $file) {
$this->addFile($phar, $file);
}
// Add runtime utilities separately to make sure they retains the docblocks as these will get copied into projects.
$this->addFile($phar, new \SplFileInfo($rootDir . '/vendor/composer/ClassLoader.php'), false);
$this->addFile($phar, new \SplFileInfo($rootDir . '/vendor/composer/InstalledVersions.php'), false);

// Add vendor files
$finder = new Finder();
$finder->files()
->ignoreVCS(true)
->notPath('/\/(composer\.(json|lock)|[A-Z]+\.md(?:own)?|\.gitignore|appveyor.yml|phpunit\.xml\.dist|phpstan\.neon\.dist|phpstan-config\.neon|phpstan-baseline\.neon)$/')
->notPath('/(.*\.(md|xml|twig|svg)|Dockerfile|phpbench\.json|yaml-lint|dev\.sh|docker-compose\.(yaml|yml)|run-tests\.sh)/')
->notPath('/bin\/(jsonlint|validate-json|simple-phpunit|phpstan|phpstan\.phar)(\.bat)?$/')
->notPath('justinrainbow/json-schema/demo/')
->notPath('justinrainbow/json-schema/dist/')
->notPath('composer/LICENSE')
->exclude('Tests')
->exclude('tests')
->exclude('docs')
->in($rootDir.'/vendor/')
->sort($finderSort)
;

$extraFiles = [];
foreach ([
$rootDir . '/vendor/composer/installed.json',
// CaBundle::getBundledCaBundlePath(),
$rootDir . '/vendor/composer/installed.json',
$rootDir . '/vendor/symfony/console/Resources/bin/hiddeninput.exe',
$rootDir . '/vendor/symfony/console/Resources/completion.bash',
$rootDir . '/vendor/symfony/console/Resources/completion.fish',
$rootDir . '/vendor/symfony/console/Resources/completion.zsh',
$rootDir . '/vendor/composer/installed.json',
] as $file) {
$extraFiles[$file] = \realpath($file);
if (!\file_exists($file)) {
throw new \RuntimeException('Extra file listed is missing from the filesystem: '.$file);
}
}
$unexpectedFiles = [];

foreach ($finder as $file) {
if (false !== ($index = \array_search($file->getRealPath(), $extraFiles, true))) {
unset($extraFiles[$index]);
} elseif (!Preg::isMatch('{(^LICENSE$|\.php$)}', $file->getFilename())) {
$unexpectedFiles[] = (string) $file;
}

if (Preg::isMatch('{\.php[\d.]*$}', $file->getFilename())) {
$this->addFile($phar, $file);
} else {
$this->addFile($phar, $file, false);
}
}

if (\count($extraFiles) > 0) {
throw new \RuntimeException('These files were expected but not added to the phar, they might be excluded or gone from the source package:'.PHP_EOL.var_export($extraFiles, true));
}
if (\count($unexpectedFiles) > 0) {
throw new \RuntimeException('These files were unexpectedly added to the phar, make sure they are excluded or listed in $extraFiles:'.PHP_EOL.var_export($unexpectedFiles, true));
}

// Add binary.
$phar->addFile($rootDir . '/bin/db-tools.php', 'bin/db-tools.php');
$content = \file_get_contents($rootDir.'/bin/db-tools');
$content = Preg::replace('{^#!/usr/bin/env php\s*}', '', $content);
$phar->addFromString('bin/db-tools', $content);

// Stubs
$phar->setStub(
<<<'EOT'
#!/usr/bin/env php
<?php
if (!\class_exists('Phar')) {
echo 'PHP\'s phar extension is missing. DbTools requires it to run. Enable the extension or recompile php without --disable-phar then try again.' . PHP_EOL;
exit(1);
}
Phar::mapPhar('db-tools.phar');
require 'phar://db-tools.phar/bin/db-tools';
__HALT_COMPILER();
EOT,
);

$phar->stopBuffering();

//$this->addFile($phar, new \SplFileInfo($rootDir.'/LICENSE.md'), false);

unset($phar);

// re-sign the phar with reproducible timestamp / signature
$util = new Timestamps($pharFile);
$util->updateTimestamps($this->versionDate);
$util->save($pharFile, \Phar::SHA512);
}

private function getRelativeFilePath(\SplFileInfo $file): string
{
$rootDir = \dirname(__DIR__, 3);

$realPath = $file->getRealPath();
$pathPrefix = $rootDir . DIRECTORY_SEPARATOR;
$pos = \strpos($realPath, $pathPrefix);
$relativePath = ($pos !== false) ? \substr_replace($realPath, '', $pos, \strlen($pathPrefix)) : $realPath;

return \strtr($relativePath, '\\', '/');
}

private function addFile(\Phar $phar, \SplFileInfo $file, bool $strip = true): void
{
$phar->addFile($this->getRelativeFilePath($file));
}
}

0 comments on commit 0a07d89

Please sign in to comment.