From d826187b330ee137542c68bddc85486fd6073ff7 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 3 Dec 2025 22:05:13 +0000 Subject: [PATCH 01/18] 434: added initial implementation of build tool checker --- src/Container.php | 8 ++ .../BuildTools/CheckAllBuildTools.php | 84 +++++++++++++++++++ .../CheckIndividualBuildToolInPath.php | 30 +++++++ src/SelfManage/BuildTools/PackageManager.php | 55 ++++++++++++ 4 files changed, 177 insertions(+) create mode 100644 src/SelfManage/BuildTools/CheckAllBuildTools.php create mode 100644 src/SelfManage/BuildTools/CheckIndividualBuildToolInPath.php create mode 100644 src/SelfManage/BuildTools/PackageManager.php diff --git a/src/Container.php b/src/Container.php index c24182b..a262c6c 100644 --- a/src/Container.php +++ b/src/Container.php @@ -39,6 +39,7 @@ use Php\Pie\Installing\UninstallUsingUnlink; use Php\Pie\Installing\UnixInstall; use Php\Pie\Installing\WindowsInstall; +use Php\Pie\SelfManage\BuildTools\CheckAllBuildTools; use Psr\Container\ContainerInterface; use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\Event\ConsoleCommandEvent; @@ -197,6 +198,13 @@ static function (ContainerInterface $container): Install { }, ); + $container->singleton( + CheckAllBuildTools::class, + static function (): CheckAllBuildTools { + return CheckAllBuildTools::buildToolsFactory(); + }, + ); + $container->alias(UninstallUsingUnlink::class, Uninstall::class); $container->alias(Ini\RemoveIniEntryWithFileGetContents::class, Ini\RemoveIniEntry::class); diff --git a/src/SelfManage/BuildTools/CheckAllBuildTools.php b/src/SelfManage/BuildTools/CheckAllBuildTools.php new file mode 100644 index 0000000..e18459e --- /dev/null +++ b/src/SelfManage/BuildTools/CheckAllBuildTools.php @@ -0,0 +1,84 @@ +value => 'gcc', + PackageManager::Apk->value => 'build-base', + ], + ), + new CheckIndividualBuildToolInPath( + 'make', + [ + PackageManager::Apt->value => 'make', + PackageManager::Apk->value => 'make', + ], + ), + ]); + } + + /** @param list $buildTools */ + public function __construct( + private readonly array $buildTools, + ) { + } + + public function check(IOInterface $io, bool $forceInstall): void + { + $io->write('Checking if all build tools are installed.', verbosity: IOInterface::VERBOSE); + $packageManager = PackageManager::detect(); + $missingTools = []; + $packagesToSuggest = []; + $allFound = true; + foreach ($this->buildTools as $buildTool) { + if ($buildTool->check() !== false) { + $io->write('Build tool ' . $buildTool->tool . ' is installed.', verbosity: IOInterface::VERY_VERBOSE); + continue; + } + + $allFound = false; + $missingTools[] = $buildTool->tool; + $packagesToSuggest[] = $buildTool->packageNameFor($packageManager); + } + + if ($allFound) { + $io->write('All build tools found.', verbosity: IOInterface::VERBOSE); + + return; + } + + $io->write('The following build tools are missing: ' . implode(', ', $missingTools) . ''); + + if (! $io->isInteractive() && ! $forceInstall) { + $io->writeError('You are not running in interactive mode, and --force was not specified. You may need to install the following build tools: ' . implode(' ', $packagesToSuggest) . ''); + + return; + } + + $io->write('The following command will be run: ' . implode(' ', $packageManager->installCommand($packagesToSuggest)), verbosity: IOInterface::VERBOSE); + + if ($io->isInteractive() && ! $forceInstall) { + if (! $io->askConfirmation('Would you like to install them now?', false)) { + $io->write('Ok, but things might not work. Just so you know.'); + + return; + } + } + + $packageManager->install($packagesToSuggest); + $io->write('Build tools installed.'); + } +} diff --git a/src/SelfManage/BuildTools/CheckIndividualBuildToolInPath.php b/src/SelfManage/BuildTools/CheckIndividualBuildToolInPath.php new file mode 100644 index 0000000..86a1d60 --- /dev/null +++ b/src/SelfManage/BuildTools/CheckIndividualBuildToolInPath.php @@ -0,0 +1,30 @@ +find($this->tool) !== null; + } + + public function packageNameFor(PackageManager $packageManager): string + { + // @todo could we do a check the package exists? + + // If we need to customise specific package names depending on OS + // specific parameters, this is likely the place to do it + return $this->packageManagerPackages[$packageManager->value]; + } +} diff --git a/src/SelfManage/BuildTools/PackageManager.php b/src/SelfManage/BuildTools/PackageManager.php new file mode 100644 index 0000000..667367d --- /dev/null +++ b/src/SelfManage/BuildTools/PackageManager.php @@ -0,0 +1,55 @@ +find($packageManager->value) !== null) { + return $packageManager; + } + } + + return null; + } + + /** + * @param list $packages + * + * @return list + */ + public function installCommand(array $packages): array + { + $cmd = match ($this) { + self::Apt => ['apt-get', 'install', '-y', '--no-install-recommends', '--no-install-suggests', ...$packages], + self::Apk => ['apk', 'add', '--no-cache', ...$packages], + }; + + if (Sudo::exists()) { + array_unshift($cmd, Sudo::find()); + } + + return $cmd; + } + + /** @param list $packages */ + public function install(array $packages): void + { + Process::run(self::installCommand($packages)); + } +} From 90b164dabe71d86399a3fb8a047c71a00513b814 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 4 Dec 2025 13:38:16 +0000 Subject: [PATCH 02/18] 434: define more build tools in checker and deduplicate --- .../BuildTools/CheckAllBuildTools.php | 44 ++++++++++++++++--- src/SelfManage/BuildTools/PackageManager.php | 2 + 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/SelfManage/BuildTools/CheckAllBuildTools.php b/src/SelfManage/BuildTools/CheckAllBuildTools.php index e18459e..756a7e9 100644 --- a/src/SelfManage/BuildTools/CheckAllBuildTools.php +++ b/src/SelfManage/BuildTools/CheckAllBuildTools.php @@ -6,12 +6,14 @@ use Composer\IO\IOInterface; +use function array_unique; use function implode; final class CheckAllBuildTools { public static function buildToolsFactory(): self { + // @todo libtool return new self([ new CheckIndividualBuildToolInPath( 'gcc', @@ -24,7 +26,35 @@ public static function buildToolsFactory(): self 'make', [ PackageManager::Apt->value => 'make', - PackageManager::Apk->value => 'make', + PackageManager::Apk->value => 'build-base', + ], + ), + new CheckIndividualBuildToolInPath( + 'autoconf', + [ + PackageManager::Apt->value => 'autoconf', + PackageManager::Apk->value => 'autoconf', + ], + ), + new CheckIndividualBuildToolInPath( + 'bison', + [ + PackageManager::Apt->value => 'bison', + PackageManager::Apk->value => 'bison', + ], + ), + new CheckIndividualBuildToolInPath( + 're2c', + [ + PackageManager::Apt->value => 're2c', + PackageManager::Apk->value => 're2c', + ], + ), + new CheckIndividualBuildToolInPath( + 'pkg-config', + [ + PackageManager::Apt->value => 'pkg-config', + PackageManager::Apk->value => 'pkgconfig', ], ), ]); @@ -41,7 +71,7 @@ public function check(IOInterface $io, bool $forceInstall): void $io->write('Checking if all build tools are installed.', verbosity: IOInterface::VERBOSE); $packageManager = PackageManager::detect(); $missingTools = []; - $packagesToSuggest = []; + $packagesToInstall = []; $allFound = true; foreach ($this->buildTools as $buildTool) { if ($buildTool->check() !== false) { @@ -51,7 +81,7 @@ public function check(IOInterface $io, bool $forceInstall): void $allFound = false; $missingTools[] = $buildTool->tool; - $packagesToSuggest[] = $buildTool->packageNameFor($packageManager); + $packagesToInstall[] = $buildTool->packageNameFor($packageManager); } if ($allFound) { @@ -63,12 +93,14 @@ public function check(IOInterface $io, bool $forceInstall): void $io->write('The following build tools are missing: ' . implode(', ', $missingTools) . ''); if (! $io->isInteractive() && ! $forceInstall) { - $io->writeError('You are not running in interactive mode, and --force was not specified. You may need to install the following build tools: ' . implode(' ', $packagesToSuggest) . ''); + $io->writeError('You are not running in interactive mode, and --force was not specified. You may need to install the following build tools: ' . implode(' ', $packagesToInstall) . ''); return; } - $io->write('The following command will be run: ' . implode(' ', $packageManager->installCommand($packagesToSuggest)), verbosity: IOInterface::VERBOSE); + $packagesToInstall = array_unique($packagesToInstall); + + $io->write('The following command will be run: ' . implode(' ', $packageManager->installCommand($packagesToInstall)), verbosity: IOInterface::VERBOSE); if ($io->isInteractive() && ! $forceInstall) { if (! $io->askConfirmation('Would you like to install them now?', false)) { @@ -78,7 +110,7 @@ public function check(IOInterface $io, bool $forceInstall): void } } - $packageManager->install($packagesToSuggest); + $packageManager->install($packagesToInstall); $io->write('Build tools installed.'); } } diff --git a/src/SelfManage/BuildTools/PackageManager.php b/src/SelfManage/BuildTools/PackageManager.php index 667367d..68c64fd 100644 --- a/src/SelfManage/BuildTools/PackageManager.php +++ b/src/SelfManage/BuildTools/PackageManager.php @@ -14,6 +14,8 @@ enum PackageManager: string { case Apt = 'apt-get'; case Apk = 'apk'; + // @todo dnf + // @todo yum public static function detect(): self|null { From 4a85df46574c0dc3e63eea01b925043ed35bc0d9 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 4 Dec 2025 14:07:55 +0000 Subject: [PATCH 03/18] Allow build tool package name to be null --- ...olInPath.php => BinaryBuildToolFinder.php} | 9 +++-- .../BuildTools/CheckAllBuildTools.php | 39 +++++++++++++------ src/SelfManage/BuildTools/PackageManager.php | 2 + 3 files changed, 34 insertions(+), 16 deletions(-) rename src/SelfManage/BuildTools/{CheckIndividualBuildToolInPath.php => BinaryBuildToolFinder.php} (69%) diff --git a/src/SelfManage/BuildTools/CheckIndividualBuildToolInPath.php b/src/SelfManage/BuildTools/BinaryBuildToolFinder.php similarity index 69% rename from src/SelfManage/BuildTools/CheckIndividualBuildToolInPath.php rename to src/SelfManage/BuildTools/BinaryBuildToolFinder.php index 86a1d60..2087f76 100644 --- a/src/SelfManage/BuildTools/CheckIndividualBuildToolInPath.php +++ b/src/SelfManage/BuildTools/BinaryBuildToolFinder.php @@ -6,8 +6,10 @@ use Symfony\Component\Process\ExecutableFinder; -class CheckIndividualBuildToolInPath +/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ +class BinaryBuildToolFinder { + /** @param array $packageManagerPackages */ public function __construct( public readonly string $tool, private readonly array $packageManagerPackages, @@ -19,10 +21,9 @@ public function check(): bool return (new ExecutableFinder())->find($this->tool) !== null; } - public function packageNameFor(PackageManager $packageManager): string + /** @return non-empty-string|null */ + public function packageNameFor(PackageManager $packageManager): string|null { - // @todo could we do a check the package exists? - // If we need to customise specific package names depending on OS // specific parameters, this is likely the place to do it return $this->packageManagerPackages[$packageManager->value]; diff --git a/src/SelfManage/BuildTools/CheckAllBuildTools.php b/src/SelfManage/BuildTools/CheckAllBuildTools.php index 756a7e9..edcee5a 100644 --- a/src/SelfManage/BuildTools/CheckAllBuildTools.php +++ b/src/SelfManage/BuildTools/CheckAllBuildTools.php @@ -9,58 +9,65 @@ use function array_unique; use function implode; -final class CheckAllBuildTools +/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ +class CheckAllBuildTools { public static function buildToolsFactory(): self { - // @todo libtool return new self([ - new CheckIndividualBuildToolInPath( + new BinaryBuildToolFinder( 'gcc', [ PackageManager::Apt->value => 'gcc', PackageManager::Apk->value => 'build-base', ], ), - new CheckIndividualBuildToolInPath( + new BinaryBuildToolFinder( 'make', [ PackageManager::Apt->value => 'make', PackageManager::Apk->value => 'build-base', ], ), - new CheckIndividualBuildToolInPath( + new BinaryBuildToolFinder( 'autoconf', [ PackageManager::Apt->value => 'autoconf', PackageManager::Apk->value => 'autoconf', ], ), - new CheckIndividualBuildToolInPath( + new BinaryBuildToolFinder( 'bison', [ PackageManager::Apt->value => 'bison', PackageManager::Apk->value => 'bison', ], ), - new CheckIndividualBuildToolInPath( + new BinaryBuildToolFinder( 're2c', [ PackageManager::Apt->value => 're2c', PackageManager::Apk->value => 're2c', ], ), - new CheckIndividualBuildToolInPath( + new BinaryBuildToolFinder( 'pkg-config', [ PackageManager::Apt->value => 'pkg-config', PackageManager::Apk->value => 'pkgconfig', ], ), + new BinaryBuildToolFinder( + 'libtoolize', + [ + PackageManager::Apt->value => 'libtool', + PackageManager::Apk->value => 'libtool', + ], + ), ]); } - /** @param list $buildTools */ + /** @param list $buildTools */ public function __construct( private readonly array $buildTools, ) { @@ -73,15 +80,23 @@ public function check(IOInterface $io, bool $forceInstall): void $missingTools = []; $packagesToInstall = []; $allFound = true; + foreach ($this->buildTools as $buildTool) { if ($buildTool->check() !== false) { $io->write('Build tool ' . $buildTool->tool . ' is installed.', verbosity: IOInterface::VERY_VERBOSE); continue; } - $allFound = false; - $missingTools[] = $buildTool->tool; - $packagesToInstall[] = $buildTool->packageNameFor($packageManager); + $allFound = false; + $missingTools[] = $buildTool->tool; + $packageName = $buildTool->packageNameFor($packageManager); + + if ($packageName === null) { + $io->writeError('Could not find package name for build tool ' . $buildTool->tool . '.'); + continue; + } + + $packagesToInstall[] = $packageName; } if ($allFound) { diff --git a/src/SelfManage/BuildTools/PackageManager.php b/src/SelfManage/BuildTools/PackageManager.php index 68c64fd..6cea7ed 100644 --- a/src/SelfManage/BuildTools/PackageManager.php +++ b/src/SelfManage/BuildTools/PackageManager.php @@ -10,12 +10,14 @@ use function array_unshift; +/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ enum PackageManager: string { case Apt = 'apt-get'; case Apk = 'apk'; // @todo dnf // @todo yum + // @todo brew public static function detect(): self|null { From ae10e7fce28c359ec714eb3af1a00a8e7ec41fc4 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 4 Dec 2025 17:12:33 +0000 Subject: [PATCH 04/18] 434: improved messaging and flow for prompts --- .../BuildTools/CheckAllBuildTools.php | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/src/SelfManage/BuildTools/CheckAllBuildTools.php b/src/SelfManage/BuildTools/CheckAllBuildTools.php index edcee5a..f628b26 100644 --- a/src/SelfManage/BuildTools/CheckAllBuildTools.php +++ b/src/SelfManage/BuildTools/CheckAllBuildTools.php @@ -7,6 +7,8 @@ use Composer\IO\IOInterface; use function array_unique; +use function array_values; +use function count; use function implode; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ @@ -76,9 +78,10 @@ public function __construct( public function check(IOInterface $io, bool $forceInstall): void { $io->write('Checking if all build tools are installed.', verbosity: IOInterface::VERBOSE); - $packageManager = PackageManager::detect(); - $missingTools = []; + /** @var list $packagesToInstall */ $packagesToInstall = []; + $missingTools = []; + $packageManager = PackageManager::detect(); $allFound = true; foreach ($this->buildTools as $buildTool) { @@ -89,10 +92,15 @@ public function check(IOInterface $io, bool $forceInstall): void $allFound = false; $missingTools[] = $buildTool->tool; - $packageName = $buildTool->packageNameFor($packageManager); + + if ($packageManager === null) { + continue; + } + + $packageName = $buildTool->packageNameFor($packageManager); if ($packageName === null) { - $io->writeError('Could not find package name for build tool ' . $buildTool->tool . '.'); + $io->writeError('Could not find package name for build tool ' . $buildTool->tool . '.', verbosity: IOInterface::VERBOSE); continue; } @@ -107,15 +115,27 @@ public function check(IOInterface $io, bool $forceInstall): void $io->write('The following build tools are missing: ' . implode(', ', $missingTools) . ''); - if (! $io->isInteractive() && ! $forceInstall) { - $io->writeError('You are not running in interactive mode, and --force was not specified. You may need to install the following build tools: ' . implode(' ', $packagesToInstall) . ''); + if ($packageManager === null) { + $io->write('Could not find a package manager to install the missing build tools.'); return; } - $packagesToInstall = array_unique($packagesToInstall); + if (! count($packagesToInstall)) { + $io->write('Could not determine packages to install.'); + + return; + } + + $proposedInstallCommand = implode(' ', $packageManager->installCommand(array_values(array_unique($packagesToInstall)))); + + if (! $io->isInteractive() && ! $forceInstall) { + $io->writeError('You are not running in interactive mode. You may need to run: ' . $proposedInstallCommand . ''); + + return; + } - $io->write('The following command will be run: ' . implode(' ', $packageManager->installCommand($packagesToInstall)), verbosity: IOInterface::VERBOSE); + $io->write('The following command will be run: ' . $proposedInstallCommand, verbosity: IOInterface::VERY_VERBOSE); if ($io->isInteractive() && ! $forceInstall) { if (! $io->askConfirmation('Would you like to install them now?', false)) { @@ -125,7 +145,7 @@ public function check(IOInterface $io, bool $forceInstall): void } } - $packageManager->install($packagesToInstall); - $io->write('Build tools installed.'); + $packageManager->install(array_values(array_unique($packagesToInstall))); + $io->write('Missing build tools have been installed.'); } } From 88068ee086dee0eab06853db92055c6692626d4b Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 4 Dec 2025 17:15:44 +0000 Subject: [PATCH 05/18] 434: add build tools check for build and install commands --- src/Command/BuildCommand.php | 4 ++++ src/Command/InstallCommand.php | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/Command/BuildCommand.php b/src/Command/BuildCommand.php index 3b941db..1a5cc3c 100644 --- a/src/Command/BuildCommand.php +++ b/src/Command/BuildCommand.php @@ -15,6 +15,7 @@ use Php\Pie\DependencyResolver\InvalidPackageName; use Php\Pie\DependencyResolver\UnableToResolveRequirement; use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages; +use Php\Pie\SelfManage\BuildTools\CheckAllBuildTools; use Psr\Container\ContainerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -35,6 +36,7 @@ public function __construct( private readonly ComposerIntegrationHandler $composerIntegrationHandler, private readonly FindMatchingPackages $findMatchingPackages, private readonly IOInterface $io, + private readonly CheckAllBuildTools $checkBuildTools, ) { parent::__construct(); } @@ -64,6 +66,8 @@ public function execute(InputInterface $input, OutputInterface $output): int $forceInstallPackageVersion = CommandHelper::determineForceInstallingPackageVersion($input); CommandHelper::applyNoCacheOptionIfSet($input, $this->io); + $this->checkBuildTools->check($this->io, false); // @todo force or not + $composer = PieComposerFactory::createPieComposer( $this->container, new PieComposerRequest( diff --git a/src/Command/InstallCommand.php b/src/Command/InstallCommand.php index d4b66bd..8dd8055 100644 --- a/src/Command/InstallCommand.php +++ b/src/Command/InstallCommand.php @@ -16,6 +16,7 @@ use Php\Pie\DependencyResolver\UnableToResolveRequirement; use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages; use Php\Pie\Platform\TargetPlatform; +use Php\Pie\SelfManage\BuildTools\CheckAllBuildTools; use Psr\Container\ContainerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -37,6 +38,7 @@ public function __construct( private readonly InvokeSubCommand $invokeSubCommand, private readonly FindMatchingPackages $findMatchingPackages, private readonly IOInterface $io, + private readonly CheckAllBuildTools $checkBuildTools, ) { parent::__construct(); } @@ -78,6 +80,8 @@ public function execute(InputInterface $input, OutputInterface $output): int $forceInstallPackageVersion = CommandHelper::determineForceInstallingPackageVersion($input); CommandHelper::applyNoCacheOptionIfSet($input, $this->io); + $this->checkBuildTools->check($this->io, false); // @todo force or not + $composer = PieComposerFactory::createPieComposer( $this->container, new PieComposerRequest( From 5b33c984e0e4314edae7ae0515617aabcc00ee93 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 8 Dec 2025 15:46:22 +0000 Subject: [PATCH 06/18] 434: add --auto-install-build-tools and --no-build-tools-check flags for controlling build tools checker --- src/Command/BuildCommand.php | 4 ++- src/Command/CommandHelper.php | 27 +++++++++++++++++++ src/Command/InstallCommand.php | 4 ++- .../BuildTools/CheckAllBuildTools.php | 10 ++++--- src/SelfManage/BuildTools/PackageManager.php | 17 ++++++------ 5 files changed, 48 insertions(+), 14 deletions(-) diff --git a/src/Command/BuildCommand.php b/src/Command/BuildCommand.php index 1a5cc3c..2099d24 100644 --- a/src/Command/BuildCommand.php +++ b/src/Command/BuildCommand.php @@ -66,7 +66,9 @@ public function execute(InputInterface $input, OutputInterface $output): int $forceInstallPackageVersion = CommandHelper::determineForceInstallingPackageVersion($input); CommandHelper::applyNoCacheOptionIfSet($input, $this->io); - $this->checkBuildTools->check($this->io, false); // @todo force or not + if (CommandHelper::shouldCheckForBuildTools($input)) { + $this->checkBuildTools->check($this->io, CommandHelper::autoInstallBuildTools($input)); + } $composer = PieComposerFactory::createPieComposer( $this->container, diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index c43ff07..e79a871 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -61,6 +61,8 @@ final class CommandHelper private const OPTION_SKIP_ENABLE_EXTENSION = 'skip-enable-extension'; private const OPTION_FORCE = 'force'; private const OPTION_NO_CACHE = 'no-cache'; + private const OPTION_AUTO_INSTALL_BUILD_TOOLS = 'auto-install-build-tools'; + private const OPTION_SUPPRESS_BUILD_TOOLS_CHECK = 'no-build-tools-check'; private function __construct() { @@ -139,6 +141,19 @@ public static function configureDownloadBuildInstallOptions(Command $command, bo 'When installing a PHP project, allow non-interactive project installations. Only used in certain contexts.', ); + $command->addOption( + self::OPTION_AUTO_INSTALL_BUILD_TOOLS, + null, + InputOption::VALUE_NONE, + 'If build tools are missing, automatically install them, instead of prompting.', + ); + $command->addOption( + self::OPTION_SUPPRESS_BUILD_TOOLS_CHECK, + null, + InputOption::VALUE_NONE, + 'Do not perform the check to see if build tools are present on the system.', + ); + /** * Allows additional options for the `./configure` command to be passed here. * Note, this means you probably need to call {@see self::validateInput()} to validate the input manually... @@ -228,6 +243,18 @@ public static function determineForceInstallingPackageVersion(InputInterface $in return $input->hasOption(self::OPTION_FORCE) && $input->getOption(self::OPTION_FORCE); } + public static function autoInstallBuildTools(InputInterface $input): bool + { + return $input->hasOption(self::OPTION_AUTO_INSTALL_BUILD_TOOLS) + && $input->getOption(self::OPTION_AUTO_INSTALL_BUILD_TOOLS); + } + + public static function shouldCheckForBuildTools(InputInterface $input): bool + { + return ! $input->hasOption(self::OPTION_SUPPRESS_BUILD_TOOLS_CHECK) + || ! $input->getOption(self::OPTION_SUPPRESS_BUILD_TOOLS_CHECK); + } + public static function determinePhpizePathFromInputs(InputInterface $input): PhpizePath|null { if ($input->hasOption(self::OPTION_WITH_PHPIZE_PATH)) { diff --git a/src/Command/InstallCommand.php b/src/Command/InstallCommand.php index 8dd8055..c7f3c20 100644 --- a/src/Command/InstallCommand.php +++ b/src/Command/InstallCommand.php @@ -80,7 +80,9 @@ public function execute(InputInterface $input, OutputInterface $output): int $forceInstallPackageVersion = CommandHelper::determineForceInstallingPackageVersion($input); CommandHelper::applyNoCacheOptionIfSet($input, $this->io); - $this->checkBuildTools->check($this->io, false); // @todo force or not + if (CommandHelper::shouldCheckForBuildTools($input)) { + $this->checkBuildTools->check($this->io, CommandHelper::autoInstallBuildTools($input)); + } $composer = PieComposerFactory::createPieComposer( $this->container, diff --git a/src/SelfManage/BuildTools/CheckAllBuildTools.php b/src/SelfManage/BuildTools/CheckAllBuildTools.php index f628b26..2f66484 100644 --- a/src/SelfManage/BuildTools/CheckAllBuildTools.php +++ b/src/SelfManage/BuildTools/CheckAllBuildTools.php @@ -75,7 +75,7 @@ public function __construct( ) { } - public function check(IOInterface $io, bool $forceInstall): void + public function check(IOInterface $io, bool $autoInstallIfMissing): void { $io->write('Checking if all build tools are installed.', verbosity: IOInterface::VERBOSE); /** @var list $packagesToInstall */ @@ -129,15 +129,17 @@ public function check(IOInterface $io, bool $forceInstall): void $proposedInstallCommand = implode(' ', $packageManager->installCommand(array_values(array_unique($packagesToInstall)))); - if (! $io->isInteractive() && ! $forceInstall) { - $io->writeError('You are not running in interactive mode. You may need to run: ' . $proposedInstallCommand . ''); + if (! $io->isInteractive() && ! $autoInstallIfMissing) { + $io->writeError('You are not running in interactive mode, and you did not provide the --auto-install-build-tools flag.'); + $io->writeError('You may need to run: ' . $proposedInstallCommand . ''); + $io->writeError(''); return; } $io->write('The following command will be run: ' . $proposedInstallCommand, verbosity: IOInterface::VERY_VERBOSE); - if ($io->isInteractive() && ! $forceInstall) { + if ($io->isInteractive() && ! $autoInstallIfMissing) { if (! $io->askConfirmation('Would you like to install them now?', false)) { $io->write('Ok, but things might not work. Just so you know.'); diff --git a/src/SelfManage/BuildTools/PackageManager.php b/src/SelfManage/BuildTools/PackageManager.php index 6cea7ed..578c66b 100644 --- a/src/SelfManage/BuildTools/PackageManager.php +++ b/src/SelfManage/BuildTools/PackageManager.php @@ -39,21 +39,22 @@ public static function detect(): self|null */ public function installCommand(array $packages): array { - $cmd = match ($this) { + return match ($this) { self::Apt => ['apt-get', 'install', '-y', '--no-install-recommends', '--no-install-suggests', ...$packages], self::Apk => ['apk', 'add', '--no-cache', ...$packages], }; - - if (Sudo::exists()) { - array_unshift($cmd, Sudo::find()); - } - - return $cmd; } /** @param list $packages */ public function install(array $packages): void { - Process::run(self::installCommand($packages)); + $cmd = self::installCommand($packages); + + // @todo ideally only add sudo if it's needed + if (Sudo::exists()) { + array_unshift($cmd, Sudo::find()); + } + + Process::run($cmd); } } From d0613824edde6debf60b9c7fdd15b2b2a05d46b7 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 8 Dec 2025 15:49:23 +0000 Subject: [PATCH 07/18] 434: ensure warning is not emitted when package manager does not have a package configured --- src/SelfManage/BuildTools/BinaryBuildToolFinder.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/SelfManage/BuildTools/BinaryBuildToolFinder.php b/src/SelfManage/BuildTools/BinaryBuildToolFinder.php index 2087f76..e90a507 100644 --- a/src/SelfManage/BuildTools/BinaryBuildToolFinder.php +++ b/src/SelfManage/BuildTools/BinaryBuildToolFinder.php @@ -24,6 +24,10 @@ public function check(): bool /** @return non-empty-string|null */ public function packageNameFor(PackageManager $packageManager): string|null { + if (! array_key_exists($packageManager->value, $this->packageManagerPackages)) { + return null; + } + // If we need to customise specific package names depending on OS // specific parameters, this is likely the place to do it return $this->packageManagerPackages[$packageManager->value]; From 772a35e03dbdfbdce0f40e9e961cde56c28f5e8e Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 8 Dec 2025 16:18:58 +0000 Subject: [PATCH 08/18] 434: add phpize install --- src/Command/BuildCommand.php | 2 +- src/Command/InstallCommand.php | 2 +- src/Platform/TargetPhp/PhpBinaryPath.php | 26 +++++++++++++++++++ .../BuildTools/BinaryBuildToolFinder.php | 18 ++++++++++--- .../BuildTools/CheckAllBuildTools.php | 14 +++++++--- 5 files changed, 54 insertions(+), 8 deletions(-) diff --git a/src/Command/BuildCommand.php b/src/Command/BuildCommand.php index 2099d24..c2fe818 100644 --- a/src/Command/BuildCommand.php +++ b/src/Command/BuildCommand.php @@ -67,7 +67,7 @@ public function execute(InputInterface $input, OutputInterface $output): int CommandHelper::applyNoCacheOptionIfSet($input, $this->io); if (CommandHelper::shouldCheckForBuildTools($input)) { - $this->checkBuildTools->check($this->io, CommandHelper::autoInstallBuildTools($input)); + $this->checkBuildTools->check($this->io, $targetPlatform, CommandHelper::autoInstallBuildTools($input)); } $composer = PieComposerFactory::createPieComposer( diff --git a/src/Command/InstallCommand.php b/src/Command/InstallCommand.php index c7f3c20..ed770d4 100644 --- a/src/Command/InstallCommand.php +++ b/src/Command/InstallCommand.php @@ -81,7 +81,7 @@ public function execute(InputInterface $input, OutputInterface $output): int CommandHelper::applyNoCacheOptionIfSet($input, $this->io); if (CommandHelper::shouldCheckForBuildTools($input)) { - $this->checkBuildTools->check($this->io, CommandHelper::autoInstallBuildTools($input)); + $this->checkBuildTools->check($this->io, $targetPlatform, CommandHelper::autoInstallBuildTools($input)); } $composer = PieComposerFactory::createPieComposer( diff --git a/src/Platform/TargetPhp/PhpBinaryPath.php b/src/Platform/TargetPhp/PhpBinaryPath.php index 9f4a557..120f932 100644 --- a/src/Platform/TargetPhp/PhpBinaryPath.php +++ b/src/Platform/TargetPhp/PhpBinaryPath.php @@ -298,6 +298,32 @@ public function majorMinorVersion(): string return $phpVersion; } + /** @return non-empty-string */ + public function majorVersion(): int + { + $phpVersion = self::cleanWarningAndDeprecationsFromOutput(Process::run([ + $this->phpBinaryPath, + '-r', + 'echo PHP_MAJOR_VERSION;', + ])); + Assert::stringNotEmpty($phpVersion, 'Could not determine PHP version'); + + return (int) $phpVersion; + } + + /** @return non-empty-string */ + public function minorVersion(): int + { + $phpVersion = self::cleanWarningAndDeprecationsFromOutput(Process::run([ + $this->phpBinaryPath, + '-r', + 'echo PHP_MINOR_VERSION;', + ])); + Assert::stringNotEmpty($phpVersion, 'Could not determine PHP version'); + + return (int) $phpVersion; + } + public function machineType(): Architecture { $phpMachineType = self::cleanWarningAndDeprecationsFromOutput(Process::run([ diff --git a/src/SelfManage/BuildTools/BinaryBuildToolFinder.php b/src/SelfManage/BuildTools/BinaryBuildToolFinder.php index e90a507..4c23663 100644 --- a/src/SelfManage/BuildTools/BinaryBuildToolFinder.php +++ b/src/SelfManage/BuildTools/BinaryBuildToolFinder.php @@ -4,8 +4,12 @@ namespace Php\Pie\SelfManage\BuildTools; +use Php\Pie\Platform\TargetPlatform; use Symfony\Component\Process\ExecutableFinder; +use function array_key_exists; +use function str_replace; + /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ class BinaryBuildToolFinder { @@ -22,14 +26,22 @@ public function check(): bool } /** @return non-empty-string|null */ - public function packageNameFor(PackageManager $packageManager): string|null + public function packageNameFor(PackageManager $packageManager, TargetPlatform $targetPlatform): string|null { - if (! array_key_exists($packageManager->value, $this->packageManagerPackages)) { + if (! array_key_exists($packageManager->value, $this->packageManagerPackages) || $this->packageManagerPackages[$packageManager->value] === null) { return null; } // If we need to customise specific package names depending on OS // specific parameters, this is likely the place to do it - return $this->packageManagerPackages[$packageManager->value]; + return str_replace( + '{major}', + (string) $targetPlatform->phpBinaryPath->majorVersion(), + str_replace( + '{minor}', + (string) $targetPlatform->phpBinaryPath->minorVersion(), + $this->packageManagerPackages[$packageManager->value], + ), + ); } } diff --git a/src/SelfManage/BuildTools/CheckAllBuildTools.php b/src/SelfManage/BuildTools/CheckAllBuildTools.php index 2f66484..3268525 100644 --- a/src/SelfManage/BuildTools/CheckAllBuildTools.php +++ b/src/SelfManage/BuildTools/CheckAllBuildTools.php @@ -6,6 +6,7 @@ use Composer\IO\IOInterface; +use Php\Pie\Platform\TargetPlatform; use function array_unique; use function array_values; use function count; @@ -66,6 +67,13 @@ public static function buildToolsFactory(): self PackageManager::Apk->value => 'libtool', ], ), + new BinaryBuildToolFinder( + 'phpize', + [ + PackageManager::Apt->value => 'php-dev', + PackageManager::Apk->value => 'php{major}{minor}-dev', + ], + ), ]); } @@ -75,7 +83,7 @@ public function __construct( ) { } - public function check(IOInterface $io, bool $autoInstallIfMissing): void + public function check(IOInterface $io, TargetPlatform $targetPlatform, bool $autoInstallIfMissing): void { $io->write('Checking if all build tools are installed.', verbosity: IOInterface::VERBOSE); /** @var list $packagesToInstall */ @@ -97,7 +105,7 @@ public function check(IOInterface $io, bool $autoInstallIfMissing): void continue; } - $packageName = $buildTool->packageNameFor($packageManager); + $packageName = $buildTool->packageNameFor($packageManager, $targetPlatform); if ($packageName === null) { $io->writeError('Could not find package name for build tool ' . $buildTool->tool . '.', verbosity: IOInterface::VERBOSE); @@ -137,7 +145,7 @@ public function check(IOInterface $io, bool $autoInstallIfMissing): void return; } - $io->write('The following command will be run: ' . $proposedInstallCommand, verbosity: IOInterface::VERY_VERBOSE); + $io->write('The following command will be run: ' . $proposedInstallCommand, verbosity: IOInterface::VERBOSE); if ($io->isInteractive() && ! $autoInstallIfMissing) { if (! $io->askConfirmation('Would you like to install them now?', false)) { From b08066a7596a03d4d869d08b97412cc7ef8b4a02 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 8 Dec 2025 17:19:13 +0000 Subject: [PATCH 09/18] 434: add test for CheckAllBuildTools --- src/Command/BuildCommand.php | 8 +- src/Command/InstallCommand.php | 8 +- .../BuildTools/CheckAllBuildTools.php | 3 +- src/SelfManage/BuildTools/PackageManager.php | 13 +- .../BuildTools/CheckAllBuildToolsTest.php | 143 ++++++++++++++++++ 5 files changed, 168 insertions(+), 7 deletions(-) create mode 100644 test/unit/SelfManage/BuildTools/CheckAllBuildToolsTest.php diff --git a/src/Command/BuildCommand.php b/src/Command/BuildCommand.php index c2fe818..8904188 100644 --- a/src/Command/BuildCommand.php +++ b/src/Command/BuildCommand.php @@ -16,6 +16,7 @@ use Php\Pie\DependencyResolver\UnableToResolveRequirement; use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages; use Php\Pie\SelfManage\BuildTools\CheckAllBuildTools; +use Php\Pie\SelfManage\BuildTools\PackageManager; use Psr\Container\ContainerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -67,7 +68,12 @@ public function execute(InputInterface $input, OutputInterface $output): int CommandHelper::applyNoCacheOptionIfSet($input, $this->io); if (CommandHelper::shouldCheckForBuildTools($input)) { - $this->checkBuildTools->check($this->io, $targetPlatform, CommandHelper::autoInstallBuildTools($input)); + $this->checkBuildTools->check( + $this->io, + PackageManager::detect(), + $targetPlatform, + CommandHelper::autoInstallBuildTools($input), + ); } $composer = PieComposerFactory::createPieComposer( diff --git a/src/Command/InstallCommand.php b/src/Command/InstallCommand.php index ed770d4..33e93d3 100644 --- a/src/Command/InstallCommand.php +++ b/src/Command/InstallCommand.php @@ -17,6 +17,7 @@ use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages; use Php\Pie\Platform\TargetPlatform; use Php\Pie\SelfManage\BuildTools\CheckAllBuildTools; +use Php\Pie\SelfManage\BuildTools\PackageManager; use Psr\Container\ContainerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -81,7 +82,12 @@ public function execute(InputInterface $input, OutputInterface $output): int CommandHelper::applyNoCacheOptionIfSet($input, $this->io); if (CommandHelper::shouldCheckForBuildTools($input)) { - $this->checkBuildTools->check($this->io, $targetPlatform, CommandHelper::autoInstallBuildTools($input)); + $this->checkBuildTools->check( + $this->io, + PackageManager::detect(), + $targetPlatform, + CommandHelper::autoInstallBuildTools($input), + ); } $composer = PieComposerFactory::createPieComposer( diff --git a/src/SelfManage/BuildTools/CheckAllBuildTools.php b/src/SelfManage/BuildTools/CheckAllBuildTools.php index 3268525..07b6a2a 100644 --- a/src/SelfManage/BuildTools/CheckAllBuildTools.php +++ b/src/SelfManage/BuildTools/CheckAllBuildTools.php @@ -83,13 +83,12 @@ public function __construct( ) { } - public function check(IOInterface $io, TargetPlatform $targetPlatform, bool $autoInstallIfMissing): void + public function check(IOInterface $io, PackageManager|null $packageManager, TargetPlatform $targetPlatform, bool $autoInstallIfMissing): void { $io->write('Checking if all build tools are installed.', verbosity: IOInterface::VERBOSE); /** @var list $packagesToInstall */ $packagesToInstall = []; $missingTools = []; - $packageManager = PackageManager::detect(); $allFound = true; foreach ($this->buildTools as $buildTool) { diff --git a/src/SelfManage/BuildTools/PackageManager.php b/src/SelfManage/BuildTools/PackageManager.php index 578c66b..cda35fe 100644 --- a/src/SelfManage/BuildTools/PackageManager.php +++ b/src/SelfManage/BuildTools/PackageManager.php @@ -9,10 +9,12 @@ use Symfony\Component\Process\ExecutableFinder; use function array_unshift; +use function implode; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ enum PackageManager: string { + case Test = 'test'; case Apt = 'apt-get'; case Apk = 'apk'; // @todo dnf @@ -24,6 +26,10 @@ public static function detect(): self|null $executableFinder = new ExecutableFinder(); foreach (self::cases() as $packageManager) { + if ($packageManager === self::Test) { + continue; + } + if ($executableFinder->find($packageManager->value) !== null) { return $packageManager; } @@ -40,6 +46,7 @@ public static function detect(): self|null public function installCommand(array $packages): array { return match ($this) { + self::Test => ['echo', '"fake installing ' . implode(', ', $packages) . '"'], self::Apt => ['apt-get', 'install', '-y', '--no-install-recommends', '--no-install-suggests', ...$packages], self::Apk => ['apk', 'add', '--no-cache', ...$packages], }; @@ -51,9 +58,9 @@ public function install(array $packages): void $cmd = self::installCommand($packages); // @todo ideally only add sudo if it's needed - if (Sudo::exists()) { - array_unshift($cmd, Sudo::find()); - } +// if (Sudo::exists()) { +// array_unshift($cmd, Sudo::find()); +// } Process::run($cmd); } diff --git a/test/unit/SelfManage/BuildTools/CheckAllBuildToolsTest.php b/test/unit/SelfManage/BuildTools/CheckAllBuildToolsTest.php new file mode 100644 index 0000000..b3bbe6d --- /dev/null +++ b/test/unit/SelfManage/BuildTools/CheckAllBuildToolsTest.php @@ -0,0 +1,143 @@ +value => 'coreutils']), + ]); + + $checkAllBuildTools->check( + $io, + PackageManager::Test, + new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + null, + ), + false, + ); + + $outputString = $io->getOutput(); + self::assertStringContainsString('Checking if all build tools are installed.', $outputString); + self::assertStringContainsString('Build tool echo is installed.', $outputString); + self::assertStringContainsString('All build tools found.', $outputString); + } + + public function testCheckInstallsMissingToolWhenPromptedInInteractiveMode(): void + { + $io = new BufferIO(verbosity: OutputInterface::VERBOSITY_VERY_VERBOSE); + $io->setUserInputs(['y']); // answer yes to install build tools + + $checkAllBuildTools = new CheckAllBuildTools([ + new BinaryBuildToolFinder('bloop', [PackageManager::Test->value => 'coreutils']), + ]); + + $checkAllBuildTools->check( + $io, + PackageManager::Test, + new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + null, + ), + false, + ); + + $outputString = $io->getOutput(); + self::assertStringContainsString('Checking if all build tools are installed.', $outputString); + self::assertStringContainsString('The following build tools are missing: bloop', $outputString); + self::assertStringContainsString('The following command will be run: echo "fake installing coreutils"', $outputString); + self::assertStringContainsString('Missing build tools have been installed.', $outputString); + } + + public function testCheckDoesNotInstallToolsWhenInNonInteractiveModeAndFlagNotProvided(): void + { + $io = new BufferIO(verbosity: OutputInterface::VERBOSITY_VERY_VERBOSE); + + $checkAllBuildTools = new CheckAllBuildTools([ + new BinaryBuildToolFinder('bloop', [PackageManager::Test->value => 'coreutils']), + ]); + + $checkAllBuildTools->check( + $io, + PackageManager::Test, + new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + null, + ), + false, + ); + + $outputString = $io->getOutput(); + self::assertStringContainsString('Checking if all build tools are installed.', $outputString); + self::assertStringContainsString('The following build tools are missing: bloop', $outputString); + self::assertStringContainsString('You are not running in interactive mode, and you did not provide the --auto-install-build-tools flag.', $outputString); + self::assertStringContainsString('You may need to run: echo "fake installing coreutils"', $outputString); + } + + public function testCheckInstallsMissingToolInNonInteractiveModeAndFlagIsProvided(): void + { + $io = new BufferIO(verbosity: OutputInterface::VERBOSITY_VERY_VERBOSE); + + $checkAllBuildTools = new CheckAllBuildTools([ + new BinaryBuildToolFinder('bloop', [PackageManager::Test->value => 'coreutils']), + ]); + + $checkAllBuildTools->check( + $io, + PackageManager::Test, + new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + null, + ), + true, + ); + + $outputString = $io->getOutput(); + self::assertStringContainsString('Checking if all build tools are installed.', $outputString); + self::assertStringContainsString('The following build tools are missing: bloop', $outputString); + self::assertStringContainsString('The following command will be run: echo "fake installing coreutils"', $outputString); + self::assertStringContainsString('Missing build tools have been installed.', $outputString); + } +} From e369a29cd7c4fdda2c794c32ee84b33fdffae1e7 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 9 Dec 2025 16:29:09 +0000 Subject: [PATCH 10/18] 434: add more test cases for build tool checkers --- .../BuildTools/BinaryBuildToolFinderTest.php | 74 +++++++++++++++++++ .../BuildTools/PackageManagerTest.php | 29 ++++++++ 2 files changed, 103 insertions(+) create mode 100644 test/unit/SelfManage/BuildTools/BinaryBuildToolFinderTest.php create mode 100644 test/unit/SelfManage/BuildTools/PackageManagerTest.php diff --git a/test/unit/SelfManage/BuildTools/BinaryBuildToolFinderTest.php b/test/unit/SelfManage/BuildTools/BinaryBuildToolFinderTest.php new file mode 100644 index 0000000..ac7c27d --- /dev/null +++ b/test/unit/SelfManage/BuildTools/BinaryBuildToolFinderTest.php @@ -0,0 +1,74 @@ +check()); + } + + public function testCheckFindsTool(): void + { + self::assertTrue((new BinaryBuildToolFinder('echo', []))->check()); + } + + public function testPackageNameIsNullWhenNoPackageConfiguredForPackageManager(): void + { + self::assertNull( + (new BinaryBuildToolFinder('a', [])) + ->packageNameFor( + PackageManager::Test, + TargetPlatform::fromPhpBinaryPath(PhpBinaryPath::fromCurrentProcess(), null), + ), + ); + } + + public function testPackageNameIsNullWhenPackageConfiguredForPackageManagerIsNull(): void + { + self::assertNull( + (new BinaryBuildToolFinder('a', [PackageManager::Test->value => null])) + ->packageNameFor( + PackageManager::Test, + TargetPlatform::fromPhpBinaryPath(PhpBinaryPath::fromCurrentProcess(), null), + ), + ); + } + + public function testPackageNameIsReturnedWhenPackageConfiguredForPackageManager(): void + { + self::assertSame( + 'the-package', + (new BinaryBuildToolFinder('a', [PackageManager::Test->value => 'the-package'])) + ->packageNameFor( + PackageManager::Test, + TargetPlatform::fromPhpBinaryPath(PhpBinaryPath::fromCurrentProcess(), null), + ), + ); + } + + public function testPackageNameIsReturnedWithFormattingWhenPackageConfiguredForPackageManager(): void + { + $phpBinary = PhpBinaryPath::fromCurrentProcess(); + + self::assertSame( + 'php' . $phpBinary->majorVersion() . $phpBinary->minorVersion() . '-dev', + (new BinaryBuildToolFinder('a', [PackageManager::Test->value => 'php{major}{minor}-dev'])) + ->packageNameFor( + PackageManager::Test, + TargetPlatform::fromPhpBinaryPath($phpBinary, null), + ), + ); + } +} diff --git a/test/unit/SelfManage/BuildTools/PackageManagerTest.php b/test/unit/SelfManage/BuildTools/PackageManagerTest.php new file mode 100644 index 0000000..53a4092 --- /dev/null +++ b/test/unit/SelfManage/BuildTools/PackageManagerTest.php @@ -0,0 +1,29 @@ +installCommand(['a', 'b']), + ); + self::assertSame( + ['apt-get', 'install', '-y', '--no-install-recommends', '--no-install-suggests', 'a', 'b'], + PackageManager::Apt->installCommand(['a', 'b']), + ); + self::assertSame( + ['apk', 'add', '--no-cache', 'a', 'b'], + PackageManager::Apk->installCommand(['a', 'b']), + ); + } +} From e1f3f4dc7ee89bb62c2cad3623325a4518da57f7 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 9 Dec 2025 17:53:35 +0000 Subject: [PATCH 11/18] 434: check if we can install with sudo or gracefully fail on error installing build tools --- src/Platform/TargetPhp/PhpBinaryPath.php | 2 - .../BuildTools/CheckAllBuildTools.php | 16 +++++-- src/SelfManage/BuildTools/PackageManager.php | 47 ++++++++++++++++--- 3 files changed, 53 insertions(+), 12 deletions(-) diff --git a/src/Platform/TargetPhp/PhpBinaryPath.php b/src/Platform/TargetPhp/PhpBinaryPath.php index 120f932..c50a132 100644 --- a/src/Platform/TargetPhp/PhpBinaryPath.php +++ b/src/Platform/TargetPhp/PhpBinaryPath.php @@ -298,7 +298,6 @@ public function majorMinorVersion(): string return $phpVersion; } - /** @return non-empty-string */ public function majorVersion(): int { $phpVersion = self::cleanWarningAndDeprecationsFromOutput(Process::run([ @@ -311,7 +310,6 @@ public function majorVersion(): int return (int) $phpVersion; } - /** @return non-empty-string */ public function minorVersion(): int { $phpVersion = self::cleanWarningAndDeprecationsFromOutput(Process::run([ diff --git a/src/SelfManage/BuildTools/CheckAllBuildTools.php b/src/SelfManage/BuildTools/CheckAllBuildTools.php index 07b6a2a..63c2ae9 100644 --- a/src/SelfManage/BuildTools/CheckAllBuildTools.php +++ b/src/SelfManage/BuildTools/CheckAllBuildTools.php @@ -5,8 +5,9 @@ namespace Php\Pie\SelfManage\BuildTools; use Composer\IO\IOInterface; - use Php\Pie\Platform\TargetPlatform; +use Throwable; + use function array_unique; use function array_values; use function count; @@ -154,7 +155,16 @@ public function check(IOInterface $io, PackageManager|null $packageManager, Targ } } - $packageManager->install(array_values(array_unique($packagesToInstall))); - $io->write('Missing build tools have been installed.'); + try { + $packageManager->install(array_values(array_unique($packagesToInstall))); + + $io->write('Missing build tools have been installed.'); + } catch (Throwable $throwable) { + $io->writeError('Could not install the missing build tools. You may need to install them manually.'); + $io->writeError($throwable->__toString(), verbosity: IOInterface::VERBOSE); + $io->writeError(''); + + return; + } } } diff --git a/src/SelfManage/BuildTools/PackageManager.php b/src/SelfManage/BuildTools/PackageManager.php index cda35fe..a91fec1 100644 --- a/src/SelfManage/BuildTools/PackageManager.php +++ b/src/SelfManage/BuildTools/PackageManager.php @@ -5,18 +5,22 @@ namespace Php\Pie\SelfManage\BuildTools; use Php\Pie\File\Sudo; +use Php\Pie\Platform; use Php\Pie\Util\Process; +use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\ExecutableFinder; use function array_unshift; use function implode; +use function str_contains; +use function strtolower; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ enum PackageManager: string { case Test = 'test'; - case Apt = 'apt-get'; - case Apk = 'apk'; + case Apt = 'apt-get'; + case Apk = 'apk'; // @todo dnf // @todo yum // @todo brew @@ -57,11 +61,40 @@ public function install(array $packages): void { $cmd = self::installCommand($packages); - // @todo ideally only add sudo if it's needed -// if (Sudo::exists()) { -// array_unshift($cmd, Sudo::find()); -// } + try { + Process::run($cmd); - Process::run($cmd); + return; + } catch (ProcessFailedException $e) { + if (Platform::isInteractive() && self::isProbablyPermissionDenied($e)) { + array_unshift($cmd, Sudo::find()); + + Process::run($cmd); + + return; + } + + throw $e; + } + } + + private static function isProbablyPermissionDenied(ProcessFailedException $e): bool + { + $mergedProcessOutput = strtolower($e->getProcess()->getErrorOutput() . $e->getProcess()->getOutput()); + + $needles = [ + 'permission denied', + 'you must be root', + 'operation not permitted', + 'are you root', + ]; + + foreach ($needles as $needle) { + if (str_contains($mergedProcessOutput, $needle)) { + return true; + } + } + + return false; } } From e0fab35c361550642331f5c24999c44a05f79cc9 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 10 Dec 2025 16:45:57 +0000 Subject: [PATCH 12/18] 434: added yum/dnf support for PackageManager --- src/SelfManage/BuildTools/CheckAllBuildTools.php | 16 ++++++++++++++++ src/SelfManage/BuildTools/PackageManager.php | 8 +++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/SelfManage/BuildTools/CheckAllBuildTools.php b/src/SelfManage/BuildTools/CheckAllBuildTools.php index 63c2ae9..9872130 100644 --- a/src/SelfManage/BuildTools/CheckAllBuildTools.php +++ b/src/SelfManage/BuildTools/CheckAllBuildTools.php @@ -24,6 +24,8 @@ public static function buildToolsFactory(): self [ PackageManager::Apt->value => 'gcc', PackageManager::Apk->value => 'build-base', + PackageManager::Dnf->value => 'gcc', + PackageManager::Yum->value => 'gcc', ], ), new BinaryBuildToolFinder( @@ -31,6 +33,8 @@ public static function buildToolsFactory(): self [ PackageManager::Apt->value => 'make', PackageManager::Apk->value => 'build-base', + PackageManager::Dnf->value => 'make', + PackageManager::Yum->value => 'make', ], ), new BinaryBuildToolFinder( @@ -38,6 +42,8 @@ public static function buildToolsFactory(): self [ PackageManager::Apt->value => 'autoconf', PackageManager::Apk->value => 'autoconf', + PackageManager::Dnf->value => 'autoconf', + PackageManager::Yum->value => 'autoconf', ], ), new BinaryBuildToolFinder( @@ -45,6 +51,8 @@ public static function buildToolsFactory(): self [ PackageManager::Apt->value => 'bison', PackageManager::Apk->value => 'bison', + PackageManager::Dnf->value => 'bison', + PackageManager::Yum->value => 'bison', ], ), new BinaryBuildToolFinder( @@ -52,6 +60,8 @@ public static function buildToolsFactory(): self [ PackageManager::Apt->value => 're2c', PackageManager::Apk->value => 're2c', + PackageManager::Dnf->value => 're2c', + PackageManager::Yum->value => 're2c', ], ), new BinaryBuildToolFinder( @@ -59,6 +69,8 @@ public static function buildToolsFactory(): self [ PackageManager::Apt->value => 'pkg-config', PackageManager::Apk->value => 'pkgconfig', + PackageManager::Dnf->value => 'pkgconf-pkg-config', + PackageManager::Yum->value => 'pkgconf-pkg-config', ], ), new BinaryBuildToolFinder( @@ -66,6 +78,8 @@ public static function buildToolsFactory(): self [ PackageManager::Apt->value => 'libtool', PackageManager::Apk->value => 'libtool', + PackageManager::Dnf->value => 'libtool', + PackageManager::Yum->value => 'libtool', ], ), new BinaryBuildToolFinder( @@ -73,6 +87,8 @@ public static function buildToolsFactory(): self [ PackageManager::Apt->value => 'php-dev', PackageManager::Apk->value => 'php{major}{minor}-dev', + PackageManager::Dnf->value => 'php-devel', // @todo /usr/sbin/phpize exists on Fedora, but is like a placeholder - need step 2: run `phpize --version` + PackageManager::Yum->value => 'php-devel', ], ), ]); diff --git a/src/SelfManage/BuildTools/PackageManager.php b/src/SelfManage/BuildTools/PackageManager.php index a91fec1..c0ccc46 100644 --- a/src/SelfManage/BuildTools/PackageManager.php +++ b/src/SelfManage/BuildTools/PackageManager.php @@ -21,9 +21,9 @@ enum PackageManager: string case Test = 'test'; case Apt = 'apt-get'; case Apk = 'apk'; - // @todo dnf - // @todo yum - // @todo brew + case Dnf = 'dnf'; + case Yum = 'yum'; + // @todo enable: case Brew = 'brew'; public static function detect(): self|null { @@ -53,6 +53,8 @@ public function installCommand(array $packages): array self::Test => ['echo', '"fake installing ' . implode(', ', $packages) . '"'], self::Apt => ['apt-get', 'install', '-y', '--no-install-recommends', '--no-install-suggests', ...$packages], self::Apk => ['apk', 'add', '--no-cache', ...$packages], + self::Dnf => ['dnf', 'install', '-y', ...$packages], + self::Yum => ['yum', 'install', '-y', ...$packages], }; } From 81d8f982f0ed87c070e1c1bb0f2da46a31f1026b Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 10 Dec 2025 17:05:14 +0000 Subject: [PATCH 13/18] 434: ensure phpize is thoroughly checked on dnf/yum systems due to placeholder --- src/Platform/TargetPhp/PhpizePath.php | 44 ++++++++++++------- .../BuildTools/CheckAllBuildTools.php | 4 +- .../BuildTools/PhpizeBuildToolFinder.php | 19 ++++++++ 3 files changed, 48 insertions(+), 19 deletions(-) create mode 100644 src/SelfManage/BuildTools/PhpizeBuildToolFinder.php diff --git a/src/Platform/TargetPhp/PhpizePath.php b/src/Platform/TargetPhp/PhpizePath.php index 3d7511b..7478703 100644 --- a/src/Platform/TargetPhp/PhpizePath.php +++ b/src/Platform/TargetPhp/PhpizePath.php @@ -27,6 +27,32 @@ public function __construct(public readonly string $phpizeBinaryPath) { } + public static function looksLikeValidPhpize(string $phpizePathToCheck, string|null $forPhpApiVersion = null): bool + { + $phpizeAttempt = $phpizePathToCheck; // @todo + if ($phpizeAttempt === '') { + return false; + } + + if (! file_exists($phpizeAttempt) || ! is_executable($phpizeAttempt)) { + return false; + } + + $phpizeProcess = new Process([$phpizeAttempt, '--version']); + if ($phpizeProcess->run() !== 0) { + return false; + } + + if ( + ! preg_match('/PHP Api Version:\s*(.*)/', $phpizeProcess->getOutput(), $m) + || $m[1] === '' + ) { + return false; + } + + return $forPhpApiVersion === null || $forPhpApiVersion === $m[1]; + } + public static function guessFrom(PhpBinaryPath $phpBinaryPath): self { $expectedApiVersion = $phpBinaryPath->phpApiVersion(); @@ -45,24 +71,8 @@ public static function guessFrom(PhpBinaryPath $phpBinaryPath): self foreach ($phpizeAttempts as $phpizeAttempt) { assert($phpizeAttempt !== null); assert($phpizeAttempt !== ''); - if (! file_exists($phpizeAttempt) || ! is_executable($phpizeAttempt)) { - continue; - } - - $phpizeProcess = new Process([$phpizeAttempt, '--version']); - if ($phpizeProcess->run() !== 0) { - continue; - } - - if ( - ! preg_match('/PHP Api Version:\s*(.*)/', $phpizeProcess->getOutput(), $m) - || ! array_key_exists(1, $m) - || $m[1] === '' - ) { - continue; - } - if ($expectedApiVersion === $m[1]) { + if (self::looksLikeValidPhpize($phpizeAttempt, $expectedApiVersion)) { return new self($phpizeAttempt); } } diff --git a/src/SelfManage/BuildTools/CheckAllBuildTools.php b/src/SelfManage/BuildTools/CheckAllBuildTools.php index 9872130..e2c779a 100644 --- a/src/SelfManage/BuildTools/CheckAllBuildTools.php +++ b/src/SelfManage/BuildTools/CheckAllBuildTools.php @@ -82,12 +82,12 @@ public static function buildToolsFactory(): self PackageManager::Yum->value => 'libtool', ], ), - new BinaryBuildToolFinder( + new PhpizeBuildToolFinder( 'phpize', [ PackageManager::Apt->value => 'php-dev', PackageManager::Apk->value => 'php{major}{minor}-dev', - PackageManager::Dnf->value => 'php-devel', // @todo /usr/sbin/phpize exists on Fedora, but is like a placeholder - need step 2: run `phpize --version` + PackageManager::Dnf->value => 'php-devel', PackageManager::Yum->value => 'php-devel', ], ), diff --git a/src/SelfManage/BuildTools/PhpizeBuildToolFinder.php b/src/SelfManage/BuildTools/PhpizeBuildToolFinder.php new file mode 100644 index 0000000..e1c5101 --- /dev/null +++ b/src/SelfManage/BuildTools/PhpizeBuildToolFinder.php @@ -0,0 +1,19 @@ +find($this->tool); + + return $foundTool !== null && PhpizePath::looksLikeValidPhpize($foundTool); + } +} From 69b5290f4be80be9ea5618612b101d9fd950448e Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 10 Dec 2025 18:03:36 +0000 Subject: [PATCH 14/18] 434: added a set of Dockerfile-based end-to-end tests to check build tools get installed --- test/end-to-end/Dockerfile | 22 +++++++ test/end-to-end/dockerfile-e2e-test.sh | 82 ++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 test/end-to-end/Dockerfile create mode 100755 test/end-to-end/dockerfile-e2e-test.sh diff --git a/test/end-to-end/Dockerfile b/test/end-to-end/Dockerfile new file mode 100644 index 0000000..f75bcac --- /dev/null +++ b/test/end-to-end/Dockerfile @@ -0,0 +1,22 @@ +FROM boxproject/box:4.6.10 AS build_pie_phar +RUN apk add git +COPY . /app +RUN cd /app && touch creating_this_means_phar_will_never_be_verified && /box.phar compile + +FROM alpine AS test_pie_installs_build_tools_on_alpine +RUN apk add php php-phar php-mbstring php-iconv php-openssl bzip2-dev libbz2 +COPY --from=build_pie_phar /app/pie.phar /usr/local/bin/pie +RUN pie install --auto-install-build-tools -v php/bz2 +RUN pie show + +FROM fedora AS test_pie_installs_build_tools_on_fedora +RUN dnf install -y php php-pecl-zip unzip bzip2-devel +COPY --from=build_pie_phar /app/pie.phar /usr/local/bin/pie +RUN pie install --auto-install-build-tools -v php/bz2 +RUN pie show + +FROM ubuntu AS test_pie_installs_build_tools_on_ubuntu +COPY --from=build_pie_phar /app/pie.phar /usr/local/bin/pie +RUN apt-get update && apt-get install -y php unzip libbz2-dev +RUN pie install --auto-install-build-tools -v php/bz2 +RUN pie show diff --git a/test/end-to-end/dockerfile-e2e-test.sh b/test/end-to-end/dockerfile-e2e-test.sh new file mode 100755 index 0000000..415aef8 --- /dev/null +++ b/test/end-to-end/dockerfile-e2e-test.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash + +set -euo pipefail + +cd "$(dirname "$0")/../../" + +E2E_PATH="$(dirname "$0")" +DOCKERFILE="$E2E_PATH/Dockerfile" + +if [[ ! -f "$DOCKERFILE" ]]; then + echo "Dockerfile not found: $DOCKERFILE" >&2 + exit 1 +fi + +# Make a list of the build targets starting with test_ +mapfile -t TARGETS < <(awk ' + tolower($1) == "from" { + for (i = 1; i <= NF; i++) { + if (tolower($i) == "as" && i < NF) { + t = $(i+1) + if (substr(t,1,5) == "test_") print t + } + } + } +' "$DOCKERFILE") + +if [[ ${#TARGETS[@]} -eq 0 ]]; then + echo "No test_ targets found in $DOCKERFILE" >&2 + exit 1 +fi + +# If a specific target is provided as an argument, run only that target +if [[ $# -gt 0 ]]; then + REQUESTED_TARGET="$1" + # Verify the requested target exists among discovered test_ targets + found=false + for t in "${TARGETS[@]}"; do + if [[ "$t" == "$REQUESTED_TARGET" ]]; then + found=true + break + fi + done + if [[ $found == false ]]; then + echo "Requested target '$REQUESTED_TARGET' not found in $DOCKERFILE" >&2 + echo "Available test_ targets:" >&2 + for t in "${TARGETS[@]}"; do + echo " - $t" >&2 + done + exit 1 + fi + TARGETS=("$REQUESTED_TARGET") +fi + +PASSED=() +FAILED=() + +for TARGET in "${TARGETS[@]}"; do + echo "๐Ÿงช Running $TARGET" + LOGFILE="$E2E_PATH/$TARGET.out" + # Stream to console and to the logfile + if docker buildx build --target="$TARGET" --file "$DOCKERFILE" . |& tee "$LOGFILE"; then + PASSED+=("$TARGET") + echo "โœ… Passed $TARGET" + rm "$LOGFILE" + else + FAILED+=("$TARGET") + echo "โŒ Failed $TARGET" >&2 + fi + echo +done + +echo "================ Summary ================" +echo "Total: ${#TARGETS[@]} | Passed: ${#PASSED[@]} | Failed: ${#FAILED[@]}" +if [[ ${#FAILED[@]} -gt 0 ]]; then + echo "Failed targets:" >&2 + for f in "${FAILED[@]}"; do + echo " - $f" >&2 + done + exit 1 +fi + +echo "All test targets passed." From 82b1ec2a08ad88d8e00ac27db51c8a2c52aa6d54 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 15 Dec 2025 14:43:52 +0000 Subject: [PATCH 15/18] 434: run Dockerfile e2e test in CI --- .github/workflows/continuous-integration.yml | 16 ++++++++++++++++ phpstan-baseline.neon | 6 ------ src/Platform/TargetPhp/PhpizePath.php | 1 - 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 8bdacbb..c01bce3 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -126,6 +126,22 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + end-to-end-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + # Fixes `git describe` picking the wrong tag - see https://github.com/php/pie/issues/307 + - run: git fetch --tags --force + # Ensure some kind of previous tag exists, otherwise box fails + - run: git describe --tags HEAD || git tag 0.0.0 + - uses: ramsey/composer-install@v3 + - name: Run the tests + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: test/end-to-end/dockerfile-e2e-test.sh + behaviour-tests: runs-on: ${{ matrix.operating-system }} strategy: diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 549c691..2cda1c7 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -318,12 +318,6 @@ parameters: count: 4 path: src/Platform/TargetPhp/PhpBinaryPath.php - - - message: '#^Call to function array_key_exists\(\) with 1 and array\{non\-falsy\-string, string\} will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType - count: 1 - path: src/Platform/TargetPhp/PhpizePath.php - - message: '#^Call to function array_key_exists\(\) with 2 and array\{non\-falsy\-string, string, non\-falsy\-string\} will always evaluate to true\.$#' identifier: function.alreadyNarrowedType diff --git a/src/Platform/TargetPhp/PhpizePath.php b/src/Platform/TargetPhp/PhpizePath.php index 7478703..8112d70 100644 --- a/src/Platform/TargetPhp/PhpizePath.php +++ b/src/Platform/TargetPhp/PhpizePath.php @@ -7,7 +7,6 @@ use RuntimeException; use Symfony\Component\Process\Process; -use function array_key_exists; use function assert; use function file_exists; use function is_executable; From d6a2892a3b551afd8f00dbf07937b3552207ad72 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 15 Dec 2025 16:21:29 +0000 Subject: [PATCH 16/18] 434: add brew support for build tool checker --- src/SelfManage/BuildTools/CheckAllBuildTools.php | 8 ++++++++ src/SelfManage/BuildTools/PackageManager.php | 3 ++- test/end-to-end/Dockerfile | 13 ++++++++++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/SelfManage/BuildTools/CheckAllBuildTools.php b/src/SelfManage/BuildTools/CheckAllBuildTools.php index e2c779a..4e4b0e8 100644 --- a/src/SelfManage/BuildTools/CheckAllBuildTools.php +++ b/src/SelfManage/BuildTools/CheckAllBuildTools.php @@ -26,6 +26,7 @@ public static function buildToolsFactory(): self PackageManager::Apk->value => 'build-base', PackageManager::Dnf->value => 'gcc', PackageManager::Yum->value => 'gcc', + PackageManager::Brew->value => 'gcc', ], ), new BinaryBuildToolFinder( @@ -35,6 +36,7 @@ public static function buildToolsFactory(): self PackageManager::Apk->value => 'build-base', PackageManager::Dnf->value => 'make', PackageManager::Yum->value => 'make', + PackageManager::Brew->value => 'make', ], ), new BinaryBuildToolFinder( @@ -44,6 +46,7 @@ public static function buildToolsFactory(): self PackageManager::Apk->value => 'autoconf', PackageManager::Dnf->value => 'autoconf', PackageManager::Yum->value => 'autoconf', + PackageManager::Brew->value => 'autoconf', ], ), new BinaryBuildToolFinder( @@ -53,6 +56,7 @@ public static function buildToolsFactory(): self PackageManager::Apk->value => 'bison', PackageManager::Dnf->value => 'bison', PackageManager::Yum->value => 'bison', + PackageManager::Brew->value => 'bison', ], ), new BinaryBuildToolFinder( @@ -62,6 +66,7 @@ public static function buildToolsFactory(): self PackageManager::Apk->value => 're2c', PackageManager::Dnf->value => 're2c', PackageManager::Yum->value => 're2c', + PackageManager::Brew->value => 're2c', ], ), new BinaryBuildToolFinder( @@ -71,6 +76,7 @@ public static function buildToolsFactory(): self PackageManager::Apk->value => 'pkgconfig', PackageManager::Dnf->value => 'pkgconf-pkg-config', PackageManager::Yum->value => 'pkgconf-pkg-config', + PackageManager::Brew->value => 'pkgconf', ], ), new BinaryBuildToolFinder( @@ -80,6 +86,7 @@ public static function buildToolsFactory(): self PackageManager::Apk->value => 'libtool', PackageManager::Dnf->value => 'libtool', PackageManager::Yum->value => 'libtool', + PackageManager::Brew->value => 'libtool', ], ), new PhpizeBuildToolFinder( @@ -89,6 +96,7 @@ public static function buildToolsFactory(): self PackageManager::Apk->value => 'php{major}{minor}-dev', PackageManager::Dnf->value => 'php-devel', PackageManager::Yum->value => 'php-devel', + PackageManager::Brew->value => 'php', ], ), ]); diff --git a/src/SelfManage/BuildTools/PackageManager.php b/src/SelfManage/BuildTools/PackageManager.php index c0ccc46..dd7484e 100644 --- a/src/SelfManage/BuildTools/PackageManager.php +++ b/src/SelfManage/BuildTools/PackageManager.php @@ -23,7 +23,7 @@ enum PackageManager: string case Apk = 'apk'; case Dnf = 'dnf'; case Yum = 'yum'; - // @todo enable: case Brew = 'brew'; + case Brew = 'brew'; public static function detect(): self|null { @@ -55,6 +55,7 @@ public function installCommand(array $packages): array self::Apk => ['apk', 'add', '--no-cache', ...$packages], self::Dnf => ['dnf', 'install', '-y', ...$packages], self::Yum => ['yum', 'install', '-y', ...$packages], + self::Brew => ['brew', 'install', ...$packages], }; } diff --git a/test/end-to-end/Dockerfile b/test/end-to-end/Dockerfile index f75bcac..95d17c0 100644 --- a/test/end-to-end/Dockerfile +++ b/test/end-to-end/Dockerfile @@ -16,7 +16,18 @@ RUN pie install --auto-install-build-tools -v php/bz2 RUN pie show FROM ubuntu AS test_pie_installs_build_tools_on_ubuntu -COPY --from=build_pie_phar /app/pie.phar /usr/local/bin/pie RUN apt-get update && apt-get install -y php unzip libbz2-dev +COPY --from=build_pie_phar /app/pie.phar /usr/local/bin/pie +RUN pie install --auto-install-build-tools -v php/bz2 +RUN pie show + +FROM homebrew/brew AS test_pie_installs_build_tools_with_brew +RUN brew install php +COPY --from=build_pie_phar /app/pie.phar /usr/local/bin/pie +USER root +RUN apt-get update && apt-get install -y unzip libbz2-dev +RUN apt-get remove --allow-remove-essential -y apt +USER linuxbrew +RUN brew install bzip2 RUN pie install --auto-install-build-tools -v php/bz2 RUN pie show From 1a15aa00e97370c874f12ddd65e014ecff5bf1d2 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 17 Dec 2025 15:48:15 +0000 Subject: [PATCH 17/18] 434: don't check for build tools on Windows at all --- src/Command/CommandHelper.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index e79a871..42dda89 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -251,6 +251,10 @@ public static function autoInstallBuildTools(InputInterface $input): bool public static function shouldCheckForBuildTools(InputInterface $input): bool { + if (Platform::isWindows()) { + return false; + } + return ! $input->hasOption(self::OPTION_SUPPRESS_BUILD_TOOLS_CHECK) || ! $input->getOption(self::OPTION_SUPPRESS_BUILD_TOOLS_CHECK); } From 60236eb58aca1965999a3294b963c0aa7765199c Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 17 Dec 2025 16:13:45 +0000 Subject: [PATCH 18/18] 434: define a virtual package for Alpine --- src/SelfManage/BuildTools/PackageManager.php | 2 +- test/end-to-end/Dockerfile | 1 + .../SelfManage/BuildTools/PackageManagerTest.php | 14 +++++++++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/SelfManage/BuildTools/PackageManager.php b/src/SelfManage/BuildTools/PackageManager.php index dd7484e..da9cac1 100644 --- a/src/SelfManage/BuildTools/PackageManager.php +++ b/src/SelfManage/BuildTools/PackageManager.php @@ -52,7 +52,7 @@ public function installCommand(array $packages): array return match ($this) { self::Test => ['echo', '"fake installing ' . implode(', ', $packages) . '"'], self::Apt => ['apt-get', 'install', '-y', '--no-install-recommends', '--no-install-suggests', ...$packages], - self::Apk => ['apk', 'add', '--no-cache', ...$packages], + self::Apk => ['apk', 'add', '--no-cache', '--virtual', '.php-pie-deps', ...$packages], self::Dnf => ['dnf', 'install', '-y', ...$packages], self::Yum => ['yum', 'install', '-y', ...$packages], self::Brew => ['brew', 'install', ...$packages], diff --git a/test/end-to-end/Dockerfile b/test/end-to-end/Dockerfile index 95d17c0..627c53b 100644 --- a/test/end-to-end/Dockerfile +++ b/test/end-to-end/Dockerfile @@ -7,6 +7,7 @@ FROM alpine AS test_pie_installs_build_tools_on_alpine RUN apk add php php-phar php-mbstring php-iconv php-openssl bzip2-dev libbz2 COPY --from=build_pie_phar /app/pie.phar /usr/local/bin/pie RUN pie install --auto-install-build-tools -v php/bz2 +RUN apk del .php-pie-deps RUN pie show FROM fedora AS test_pie_installs_build_tools_on_fedora diff --git a/test/unit/SelfManage/BuildTools/PackageManagerTest.php b/test/unit/SelfManage/BuildTools/PackageManagerTest.php index 53a4092..f66d490 100644 --- a/test/unit/SelfManage/BuildTools/PackageManagerTest.php +++ b/test/unit/SelfManage/BuildTools/PackageManagerTest.php @@ -22,8 +22,20 @@ public function testInstallCommand(): void PackageManager::Apt->installCommand(['a', 'b']), ); self::assertSame( - ['apk', 'add', '--no-cache', 'a', 'b'], + ['apk', 'add', '--no-cache', '--virtual', '.php-pie-deps', 'a', 'b'], PackageManager::Apk->installCommand(['a', 'b']), ); + self::assertSame( + ['dnf', 'install', '-y', 'a', 'b'], + PackageManager::Dnf->installCommand(['a', 'b']), + ); + self::assertSame( + ['yum', 'install', '-y', 'a', 'b'], + PackageManager::Yum->installCommand(['a', 'b']), + ); + self::assertSame( + ['brew', 'install', 'a', 'b'], + PackageManager::Brew->installCommand(['a', 'b']), + ); } }