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
9 changes: 9 additions & 0 deletions src/DependencyResolver/BundledPhpExtensionRefusal.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Php\Pie\DependencyResolver;

use Php\Pie\Platform\TargetPhp\PhpBinaryPath;
use RuntimeException;

use function sprintf;
Expand All @@ -24,4 +25,12 @@ public static function forPackage(Package $package): self
$package->name(),
));
}

public static function forPhpExtraVersion(PhpBinaryPath $phpBinaryPath): self
{
return new self(sprintf(
'Cannot install bundled PHP extension for non-stable versions of PHP (detected: %s)',
$phpBinaryPath->phpVersionWithExtra(),
));
}
}
5 changes: 5 additions & 0 deletions src/DependencyResolver/ResolveDependencyWithComposer.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use function in_array;
use function preg_match;
use function sprintf;
use function str_ends_with;

/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
final class ResolveDependencyWithComposer implements DependencyResolver
Expand Down Expand Up @@ -109,6 +110,10 @@ private function assertBuildProviderProvidersBundledExtensions(TargetPlatform $t
return;
}

if (str_ends_with($targetPlatform->phpBinaryPath->phpVersionWithExtra(), '-dev')) {
throw BundledPhpExtensionRefusal::forPhpExtraVersion($targetPlatform->phpBinaryPath);
}

$buildProvider = $targetPlatform->phpBinaryPath->buildProvider();
$identifiedBuildProvider = false;
$note = '<options=bold,underscore;fg=red>Note:</> ';
Expand Down
13 changes: 13 additions & 0 deletions src/Platform/TargetPhp/PhpBinaryPath.php
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,19 @@ public function version(): string
return $phpVersion;
}

/** @return non-empty-string */
public function phpVersionWithExtra(): string
{
$phpVersionWithExtra = self::cleanWarningAndDeprecationsFromOutput(Process::run([
$this->phpBinaryPath,
'-r',
'echo PHP_VERSION;',
]));
Assert::stringNotEmpty($phpVersionWithExtra, 'Could not determine PHP_VERSION');

return $phpVersionWithExtra;
}

