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/Command/BuildCommand.php b/src/Command/BuildCommand.php index 3b941db..8904188 100644 --- a/src/Command/BuildCommand.php +++ b/src/Command/BuildCommand.php @@ -15,6 +15,8 @@ use Php\Pie\DependencyResolver\InvalidPackageName; 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; @@ -35,6 +37,7 @@ public function __construct( private readonly ComposerIntegrationHandler $composerIntegrationHandler, private readonly FindMatchingPackages $findMatchingPackages, private readonly IOInterface $io, + private readonly CheckAllBuildTools $checkBuildTools, ) { parent::__construct(); } @@ -64,6 +67,15 @@ public function execute(InputInterface $input, OutputInterface $output): int $forceInstallPackageVersion = CommandHelper::determineForceInstallingPackageVersion($input); CommandHelper::applyNoCacheOptionIfSet($input, $this->io); + if (CommandHelper::shouldCheckForBuildTools($input)) { + $this->checkBuildTools->check( + $this->io, + PackageManager::detect(), + $targetPlatform, + CommandHelper::autoInstallBuildTools($input), + ); + } + $composer = PieComposerFactory::createPieComposer( $this->container, new PieComposerRequest( diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index c43ff07..42dda89 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,22 @@ 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 + { + if (Platform::isWindows()) { + return false; + } + + 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 d4b66bd..33e93d3 100644 --- a/src/Command/InstallCommand.php +++ b/src/Command/InstallCommand.php @@ -16,6 +16,8 @@ use Php\Pie\DependencyResolver\UnableToResolveRequirement; 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; @@ -37,6 +39,7 @@ public function __construct( private readonly InvokeSubCommand $invokeSubCommand, private readonly FindMatchingPackages $findMatchingPackages, private readonly IOInterface $io, + private readonly CheckAllBuildTools $checkBuildTools, ) { parent::__construct(); } @@ -78,6 +81,15 @@ public function execute(InputInterface $input, OutputInterface $output): int $forceInstallPackageVersion = CommandHelper::determineForceInstallingPackageVersion($input); CommandHelper::applyNoCacheOptionIfSet($input, $this->io); + if (CommandHelper::shouldCheckForBuildTools($input)) { + $this->checkBuildTools->check( + $this->io, + PackageManager::detect(), + $targetPlatform, + CommandHelper::autoInstallBuildTools($input), + ); + } + $composer = PieComposerFactory::createPieComposer( $this->container, new PieComposerRequest( 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/Platform/TargetPhp/PhpBinaryPath.php b/src/Platform/TargetPhp/PhpBinaryPath.php index 9f4a557..c50a132 100644 --- a/src/Platform/TargetPhp/PhpBinaryPath.php +++ b/src/Platform/TargetPhp/PhpBinaryPath.php @@ -298,6 +298,30 @@ public function majorMinorVersion(): string return $phpVersion; } + 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; + } + + 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/Platform/TargetPhp/PhpizePath.php b/src/Platform/TargetPhp/PhpizePath.php index 3d7511b..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; @@ -27,6 +26,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 +70,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/BinaryBuildToolFinder.php b/src/SelfManage/BuildTools/BinaryBuildToolFinder.php new file mode 100644 index 0000000..4c23663 --- /dev/null +++ b/src/SelfManage/BuildTools/BinaryBuildToolFinder.php @@ -0,0 +1,47 @@ + $packageManagerPackages */ + public function __construct( + public readonly string $tool, + private readonly array $packageManagerPackages, + ) { + } + + public function check(): bool + { + return (new ExecutableFinder())->find($this->tool) !== null; + } + + /** @return non-empty-string|null */ + public function packageNameFor(PackageManager $packageManager, TargetPlatform $targetPlatform): string|null + { + 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 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 new file mode 100644 index 0000000..4e4b0e8 --- /dev/null +++ b/src/SelfManage/BuildTools/CheckAllBuildTools.php @@ -0,0 +1,194 @@ +value => 'gcc', + PackageManager::Apk->value => 'build-base', + PackageManager::Dnf->value => 'gcc', + PackageManager::Yum->value => 'gcc', + PackageManager::Brew->value => 'gcc', + ], + ), + new BinaryBuildToolFinder( + 'make', + [ + PackageManager::Apt->value => 'make', + PackageManager::Apk->value => 'build-base', + PackageManager::Dnf->value => 'make', + PackageManager::Yum->value => 'make', + PackageManager::Brew->value => 'make', + ], + ), + new BinaryBuildToolFinder( + 'autoconf', + [ + PackageManager::Apt->value => 'autoconf', + PackageManager::Apk->value => 'autoconf', + PackageManager::Dnf->value => 'autoconf', + PackageManager::Yum->value => 'autoconf', + PackageManager::Brew->value => 'autoconf', + ], + ), + new BinaryBuildToolFinder( + 'bison', + [ + PackageManager::Apt->value => 'bison', + PackageManager::Apk->value => 'bison', + PackageManager::Dnf->value => 'bison', + PackageManager::Yum->value => 'bison', + PackageManager::Brew->value => 'bison', + ], + ), + new BinaryBuildToolFinder( + 're2c', + [ + PackageManager::Apt->value => 're2c', + PackageManager::Apk->value => 're2c', + PackageManager::Dnf->value => 're2c', + PackageManager::Yum->value => 're2c', + PackageManager::Brew->value => 're2c', + ], + ), + new BinaryBuildToolFinder( + 'pkg-config', + [ + PackageManager::Apt->value => 'pkg-config', + PackageManager::Apk->value => 'pkgconfig', + PackageManager::Dnf->value => 'pkgconf-pkg-config', + PackageManager::Yum->value => 'pkgconf-pkg-config', + PackageManager::Brew->value => 'pkgconf', + ], + ), + new BinaryBuildToolFinder( + 'libtoolize', + [ + PackageManager::Apt->value => 'libtool', + PackageManager::Apk->value => 'libtool', + PackageManager::Dnf->value => 'libtool', + PackageManager::Yum->value => 'libtool', + PackageManager::Brew->value => 'libtool', + ], + ), + new PhpizeBuildToolFinder( + 'phpize', + [ + PackageManager::Apt->value => 'php-dev', + PackageManager::Apk->value => 'php{major}{minor}-dev', + PackageManager::Dnf->value => 'php-devel', + PackageManager::Yum->value => 'php-devel', + PackageManager::Brew->value => 'php', + ], + ), + ]); + } + + /** @param list $buildTools */ + public function __construct( + private readonly array $buildTools, + ) { + } + + 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 = []; + $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; + + if ($packageManager === null) { + continue; + } + + $packageName = $buildTool->packageNameFor($packageManager, $targetPlatform); + + if ($packageName === null) { + $io->writeError('Could not find package name for build tool ' . $buildTool->tool . '.', verbosity: IOInterface::VERBOSE); + continue; + } + + $packagesToInstall[] = $packageName; + } + + if ($allFound) { + $io->write('All build tools found.', verbosity: IOInterface::VERBOSE); + + return; + } + + $io->write('The following build tools are missing: ' . implode(', ', $missingTools) . ''); + + if ($packageManager === null) { + $io->write('Could not find a package manager to install the missing build tools.'); + + return; + } + + if (! count($packagesToInstall)) { + $io->write('Could not determine packages to install.'); + + return; + } + + $proposedInstallCommand = implode(' ', $packageManager->installCommand(array_values(array_unique($packagesToInstall)))); + + 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::VERBOSE); + + 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.'); + + return; + } + } + + 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 new file mode 100644 index 0000000..da9cac1 --- /dev/null +++ b/src/SelfManage/BuildTools/PackageManager.php @@ -0,0 +1,103 @@ +find($packageManager->value) !== null) { + return $packageManager; + } + } + + return null; + } + + /** + * @param list $packages + * + * @return list + */ + 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', '--virtual', '.php-pie-deps', ...$packages], + self::Dnf => ['dnf', 'install', '-y', ...$packages], + self::Yum => ['yum', 'install', '-y', ...$packages], + self::Brew => ['brew', 'install', ...$packages], + }; + } + + /** @param list $packages */ + public function install(array $packages): void + { + $cmd = self::installCommand($packages); + + try { + 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; + } +} 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); + } +} diff --git a/test/end-to-end/Dockerfile b/test/end-to-end/Dockerfile new file mode 100644 index 0000000..627c53b --- /dev/null +++ b/test/end-to-end/Dockerfile @@ -0,0 +1,34 @@ +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 apk del .php-pie-deps +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 +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 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." 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/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); + } +} diff --git a/test/unit/SelfManage/BuildTools/PackageManagerTest.php b/test/unit/SelfManage/BuildTools/PackageManagerTest.php new file mode 100644 index 0000000..f66d490 --- /dev/null +++ b/test/unit/SelfManage/BuildTools/PackageManagerTest.php @@ -0,0 +1,41 @@ +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', '--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']), + ); + } +}