|
| 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