Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Command/BuildCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public function configure(): void
public function execute(InputInterface $input, OutputInterface $output): int
{
$targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $this->io);
CommandHelper::assertExtensionPathIsConsistent($targetPlatform, $input, $this->io);
try {
$requestedNamesAndVersions = CommandHelper::requestedNameAndVersionPairs($input);
} catch (InvalidPackageName $invalidPackageName) {
Expand Down
30 changes: 30 additions & 0 deletions src/Command/CommandHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
use Php\Pie\Platform\TargetPhp\PhpBinaryPath;
use Php\Pie\Platform\TargetPhp\PhpizePath;
use Php\Pie\Platform\TargetPlatform;
use Php\Pie\Util\Realpath;
use Psr\Container\ContainerInterface;
use RuntimeException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
Expand Down Expand Up @@ -313,6 +315,34 @@ public static function determineForceInstallingPackageVersion(InputInterface $in
return $input->hasOption(self::OPTION_FORCE) && $input->getOption(self::OPTION_FORCE);
}

public static function assertExtensionPathIsConsistent(TargetPlatform $targetPlatform, InputInterface $input, IOInterface $io): void
{
$phpConfigExtensionPath = $targetPlatform->phpBinaryPath->phpConfigExtensionPath();
$iniExtensionPath = $targetPlatform->phpBinaryPath->extensionPath();

if ($phpConfigExtensionPath === null || Realpath::compare($phpConfigExtensionPath, $iniExtensionPath)) {
return;
}

$message = sprintf(
<<<'ERROR'
The php.ini `extension_dir` directive (%s) does not match `php-config --extension-dir` (%s). This means
that installs will likely fail (as the extension will be installed in one place, but PHP is looking in
another place).


ERROR,
$iniExtensionPath,
$phpConfigExtensionPath,
);

if (! self::determineForceInstallingPackageVersion($input)) {
throw new RuntimeException($message . 'Re-run with --force to attempt the install anyway.');
}

$io->writeError('<comment>Warning: ' . $message . 'Proceeding anyway because --force was used.</comment>');
}

public static function noDev(InputInterface $input): bool
{
return $input->hasOption(self::OPTION_NO_DEV) && $input->getOption(self::OPTION_NO_DEV);
Expand Down
1 change: 1 addition & 0 deletions src/Command/InstallCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ public function execute(InputInterface $input, OutputInterface $output): int
}

$targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $this->io);
CommandHelper::assertExtensionPathIsConsistent($targetPlatform, $input, $this->io);
try {
$requestedNamesAndVersions = CommandHelper::requestedNameAndVersionPairs($input);
} catch (InvalidPackageName $invalidPackageName) {
Expand Down
1 change: 1 addition & 0 deletions src/Command/InstallExtensionsForProjectCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ private function handlePhpProject(InputInterface $input, RootPackageInterface $r
{
$extensionToPackageSelections = CommandHelper::determineExtensionToPackageSelections($input);
$targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $this->io);
CommandHelper::assertExtensionPathIsConsistent($targetPlatform, $input, $this->io);

$allowNonInteractive = $input->hasOption(CommandHelper::OPTION_ALLOW_NON_INTERACTIVE_PROJECT_INSTALL) && $input->getOption(CommandHelper::OPTION_ALLOW_NON_INTERACTIVE_PROJECT_INSTALL);
if ($allowNonInteractive) {
Expand Down
1 change: 1 addition & 0 deletions src/Command/UpgradeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public function execute(InputInterface $input, OutputInterface $output): int
}

$targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $this->io);
CommandHelper::assertExtensionPathIsConsistent($targetPlatform, $input, $this->io);

$forceInstallPackageVersion = CommandHelper::determineForceInstallingPackageVersion($input);
CommandHelper::applyNoCacheOptionIfSet($input, $this->io);
Expand Down
12 changes: 12 additions & 0 deletions src/Platform/TargetPhp/PhpBinaryPath.php
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,18 @@ public function extensionPath(string|null $prefixInstallRoot = null): string
throw ExtensionPathProblem::new($this, $extensionPath);
}

/** @return non-empty-string|null */
public function phpConfigExtensionPath(): string|null
{
if ($this->phpConfigPath === null) {
return null;
}

$extensionDir = self::cleanWarningAndDeprecationsFromOutput(Process::run([$this->phpConfigPath, '--extension-dir']));

return $extensionDir !== '' ? $extensionDir : null;
}

