Skip to content

Commit beefd06

Browse files
committed
issue #154 - PHAR file creation
1 parent 4c4ee9c commit beefd06

File tree

7 files changed

+287
-6
lines changed

7 files changed

+287
-6
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
/.phpunit.result.cache
33
/.php-cs-fixer.cache
44
/composer.lock
5+
/composer.phar
56
/phpstan.neon
67
/phpunit.xml
78
/vendor/
8-
test_db*
9+
test_db*

bin/compile

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/usr/bin/bash
2+
3+
ROOTPATH="`dirname $0`"
4+
ROOTPATH="`dirname $ROOTPATH`"
5+
COMPOSER="$ROOTPATH/composer.phar"
6+
7+
echo " ==> Using composer $COMPOSER"
8+
echo " ==> Will generate $ROOTPATH/db-tools.phar"
9+
10+
# Make a backup of the composer.json file.
11+
cp "$ROOTPATH/composer.json" "$ROOTPATH/composer.json.dist"
12+
13+
if [ ! -e "$COMPOSER" ]; then
14+
echo " ==> Download composer in $COMPOSER"
15+
wget --quiet https://getcomposer.org/download/latest-stable/composer.phar -o "$COMPOSER"
16+
fi
17+
18+
# Prepare composer, install without depdendencies.
19+
echo " ==> Prepare environment"
20+
rm -rf "$ROOTPATH/composer.lock"
21+
rm -rf "$ROOTPATH/vendor"
22+
23+
# Install PHAR only tooling.
24+
echo " ==> Require compile-only dependencies"
25+
php "$COMPOSER" -n require --no-audit composer/pcre:'^3.1' seld/phar-utils:'^1.2'
26+
php "$COMPOSER" -n -q config autoloader-suffix DbToolsPhar
27+
php "$COMPOSER" -n install --no-dev
28+
php "$COMPOSER" -n config autoloader-suffix --unset
29+
30+
# Compile PHAR file
31+
echo " ==> Running compilation"
32+
php -d phar.readonly=0 bin/compile.php
33+
chmod +x "$ROOTPATH/db-tools.phar"
34+
35+
# Clean up environment
36+
echo " ==> Cleaning up environment"
37+
cp "$ROOTPATH/composer.json.dist" "$ROOTPATH/composer.json"
38+
rm -rf "$ROOTPATH/composer.lock" "$ROOTPATH/composer.json.dist"
39+
php "$COMPOSER" -n -q install

bin/compile.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MakinaCorpus\DbToolsBundle;
6+
7+
use MakinaCorpus\DbToolsBundle\Bridge\Standalone\PharCompiler;
8+
9+
/**
10+
* Please run before running this:
11+
* $ composer config autoloader-suffix DbToolsPhar
12+
* $ composer install --no-dev
13+
* $ composer config autoloader-suffix --unset
14+
* $ php -d phar.readonly=0 bin/compile.php
15+
*/
16+
17+
(static function (): void {
18+
$autoloadFiles = [
19+
__DIR__ . '/../vendor/autoload.php',
20+
__DIR__ . '/../../../autoload.php',
21+
];
22+
23+
$autoloaderFound = false;
24+
foreach ($autoloadFiles as $autoloadFile) {
25+
if (!\file_exists($autoloadFile)) {
26+
continue;
27+
}
28+
require_once $autoloadFile;
29+
$autoloaderFound = true;
30+
}
31+
32+
if (!$autoloaderFound) {
33+
if (\extension_loaded('phar') && \Phar::running() !== '') {
34+
\fwrite(STDERR, 'The PHAR was built without dependencies!' . \PHP_EOL);
35+
exit(1);
36+
}
37+
\fwrite(STDERR, 'vendor/autoload.php could not be found. Did you run `composer install`?' . \PHP_EOL);
38+
exit(1);
39+
}
40+
41+
$cwd = \getcwd();
42+
\assert(\is_string($cwd));
43+
\chdir(__DIR__.'/../');
44+
$ts = \rtrim(\exec('git log -n1 --pretty=%ct HEAD'));
45+
if (!\is_numeric($ts)) {
46+
echo 'Could not detect date using "git log -n1 --pretty=%ct HEAD"'.\PHP_EOL;
47+
exit(1);
48+
}
49+
\chdir($cwd);
50+
51+
\error_reporting(-1);
52+
\ini_set('display_errors', '1');
53+
54+
$compiler = new PharCompiler();
55+
$compiler->compile();
56+
exit(1);
57+
})();