/** @return non-empty-string */
public function majorMinorVersion(): string
{
Expand Down
57 changes: 49 additions & 8 deletions test/unit/DependencyResolver/FetchDependencyStatusesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,24 @@
use Composer\Package\CompletePackage;
use Composer\Package\Link;
use Composer\Semver\Constraint\Constraint;
use Composer\Semver\VersionParser;
use Php\Pie\DependencyResolver\FetchDependencyStatuses;
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 PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

use function assert;

use const PHP_MAJOR_VERSION;
use const PHP_MINOR_VERSION;
use const PHP_RELEASE_VERSION;
use const PHP_VERSION;

#[CoversClass(FetchDependencyStatuses::class)]
final class FetchDependencyStatusesTest extends TestCase
{
Expand All @@ -26,15 +38,44 @@ public function testNoRequiresReturnsEmptyArray(): void
self::assertEquals([], (new FetchDependencyStatuses())(TargetPlatform::fromPhpBinaryPath(PhpBinaryPath::fromCurrentProcess(), null, null), $this->createMock(Composer::class), $package));
}

public function testRequiresReturnsListOfStatuses(): void
/** @return array<non-empty-string, array{0: non-empty-string, 1: non-empty-string}> */
public function phpVersionProvider(): array
{
return [
'8.2.0' => ['8.2.0', '8.2.0'],
'8.2.0-dev' => ['8.2.0', '8.2.0-dev'],
'8.2.0-alpha' => ['8.2.0', '8.2.0-alpha'],
'8.2.0-RC1' => ['8.2.0', '8.2.0-RC1'],
PHP_VERSION => [PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION, PHP_VERSION],
];
}

#[DataProvider('phpVersionProvider')]
public function testRequiresReturnsListOfStatuses(string $version, string $versionWithExtra): void
{
$php = PhpBinaryPath::fromCurrentProcess();
$php = $this->createMock(PhpBinaryPath::class);
$php->method('operatingSystem')->willReturn(OperatingSystem::NonWindows);
$php->method('operatingSystemFamily')->willReturn(OperatingSystemFamily::Linux);
$php->method('machineType')->willReturn(Architecture::x86_64);
$php->expects(self::any())
->method('version')
->willReturn($version);
$php->expects(self::any())
->method('phpVersionWithExtra')
->willReturn($versionWithExtra);
$php->expects(self::any())
->method('extensions')
->willReturn(['Core' => $versionWithExtra, 'standard' => $versionWithExtra]);

$versionParser = new VersionParser();
$parsedPhpVersion = $versionParser->parseConstraints($php->phpVersionWithExtra());
assert($parsedPhpVersion instanceof Constraint);

$package = new CompletePackage('vendor/foo', '1.2.3.0', '1.2.3');
$package->setRequires([
'ext-core' => new Link('__root__', 'ext-core', new Constraint('=', $php->version() . '.0')),
'ext-nonsense_extension' => new Link('__root__', 'ext-nonsense_extension', new Constraint('=', '*')),
'ext-standard' => new Link('__root__', 'ext-standard', new Constraint('<', '1.0.0.0')),
'ext-core' => new Link('__root__', 'ext-core', $versionParser->parseConstraints('= ' . $php->phpVersionWithExtra())),
'ext-nonsense_extension' => new Link('__root__', 'ext-nonsense_extension', $versionParser->parseConstraints('*')),
'ext-standard' => new Link('__root__', 'ext-standard', $versionParser->parseConstraints('< 1.0.0')),
]);

$deps = (new FetchDependencyStatuses())(
Expand All @@ -45,8 +86,8 @@ public function testRequiresReturnsListOfStatuses(): void

self::assertCount(3, $deps);

self::assertSame('ext-core: == ' . $php->version() . '.0 ✅', $deps[0]->asPrettyString());
self::assertSame('ext-nonsense_extension: == * 🚫 (not installed)', $deps[1]->asPrettyString());
self::assertSame('ext-standard: < 1.0.0.0 🚫 (your version is ' . $php->version() . '.0)', $deps[2]->asPrettyString());
self::assertSame('ext-core: = ' . $php->phpVersionWithExtra() . ' ✅', $deps[0]->asPrettyString());
self::assertSame('ext-nonsense_extension: * 🚫 (not installed)', $deps[1]->asPrettyString());
self::assertSame('ext-standard: < 1.0.0 🚫 (your version is ' . $parsedPhpVersion->getVersion() . ')', $deps[2]->asPrettyString());
}
}
130 changes: 128 additions & 2 deletions test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@
use Composer\Repository\RepositoryFactory;
use Composer\Repository\RepositoryManager;
use Composer\Semver\Constraint\Constraint;
use Php\Pie\ComposerIntegration\BundledPhpExtensionsRepository;
use Php\Pie\ComposerIntegration\QuieterConsoleIO;
use Php\Pie\DependencyResolver\BundledPhpExtensionRefusal;
use Php\Pie\DependencyResolver\IncompatibleOperatingSystemFamily;
use Php\Pie\DependencyResolver\IncompatibleThreadSafetyMode;
use Php\Pie\DependencyResolver\RequestedPackageAndVersion;
use Php\Pie\DependencyResolver\ResolveDependencyWithComposer;
use Php\Pie\DependencyResolver\UnableToResolveRequirement;
use Php\Pie\ExtensionType;
use Php\Pie\Platform\Architecture;
use Php\Pie\Platform\OperatingSystem;
use Php\Pie\Platform\OperatingSystemFamily;
Expand Down Expand Up @@ -49,13 +52,21 @@ public function setUp(): void
]);
$this->localRepo = $this->createMock(InstalledRepositoryInterface::class);
$this->localRepo->method('getPackages')->willReturn([
new CompletePackage('already/installed1', '1.2.3.0', '1.2.3'),
new CompletePackage('a/installed1', '1.2.3.0', '1.2.3'),
$packageWithReplaces,
]);

$bundledPhpPackage = new CompletePackage('php/bundled', '8.3.0', '8.3.0.0');
$bundledPhpPackage->setType(ExtensionType::PhpModule->value);

$repoManager = $this->createMock(RepositoryManager::class);
$repoManager->method('getRepositories')
->willReturn([new CompositeRepository(RepositoryFactory::defaultReposWithDefaultManager(new NullIO()))]);
->willReturn([
new CompositeRepository([
...RepositoryFactory::defaultReposWithDefaultManager(new NullIO()),
new BundledPhpExtensionsRepository([$bundledPhpPackage]),
]),
]);
$repoManager->method('getLocalRepository')->willReturn($this->localRepo);

