Skip to content

Commit 6da2c7e

Browse files
authored
Merge pull request #452 from asgrim/434-prompt-to-install-build-tools
Prompt to install build tools if they're missing
2 parents f46c587 + 60236eb commit 6da2c7e

17 files changed

+867
-24
lines changed

.github/workflows/continuous-integration.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,22 @@ jobs:
126126
env:
127127
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
128128

129+
end-to-end-tests:
130+
runs-on: ubuntu-latest
131+
steps:
132+
- uses: actions/checkout@v6
133+
with:
134+
fetch-depth: 0
135+
# Fixes `git describe` picking the wrong tag - see https://github.com/php/pie/issues/307
136+
- run: git fetch --tags --force
137+
# Ensure some kind of previous tag exists, otherwise box fails
138+
- run: git describe --tags HEAD || git tag 0.0.0
139+
- uses: ramsey/composer-install@v3
140+
- name: Run the tests
141+
env:
142+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
143+
run: test/end-to-end/dockerfile-e2e-test.sh
144+
129145
behaviour-tests:
130146
runs-on: ${{ matrix.operating-system }}
131147
strategy:

phpstan-baseline.neon

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -318,12 +318,6 @@ parameters:
318318
count: 4
319319
path: src/Platform/TargetPhp/PhpBinaryPath.php
320320

321-
-
322-
message: '#^Call to function array_key_exists\(\) with 1 and array\{non\-falsy\-string, string\} will always evaluate to true\.$#'
323-
identifier: function.alreadyNarrowedType
324-
count: 1
325-
path: src/Platform/TargetPhp/PhpizePath.php
326-
327321
-
328322
message: '#^Call to function array_key_exists\(\) with 2 and array\{non\-falsy\-string, string, non\-falsy\-string\} will always evaluate to true\.$#'
329323
identifier: function.alreadyNarrowedType

src/Command/BuildCommand.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
use Php\Pie\DependencyResolver\InvalidPackageName;
1616
use Php\Pie\DependencyResolver\UnableToResolveRequirement;
1717
use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages;
18+
use Php\Pie\SelfManage\BuildTools\CheckAllBuildTools;
19+
use Php\Pie\SelfManage\BuildTools\PackageManager;
1820
use Psr\Container\ContainerInterface;
1921
use Symfony\Component\Console\Attribute\AsCommand;
2022
use Symfony\Component\Console\Command\Command;
@@ -35,6 +37,7 @@ public function __construct(
3537
private readonly ComposerIntegrationHandler $composerIntegrationHandler,
3638
private readonly FindMatchingPackages $findMatchingPackages,
3739
private readonly IOInterface $io,
40+
private readonly CheckAllBuildTools $checkBuildTools,
3841
) {
3942
parent::__construct();
4043
}
@@ -64,6 +67,15 @@ public function execute(InputInterface $input, OutputInterface $output): int
6467
$forceInstallPackageVersion = CommandHelper::determineForceInstallingPackageVersion($input);
6568
CommandHelper::applyNoCacheOptionIfSet($input, $this->io);
6669