composer.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"makinacorpus/query-builder": "^1.6.1",
1717
"psr/log": "^3.0",
1818
"symfony/config": "^6.0|^7.0",
19+
"symfony/console": "^6.0|^7.0",
1920
"symfony/filesystem": "^6.0|^7.0",
2021
"symfony/finder": "^6.0|^7.0",
2122
"symfony/options-resolver": "^6.0|^7.0",
@@ -34,12 +35,13 @@
3435
"symfony/validator": "^6.3|^7.0"
3536
},
3637
"suggest": {
37-
"doctrine/doctrine-bundle": "For autoconfiguration in Symfony project context",
38-
"symfony/console": "In order to use the standalone CLI tool or Symfony console commands",
3938
"symfony/password-hasher": "In order to use the password hash anonymizer"
4039
},
4140
"conflict": {
42-
"symfony/console": "<6.0|>=8.0",
41+
"composer/pcre": "<3.1|>=4.0",
42+
"doctrine/dbal": "<3.0|>=5.0",
43+
"doctrine/orm": "<2.15|>=4.0",
44+
"seld/phar-utils": "<1.2|>=2.0",
4345
"symfony/password-hasher": "<6.0|>=8.0"
4446
},
4547
"autoload": {

phpstan.neon.dist

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@ parameters:
66
excludePaths:
77
- src/DependencyInjection/DbToolsConfiguration.php
88
checkMissingOverrideMethodAttribute: true
9+
ignoreErrors:
10+
- '#Instantiated class Seld\\PharUtils\\Timestamps not found.#'
11+
- '#on an unknown class Seld\\PharUtils\\Timestamps.#'

src/Bridge/Standalone/Bootstrap.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,10 @@ public static function run(): void
5555
*/
5656
public static function createApplication(): Application
5757
{
58-
// @todo Test in PHAR context.
5958
if (\class_exists(InstalledVersions::class)) {
6059
$version = InstalledVersions::getVersion('makinacorpus/db-tools-bundle');
6160
}
6261
$version ??= 'cli';
63-
\assert($version !== null);
6462

6563
$application = new Application('Db Tools', $version);
6664
$application->setCatchExceptions(true);
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MakinaCorpus\DbToolsBundle\Bridge\Standalone;
6+
7+
use Composer\Pcre\Preg;
8+
use Symfony\Component\Finder\Finder;
9+
use Symfony\Component\Process\Process;
10+
use Seld\PharUtils\Timestamps;
11+
12+
/**
13+
* The Compiler class compiles composer into a phar.
14+
*
15+
* Heavily inspired from composer code, all credits to their authors.
16+
*
17+
* @see https://github.com/composer/composer/blob/main/src/Composer/Compiler.php
18+
* @see https://getcomposer.org/
19+
*/
20+
class PharCompiler
21+
{
22+
private \DateTime $versionDate;
23+
24+
/**
25+
* Creates the PHAR.
26+
*
27+
* @param ?string $pharFile
28+
* Full target PHAR file name.
29+
*/
30+
public function compile(?string $pharFile = null): void
31+
{
32+
$pharFile ??= \dirname(__DIR__, 3) . '/db-tools.phar';
33+
34+
if (\file_exists($pharFile)) {
35+
\unlink($pharFile);
36+
}
37+
38+
$rootDir = \dirname(__DIR__, 3);
39+
40+
// Next line would fetch the current reference (commit hash or tag).
41+
// $process = new Process(['git', 'log', '--pretty=%H', '-n1', 'HEAD'], $rootDir);
42+
43+
$process = new Process(['git', 'log', '-n1', '--pretty=%ci', 'HEAD'], $rootDir);
44+
if ($process->run() !== 0) {
45+
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.');
46+
}
47+
48+
$this->versionDate = new \DateTime(\trim($process->getOutput()));
49+
$this->versionDate->setTimezone(new \DateTimeZone('UTC'));
50+
51+
$phar = new \Phar($pharFile, 0, 'db-tools.phar');
52+
$phar->setSignatureAlgorithm(\Phar::SHA512);
53+
54+
$phar->startBuffering();
55+
56+
$finderSort = static fn ($a, $b): int => \strcmp(\strtr($a->getRealPath(), '\\', '/'), \strtr($b->getRealPath(), '\\', '/'));
57+
58+
// Local package sources.
59+
$finder = new Finder();
60+
$finder->files()
61+
->ignoreVCS(true)
62+
->name('*.php')
63+
->notName('Compiler.php')
64+
->notName('ClassLoader.php')
65+
->notName('InstalledVersions.php')
66+
->in($rootDir.'/src')
67+
->sort($finderSort)
68+
;
69+
foreach ($finder as $file) {
70+
$this->addFile($phar, $file);
71+
}
72+
// Add runtime utilities separately to make sure they retains the docblocks as these will get copied into projects.
73+
$this->addFile($phar, new \SplFileInfo($rootDir . '/vendor/composer/ClassLoader.php'), false);
74+
$this->addFile($phar, new \SplFileInfo($rootDir . '/vendor/composer/InstalledVersions.php'), false);
75+
76+
// Add vendor files
77+
$finder = new Finder();
78+
$finder->files()
79+
->ignoreVCS(true)
80+
->notPath('/\/(composer\.(json|lock)|[A-Z]+\.md(?:own)?|\.gitignore|appveyor.yml|phpunit\.xml\.dist|phpstan\.neon\.dist|phpstan-config\.neon|phpstan-baseline\.neon)$/')
81+
->notPath('/(.*\.(md|xml|twig|svg)|Dockerfile|phpbench\.json|yaml-lint|dev\.sh|docker-compose\.(yaml|yml)|run-tests\.sh)/')
82+
->notPath('/bin\/(jsonlint|validate-json|simple-phpunit|phpstan|phpstan\.phar)(\.bat)?$/')
83+
->notPath('justinrainbow/json-schema/demo/')
84+
->notPath('justinrainbow/json-schema/dist/')
85+
->notPath('composer/LICENSE')
86+
->exclude('Tests')
87+
->exclude('tests')
88+
->exclude('docs')
89+
->in($rootDir.'/vendor/')
90+
->sort($finderSort)
91+
;
92+
93+
$extraFiles = [];
94+
foreach ([
95+
$rootDir . '/vendor/composer/installed.json',
96+
// CaBundle::getBundledCaBundlePath(),
97+
$rootDir . '/vendor/composer/installed.json',
98+
$rootDir . '/vendor/symfony/console/Resources/bin/hiddeninput.exe',
99+
$rootDir . '/vendor/symfony/console/Resources/completion.bash',
100+
$rootDir . '/vendor/symfony/console/Resources/completion.fish',
101+
$rootDir . '/vendor/symfony/console/Resources/completion.zsh',
102+
$rootDir . '/vendor/composer/installed.json',
103+
] as $file) {
104+
$extraFiles[$file] = \realpath($file);
105+
if (!\file_exists($file)) {
106+
throw new \RuntimeException('Extra file listed is missing from the filesystem: '.$file);
107+
}
108+
}
109+
$unexpectedFiles = [];
110+
111+
foreach ($finder as $file) {
112+
if (false !== ($index = \array_search($file->getRealPath(), $extraFiles, true))) {
113+
unset($extraFiles[$index]);
114+
} elseif (!Preg::isMatch('{(^LICENSE$|\.php$)}', $file->getFilename())) {
115+
$unexpectedFiles[] = (string) $file;
116+
}
117+
118+
if (Preg::isMatch('{\.php[\d.]*$}', $file->getFilename())) {
119+
$this->addFile($phar, $file);
120+
} else {
121+
$this->addFile($phar, $file, false);
122+
}
123+
}
124+
125+
if (\count($extraFiles) > 0) {
126+
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));
127+
}
128+
if (\count($unexpectedFiles) > 0) {
129+
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));
130+
}
131+
132+
// Add binary.
133+
$phar->addFile($rootDir . '/bin/db-tools.php', 'bin/db-tools.php');
134+
$content = \file_get_contents($rootDir.'/bin/db-tools');
135+
$content = Preg::replace('{^#!/usr/bin/env php\s*}', '', $content);
136+
$phar->addFromString('bin/db-tools', $content);
137+
138+
// Stubs
139+
$phar->setStub(
140+
<<<'EOT'
141+
#!/usr/bin/env php
142+
<?php
143+
if (!\class_exists('Phar')) {
144+
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;
145+
exit(1);
146+
}
147+
Phar::mapPhar('db-tools.phar');
148+
require 'phar://db-tools.phar/bin/db-tools';
149+
__HALT_COMPILER();
150+
EOT,
151+
);
152+
153+
$phar->stopBuffering();
154+
155+
//$this->addFile($phar, new \SplFileInfo($rootDir.'/LICENSE.md'), false);
156+
157+
unset($phar);
158+
159+
// re-sign the phar with reproducible timestamp / signature
160+
$util = new Timestamps($pharFile);
161+
$util->updateTimestamps($this->versionDate);
162+
$util->save($pharFile, \Phar::SHA512);
163+
}
164+
165+
private function getRelativeFilePath(\SplFileInfo $file): string
166+
{
167+
$rootDir = \dirname(__DIR__, 3);
168+
169+
$realPath = $file->getRealPath();
170+
$pathPrefix = $rootDir . DIRECTORY_SEPARATOR;
171+
$pos = \strpos($realPath, $pathPrefix);
172+
$relativePath = ($pos !== false) ? \substr_replace($realPath, '', $pos, \strlen($pathPrefix)) : $realPath;
173+
174+
return \strtr($relativePath, '\\', '/');
175+
}
176+
177+
private function addFile(\Phar $phar, \SplFileInfo $file, bool $strip = true): void
178+
{
179+
$phar->addFile($this->getRelativeFilePath($file));
180+
}
181+
}

0 commit comments

Comments
 (0)