$this->composer = $this->createMock(Composer::class);
Expand Down Expand Up @@ -415,4 +426,119 @@ public function testPackageThatCanBeResolvedWithReplaceConflict(): void
self::assertSame('asgrim/example-pie-extension', $package->name());
self::assertStringStartsWith('1.', $package->version());
}

public function testBundledExtensionCannotBeInstalledOnDevPhpVersion(): void
{
$phpBinaryPath = $this->createMock(PhpBinaryPath::class);
$phpBinaryPath->expects(self::any())
->method('version')
->willReturn('8.3.0');
$phpBinaryPath->expects(self::any())
->method('phpVersionWithExtra')
->willReturn('8.3.0-dev');

$targetPlatform = new TargetPlatform(
OperatingSystem::NonWindows,
OperatingSystemFamily::Linux,
$phpBinaryPath,
Architecture::x86_64,
ThreadSafetyMode::ThreadSafe,
1,
null,
null,
);

$resolver = new ResolveDependencyWithComposer(
$this->createMock(IOInterface::class),
$this->createMock(QuieterConsoleIO::class),
);
$requestedPackage = new RequestedPackageAndVersion('php/bundled', null);

$this->expectException(BundledPhpExtensionRefusal::class);
$this->expectExceptionMessage('Cannot install bundled PHP extension for non-stable versions of PHP');
$resolver->__invoke($this->composer, $targetPlatform, $requestedPackage, false);
}

/** @return array<non-empty-string, array{0: non-empty-string}> */
public function buildProvidersWithBundledExtensionWarnings(): array
{
return [
'Docker' => ['https://github.com/docker-library/php'],
'Debian/Ubuntu' => ['Debian'],
'Remi Repo' => ['Remi\'s RPM repository <https://rpms.remirepo.net/> #StandWithUkraine'],
'Brew' => ['Homebrew'],
];
}

#[DataProvider('buildProvidersWithBundledExtensionWarnings')]
public function testBundledExtensionWillNotInstallOnBuildProviderWithoutForce(string $buildProvider): void
{
$phpBinaryPath = $this->createMock(PhpBinaryPath::class);
$phpBinaryPath->expects(self::any())
->method('version')
->willReturn('8.3.0');
$phpBinaryPath->expects(self::any())
->method('phpVersionWithExtra')
->willReturn('8.3.0');
$phpBinaryPath->expects(self::any())
->method('buildProvider')
->willReturn($buildProvider);

$targetPlatform = new TargetPlatform(
OperatingSystem::NonWindows,
OperatingSystemFamily::Linux,
$phpBinaryPath,
Architecture::x86_64,
ThreadSafetyMode::ThreadSafe,
1,
null,
null,
);

$resolver = new ResolveDependencyWithComposer(
$this->createMock(IOInterface::class),
$this->createMock(QuieterConsoleIO::class),
);
$requestedPackage = new RequestedPackageAndVersion('php/bundled', null);

$this->expectException(BundledPhpExtensionRefusal::class);
$this->expectExceptionMessage('Bundled PHP extension php/bundled should be installed by your distribution, not by PIE');
$resolver->__invoke($this->composer, $targetPlatform, $requestedPackage, false);
}

#[DataProvider('buildProvidersWithBundledExtensionWarnings')]
public function testBundledExtensionWillInstallOnBuildProviderWithForce(string $buildProvider): void
{
$phpBinaryPath = $this->createMock(PhpBinaryPath::class);
$phpBinaryPath->expects(self::any())
->method('version')
->willReturn('8.3.0');
$phpBinaryPath->expects(self::any())
->method('phpVersionWithExtra')
->willReturn('8.3.0');
$phpBinaryPath->expects(self::any())
->method('buildProvider')
->willReturn($buildProvider);

$targetPlatform = new TargetPlatform(
OperatingSystem::NonWindows,
OperatingSystemFamily::Linux,
$phpBinaryPath,
Architecture::x86_64,
ThreadSafetyMode::ThreadSafe,
1,
null,
null,
);

$resolver = new ResolveDependencyWithComposer(
$this->createMock(IOInterface::class),
$this->createMock(QuieterConsoleIO::class),
);
$requestedPackage = new RequestedPackageAndVersion('php/bundled', null);

$package = $resolver->__invoke($this->composer, $targetPlatform, $requestedPackage, true);

self::assertSame('php/bundled', $package->name());
}
}