70+
if (CommandHelper::shouldCheckForBuildTools($input)) {
71+
$this->checkBuildTools->check(
72+
$this->io,
73+
PackageManager::detect(),
74+
$targetPlatform,
75+
CommandHelper::autoInstallBuildTools($input),
76+
);
77+
}
78+
6779
$composer = PieComposerFactory::createPieComposer(
6880
$this->container,
6981
new PieComposerRequest(

src/Command/CommandHelper.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ final class CommandHelper
6161
private const OPTION_SKIP_ENABLE_EXTENSION = 'skip-enable-extension';
6262
private const OPTION_FORCE = 'force';
6363
private const OPTION_NO_CACHE = 'no-cache';
64+
private const OPTION_AUTO_INSTALL_BUILD_TOOLS = 'auto-install-build-tools';
65+
private const OPTION_SUPPRESS_BUILD_TOOLS_CHECK = 'no-build-tools-check';
6466

6567
private function __construct()
6668
{
@@ -139,6 +141,19 @@ public static function configureDownloadBuildInstallOptions(Command $command, bo
139141
'When installing a PHP project, allow non-interactive project installations. Only used in certain contexts.',
140142
);
141143

144+
$command->addOption(
145+
self::OPTION_AUTO_INSTALL_BUILD_TOOLS,
146+
null,
147+
InputOption::VALUE_NONE,
148+
'If build tools are missing, automatically install them, instead of prompting.',
149+
);
150+
$command->addOption(
151+
self::OPTION_SUPPRESS_BUILD_TOOLS_CHECK,
152+
null,
153+
InputOption::VALUE_NONE,
154+
'Do not perform the check to see if build tools are present on the system.',
155+
);
156+
142157
/**
143158
* Allows additional options for the `./configure` command to be passed here.
144159
* Note, this means you probably need to call {@see self::validateInput()} to validate the input manually...
@@ -233,6 +248,22 @@ public static function determineForceInstallingPackageVersion(InputInterface $in
233248
return $input->hasOption(self::OPTION_FORCE) && $input->getOption(self::OPTION_FORCE);
234249
}
235250

251+
public static function autoInstallBuildTools(InputInterface $input): bool
252+
{
253+
return $input->hasOption(self::OPTION_AUTO_INSTALL_BUILD_TOOLS)
254+
&& $input->getOption(self::OPTION_AUTO_INSTALL_BUILD_TOOLS);
255+
}
256+
257+
public static function shouldCheckForBuildTools(InputInterface $input): bool
258+
{
259+
if (Platform::isWindows()) {
260+
return false;
261+
}
262+
263+
return ! $input->hasOption(self::OPTION_SUPPRESS_BUILD_TOOLS_CHECK)
264+
|| ! $input->getOption(self::OPTION_SUPPRESS_BUILD_TOOLS_CHECK);
265+
}
266+
236267
public static function determinePhpizePathFromInputs(InputInterface $input): PhpizePath|null
237268
{
238269
if ($input->hasOption(self::OPTION_WITH_PHPIZE_PATH)) {

src/Command/InstallCommand.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
use Php\Pie\DependencyResolver\UnableToResolveRequirement;
1717
use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages;
1818
use Php\Pie\Platform\TargetPlatform;
19+
use Php\Pie\SelfManage\BuildTools\CheckAllBuildTools;
20+
use Php\Pie\SelfManage\BuildTools\PackageManager;
1921
use Psr\Container\ContainerInterface;
2022
use Symfony\Component\Console\Attribute\AsCommand;
2123
use Symfony\Component\Console\Command\Command;
@@ -37,6 +39,7 @@ public function __construct(
3739
private readonly InvokeSubCommand $invokeSubCommand,
3840
private readonly FindMatchingPackages $findMatchingPackages,
3941
private readonly IOInterface $io,
42+
private readonly CheckAllBuildTools $checkBuildTools,
4043
) {
4144
parent::__construct();
4245
}
@@ -78,6 +81,15 @@ public function execute(InputInterface $input, OutputInterface $output): int
7881
$forceInstallPackageVersion = CommandHelper::determineForceInstallingPackageVersion($input);
7982
CommandHelper::applyNoCacheOptionIfSet($input, $this->io);
8083

84+
if (CommandHelper::shouldCheckForBuildTools($input)) {
85+
$this->checkBuildTools->check(
86+
$this->io,
87+
PackageManager::detect(),
88+
$targetPlatform,
89+
CommandHelper::autoInstallBuildTools($input),
90+
);
91+
}
92+
8193
$composer = PieComposerFactory::createPieComposer(
8294
$this->container,
8395
new PieComposerRequest(

src/Container.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
use Php\Pie\Installing\UninstallUsingUnlink;
4040
use Php\Pie\Installing\UnixInstall;
4141
use Php\Pie\Installing\WindowsInstall;
42+
use Php\Pie\SelfManage\BuildTools\CheckAllBuildTools;
4243
use Psr\Container\ContainerInterface;
4344
use Symfony\Component\Console\ConsoleEvents;
4445
use Symfony\Component\Console\Event\ConsoleCommandEvent;
@@ -197,6 +198,13 @@ static function (ContainerInterface $container): Install {
197198
},
198199
);
199200

201+
$container->singleton(
202+
CheckAllBuildTools::class,
203+
static function (): CheckAllBuildTools {
204+
return CheckAllBuildTools::buildToolsFactory();
205+
},
206+
);
207+
200208
$container->alias(UninstallUsingUnlink::class, Uninstall::class);
201209

202210
$container->alias(Ini\RemoveIniEntryWithFileGetContents::class, Ini\RemoveIniEntry::class);

src/Platform/TargetPhp/PhpBinaryPath.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,30 @@ public function majorMinorVersion(): string
298298
return $phpVersion;
299299
}
300300

301+
public function majorVersion(): int
302+
{
303+
$phpVersion = self::cleanWarningAndDeprecationsFromOutput(Process::run([
304+
$this->phpBinaryPath,
305+
'-r',
306+
'echo PHP_MAJOR_VERSION;',
307+
]));
308+
Assert::stringNotEmpty($phpVersion, 'Could not determine PHP version');
309+
310+
return (int) $phpVersion;
311+
}
312+
313+
public function minorVersion(): int
314+
{
315+
$phpVersion = self::cleanWarningAndDeprecationsFromOutput(Process::run([
316+
$this->phpBinaryPath,
317+
'-r',
318+
'echo PHP_MINOR_VERSION;',
319+
]));
320+
Assert::stringNotEmpty($phpVersion, 'Could not determine PHP version');
321+
322+
return (int) $phpVersion;
323+
}
324+
301325
public function machineType(): Architecture
302326
{
303327
$phpMachineType = self::cleanWarningAndDeprecationsFromOutput(Process::run([

src/Platform/TargetPhp/PhpizePath.php

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
use RuntimeException;
88
use Symfony\Component\Process\Process;
99

10-
use function array_key_exists;
1110
use function assert;
1211
use function file_exists;
1312
use function is_executable;
@@ -27,6 +26,32 @@ public function __construct(public readonly string $phpizeBinaryPath)
2726
{
2827
}
2928

29+
public static function looksLikeValidPhpize(string $phpizePathToCheck, string|null $forPhpApiVersion = null): bool
30+
{
31+
$phpizeAttempt = $phpizePathToCheck; // @todo
32+
if ($phpizeAttempt === '') {
33+
return false;
34+
}
35+
36+
if (! file_exists($phpizeAttempt) || ! is_executable($phpizeAttempt)) {
37+
return false;
38+
}
39+
40+
$phpizeProcess = new Process([$phpizeAttempt, '--version']);
41+
if ($phpizeProcess->run() !== 0) {
42+
return false;
43+
}
44+
45+
if (
46+
! preg_match('/PHP Api Version:\s*(.*)/', $phpizeProcess->getOutput(), $m)
47+
|| $m[1] === ''
48+
) {
49+
return false;
50+
}
51+
52+
return $forPhpApiVersion === null || $forPhpApiVersion === $m[1];
53+
}
54+
3055
public static function guessFrom(PhpBinaryPath $phpBinaryPath): self
3156
{
3257
$expectedApiVersion = $phpBinaryPath->phpApiVersion();
@@ -45,24 +70,8 @@ public static function guessFrom(PhpBinaryPath $phpBinaryPath): self
4570
foreach ($phpizeAttempts as $phpizeAttempt) {
4671
assert($phpizeAttempt !== null);
4772
assert($phpizeAttempt !== '');
48-
if (! file_exists($phpizeAttempt) || ! is_executable($phpizeAttempt)) {
49-
continue;
50-
}
51-
52-
$phpizeProcess = new Process([$phpizeAttempt, '--version']);
53-
if ($phpizeProcess->run() !== 0) {
54-
continue;
55-
}
56-
57-
if (
58-
! preg_match('/PHP Api Version:\s*(.*)/', $phpizeProcess->getOutput(), $m)
59-
|| ! array_key_exists(1, $m)
60-
|| $m[1] === ''
61-
) {
62-
continue;
63-
}
6473

65-
if ($expectedApiVersion === $m[1]) {
74+
if (self::looksLikeValidPhpize($phpizeAttempt, $expectedApiVersion)) {
6675
return new self($phpizeAttempt);
6776
}
6877
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Php\Pie\SelfManage\BuildTools;
6+
7+
use Php\Pie\Platform\TargetPlatform;
8+
use Symfony\Component\Process\ExecutableFinder;
9+
10+
use function array_key_exists;
11+
use function str_replace;
12+
13+
/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
14+
class BinaryBuildToolFinder
15+
{
16+
/** @param array<PackageManager::*, non-empty-string|null> $packageManagerPackages */
17+
public function __construct(
18+
public readonly string $tool,
19+
private readonly array $packageManagerPackages,
20+
) {
21+
}
22+
23+
public function check(): bool
24+
{
25+
return (new ExecutableFinder())->find($this->tool) !== null;
26+
}
27+
28+
/** @return non-empty-string|null */
29+
public function packageNameFor(PackageManager $packageManager, TargetPlatform $targetPlatform): string|null
30+
{
31+
if (! array_key_exists($packageManager->value, $this->packageManagerPackages) || $this->packageManagerPackages[$packageManager->value] === null) {
32+
return null;
33+
}
34+
35+
// If we need to customise specific package names depending on OS
36+
// specific parameters, this is likely the place to do it
37+
return str_replace(
38+
'{major}',
39+
(string) $targetPlatform->phpBinaryPath->majorVersion(),
40+
str_replace(
41+
'{minor}',
42+
(string) $targetPlatform->phpBinaryPath->minorVersion(),
43+
$this->packageManagerPackages[$packageManager->value],
44+
),
45+
);
46+
}
47+
}

0 commit comments

Comments
 (0)