#!/usr/bin/env php startBuffering(); // Dépendances du CLI, calculées depuis le graphe réel de Composer. // // Une liste écrite à la main est fatalement incomplète : l'autoloader charge // d'emblée des polyfills que personne ne pense à citer, et le PHAR échoue à // l'exécution — après le build, donc chez l'utilisateur. Partir de // `installed.json` rend l'ensemble correct par construction. $roots = ['symfony/console', 'symfony/http-client']; $installed = json_decode((string) file_get_contents($root.'/vendor/composer/installed.json'), true); if (!\is_array($installed) || !isset($installed['packages'])) { fwrite(\STDERR, "vendor/composer/installed.json illisible. Lancez « composer install ».\n"); exit(1); } $byName = []; foreach ($installed['packages'] as $package) { $byName[$package['name']] = $package; } /** Fermeture transitive des dépendances, hors extensions et PHP lui-même. */ $collect = static function (string $name, array &$seen) use (&$collect, $byName): void { if (isset($seen[$name]) || !isset($byName[$name])) { return; } $seen[$name] = true; foreach (array_keys($byName[$name]['require'] ?? []) as $dependency) { if (!str_contains($dependency, '/')) { continue; // php, ext-*, lib-* } $collect($dependency, $seen); } }; $seen = []; foreach ($roots as $rootPackage) { $collect($rootPackage, $seen); } // L'autoloader requiert d'office tous les fichiers déclarés en `autoload_files`. // On y prend les polyfills — indispensables — mais SURTOUT PAS les dépendances // de développement : PHPStan pèse à lui seul 47 Mo, et se retrouverait embarqué // dans un binaire qui n'en a aucun usage. // // `dev-package-names` est maintenu par Composer : s'appuyer dessus vaut mieux // que deviner ce qui relève du développement. $devPackages = array_flip((array) ($installed['dev-package-names'] ?? [])); $eager = require $root.'/vendor/composer/autoload_files.php'; $eagerKept = []; foreach ($eager as $hash => $file) { $normalised = str_replace('\\', '/', (string) $file); if (1 !== preg_match('#/vendor/([^/]+/[^/]+)/#', $normalised, $matches)) { continue; } if (isset($devPackages[$matches[1]])) { continue; } $seen[$matches[1]] = true; $eagerKept[$hash] = $matches[0].substr($normalised, strpos($normalised, $matches[0]) + \strlen($matches[0])); $eagerKept[$hash] = substr($normalised, strpos($normalised, '/vendor/') + \strlen('/vendor/')); } $vendors = array_keys($seen); sort($vendors); $added = 0; /** * Filtre d'inclusion. * * On garde TOUS les types de fichiers, pas seulement le PHP : Console lit ses * gabarits de complétion shell depuis un répertoire Resources/, et les omettre * produit un PHAR qui plante au lancement — pas au build. Ne sont écartés que * les tests et la documentation, qui ne servent jamais à l'exécution. */ $keep = static function (SplFileInfo $file): bool { $path = str_replace('\\', '/', $file->getPathname()); foreach (['/Tests/', '/tests/', '/docs/', '/.github/'] as $excluded) { if (str_contains($path, $excluded)) { return false; } } return !\in_array(strtolower($file->getExtension()), ['md', 'dist', 'lock'], true); }; // Uniquement src/Cli : l'autoloader de Composer n'est PAS embarqué. Il // référence des paquets écartés du binaire, et sa seule présence suffirait à // faire échouer le démarrage si quelque chose venait à le requérir. foreach (['src/Cli'] as $relative) { $base = $root.'/'.$relative; $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($base, FilesystemIterator::SKIP_DOTS)); foreach ($files as $file) { if (!$file instanceof SplFileInfo || !$keep($file)) { continue; } $phar->addFile($file->getPathname(), $relative.'/'.substr($file->getPathname(), \strlen($base) + 1)); ++$added; } } foreach ($vendors as $vendor) { $base = $root.'/vendor/'.$vendor; if (!is_dir($base)) { // Un paquet « replace » (les polyfills remplacés par PHP lui-même) n'a // pas de répertoire : c'est normal, on passe. continue; } $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($base, FilesystemIterator::SKIP_DOTS)); foreach ($files as $file) { if (!$file instanceof SplFileInfo || !$keep($file)) { continue; } $phar->addFile($file->getPathname(), 'vendor/'.$vendor.'/'.substr($file->getPathname(), \strlen($base) + 1)); ++$added; } } $phar->addFile($root.'/bin/tqs', 'bin/tqs'); ++$added; // Autoloader DÉDIÉ, généré ici. // // Réutiliser celui de l'application ne marche pas : Composer inline la liste des // fichiers à charger dans `autoload_static.php`, si bien que réécrire // `autoload_files.php` ne change rien — le PHAR tente alors de requérir des // paquets de développement absents, et meurt au démarrage. // // Trente lignes générées valent mieux qu'un rafistolage du générateur d'un // autre outil : ce qui est embarqué est exactement ce qui est déclaré. $psr4 = ['App\\Cli\\' => 'src/Cli/']; foreach ($vendors as $vendor) { $package = $byName[$vendor] ?? null; foreach (($package['autoload']['psr-4'] ?? []) as $namespace => $paths) { foreach ((array) $paths as $path) { $psr4[$namespace] = 'vendor/'.$vendor.('' === $path ? '/' : '/'.trim($path, '/').'/'); } } } $eagerFiles = []; foreach ($vendors as $vendor) { foreach (($byName[$vendor]['autoload']['files'] ?? []) as $file) { $eagerFiles[] = 'vendor/'.$vendor.'/'.ltrim($file, '/'); } } $phar->addFromString('autoload.php', sprintf( <<<'PHP' $directory) { if (!str_starts_with($class, $prefix)) { continue; } $relative = str_replace('\\', '/', substr($class, strlen($prefix))); $file = $base . $directory . $relative . '.php'; if (is_file($file)) { require $file; return; } } }); $files = %s; foreach ($files as $file) { require $base . $file; } PHP, var_export($psr4, true), var_export($eagerFiles, true), )); $phar->setStub(<<<'STUB' #!/usr/bin/env php stopBuffering(); chmod($target, 0o755); printf( "tqs.phar construit : %s\n %d fichiers · %d dépendances · %s\n", $target, $added, \count($vendors), number_format(filesize($target) / 1024, 0, ',', ' ').' Ko', );