public function assertExtensionIsLoadedInRuntime(ExtensionName $extension, IOInterface|null $io = null): void
{
if (! in_array(strtolower($extension->name()), array_map('strtolower', array_keys($this->extensions())))) {
Expand Down
15 changes: 15 additions & 0 deletions src/Util/Realpath.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Php\Pie\Util;

use function Safe\realpath;

final class Realpath
{
public static function compare(string $path1, string $path2): bool
{
return realpath($path1) === realpath($path2);
}
}
124 changes: 124 additions & 0 deletions test/unit/Command/CommandHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,17 @@
use Php\Pie\DependencyResolver\ResolvedPackageRequest;
use Php\Pie\DependencyResolver\UnableToResolveRequirement;
use Php\Pie\Downloading\DownloadUrlMethod;
use Php\Pie\Platform\Architecture;
use Php\Pie\Platform\OperatingSystem;
use Php\Pie\Platform\OperatingSystemFamily;
use Php\Pie\Platform\TargetPhp\PhpBinaryPath;
use Php\Pie\Platform\TargetPlatform;
use Php\Pie\Platform\ThreadSafetyMode;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\RequiresOperatingSystemFamily;
use PHPUnit\Framework\TestCase;
use RuntimeException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputDefinition;
Expand All @@ -37,8 +43,14 @@

use function array_combine;
use function array_map;
use function Safe\mkdir;
use function Safe\symlink;
use function str_replace;
use function sys_get_temp_dir;
use function trim;
use function uniqid;

use const DIRECTORY_SEPARATOR;

#[CoversClass(CommandHelper::class)]
final class CommandHelperTest extends TestCase
Expand Down Expand Up @@ -376,6 +388,118 @@ public function testDetermineSuppressedDownloadUrlMethodsThrowsForInvalidValue()
CommandHelper::determineSuppressedDownloadUrlMethods($input);
}

private function targetPlatformWithExtensionPaths(string|null $phpConfigExtensionPath, string $iniExtensionPath): TargetPlatform
{
$phpBinary = $this->createMock(PhpBinaryPath::class);
$phpBinary->method('phpConfigExtensionPath')->willReturn($phpConfigExtensionPath);
$phpBinary->method('extensionPath')->willReturn($iniExtensionPath);

return new TargetPlatform(
OperatingSystem::NonWindows,
OperatingSystemFamily::Linux,
$phpBinary,
Architecture::x86_64,
ThreadSafetyMode::NonThreadSafe,
1,
null,
null,
);
}

/** @return non-empty-string */
private function realTempDir(): string
{
$dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie-test-extension-dir-', true);
mkdir($dir, 0777, true);

return $dir;
}

public function testAssertExtensionPathDoesNothingWhenPhpConfigNotUsed(): void
{
$targetPlatform = $this->targetPlatformWithExtensionPaths(null, $this->realTempDir());
$io = new BufferIO();

CommandHelper::assertExtensionPathIsConsistent($targetPlatform, new ArrayInput([]), $io);

self::assertSame('', $io->getOutput());
}

public function testAssertExtensionPathDoesNothingWhenPathsMatch(): void
{
$dir = $this->realTempDir();
$targetPlatform = $this->targetPlatformWithExtensionPaths($dir, $dir);
$io = new BufferIO();

CommandHelper::assertExtensionPathIsConsistent($targetPlatform, new ArrayInput([]), $io);

self::assertSame('', $io->getOutput());
}

public function testAssertExtensionPathDoesNothingWhenPathsAreSymlinkedToTheSameRealPath(): void
{
if (Platform::isWindows()) {
self::markTestSkipped('Skipping for Windows as ineffective');
}

$realDir = $this->realTempDir();

$symlinkPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie-test-extension-dir-symlink-', true);
symlink($realDir, $symlinkPath);

$targetPlatform = $this->targetPlatformWithExtensionPaths($symlinkPath, $realDir);
$io = new BufferIO();

CommandHelper::assertExtensionPathIsConsistent($targetPlatform, new ArrayInput([]), $io);

self::assertSame('', $io->getOutput());
}

public function testAssertExtensionPathThrowsWhenPathsMatch(): void
{
$phpConfigExtensionPath = $this->realTempDir();
$iniExtensionPath = $this->realTempDir();
$targetPlatform = $this->targetPlatformWithExtensionPaths($phpConfigExtensionPath, $iniExtensionPath);

$this->expectException(RuntimeException::class);
$this->expectExceptionMessage(<<<EXCEPTION
The php.ini `extension_dir` directive ($iniExtensionPath) does not match `php-config --extension-dir` ($phpConfigExtensionPath). This means
that installs will likely fail (as the extension will be installed in one place, but PHP is looking in
another place).

Re-run with --force to attempt the install anyway.
EXCEPTION);

CommandHelper::assertExtensionPathIsConsistent($targetPlatform, new ArrayInput([]), new BufferIO());
}

public function testAssertExtensionPathWarnsWhenPathsMatchButLukeUsesTheForce(): void
{
$phpConfigExtensionPath = $this->realTempDir();
$iniExtensionPath = $this->realTempDir();
$targetPlatform = $this->targetPlatformWithExtensionPaths($phpConfigExtensionPath, $iniExtensionPath);

$command = new Command();
CommandHelper::configureDownloadBuildInstallOptions($command);
$input = new ArrayInput(['--force' => true]);
CommandHelper::validateInput($input, $command);

$io = new BufferIO();

CommandHelper::assertExtensionPathIsConsistent($targetPlatform, $input, $io);

self::assertStringContainsString(
<<<WARNING
Warning: The php.ini `extension_dir` directive ($iniExtensionPath) does not match `php-config --extension-dir` ($phpConfigExtensionPath). This means
that installs will likely fail (as the extension will be installed in one place, but PHP is looking in
another place).

Proceeding anyway because --force was used.
WARNING,
$io->getOutput(),
);
}

public function testListRepositories(): void
{
$io = new BufferIO();
Expand Down
15 changes: 15 additions & 0 deletions test/unit/Platform/TargetPhp/PhpBinaryPathTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,21 @@ public function testFromPhpConfigExecutable(string $phpConfigPath, string $expec
);

self::assertSame($phpConfigPath, $phpBinary->phpConfigPath());

self::assertSame(
Process::run([$phpConfigPath, '--extension-dir']),
$phpBinary->phpConfigExtensionPath(),
);
}

public function testPhpConfigExtensionPathIsNullWhenPhpConfigIsNotPresent(): void
{
$phpExecutable = trim((string) (new PhpExecutableFinder())->find());
assert($phpExecutable !== '');

$phpBinary = PhpBinaryPath::fromPhpBinaryPath($phpExecutable);

self::assertNull($phpBinary->phpConfigExtensionPath());
}

public function testExtensions(): void
Expand Down
63 changes: 63 additions & 0 deletions test/unit/Util/RealpathTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

namespace Php\PieUnitTest\Util;

use Composer\Util\Platform;
use Php\Pie\Util\Realpath;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;

use function Safe\mkdir;
use function Safe\symlink;
use function sys_get_temp_dir;
use function uniqid;

use const DIRECTORY_SEPARATOR;

#[CoversClass(Realpath::class)]
final class RealpathTest extends TestCase
{
/** @return non-empty-string */
private function realTempDir(): string
{
$dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie-test-realpath-', true);
mkdir($dir, 0777, true);

return $dir;
}

public function testSamePathIsEqual(): void
{
$dir = $this->realTempDir();

self::assertTrue(Realpath::compare($dir, $dir));
}

public function testDifferentPathsAreNotEqual(): void
{
self::assertFalse(Realpath::compare($this->realTempDir(), $this->realTempDir()));
}

public function testSymlinkedPathIsEqualToItsRealTarget(): void
{
if (Platform::isWindows()) {
self::markTestSkipped('Skipping for Windows as ineffective');
}

$realDir = $this->realTempDir();

$symlinkPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie-test-realpath-symlink-', true);
symlink($realDir, $symlinkPath);

self::assertTrue(Realpath::compare($symlinkPath, $realDir));
}

public function testTrailingDirectorySeparatorIsNormalised(): void
{
$dir = $this->realTempDir();

self::assertTrue(Realpath::compare($dir, $dir . DIRECTORY_SEPARATOR));
}
}