diff --git a/src/Container.php b/src/Container.php index eb4db737..fac8f57e 100644 --- a/src/Container.php +++ b/src/Container.php @@ -8,8 +8,6 @@ use Composer\Factory as ComposerFactory; use Composer\IO\ConsoleIO; use Composer\IO\IOInterface; -use Composer\Repository\CompositeRepository; -use Composer\Repository\RepositorySet; use Composer\Util\AuthHelper; use Composer\Util\Platform; use GuzzleHttp\Client; @@ -79,12 +77,8 @@ public static function factory(): ContainerInterface $container->singleton( DependencyResolver::class, static function (ContainerInterface $container): DependencyResolver { - $composer = $container->get(Composer::class); - $repositorySet = new RepositorySet(); - $repositorySet->addRepository(new CompositeRepository($composer->getRepositoryManager()->getRepositories())); - return new ResolveDependencyWithComposer( - $repositorySet, + $container->get(Composer::class), new ResolveTargetPhpToPlatformRepository(), ); }, diff --git a/src/DependencyResolver/ResolveDependencyWithComposer.php b/src/DependencyResolver/ResolveDependencyWithComposer.php index f7af0c13..6872f5c1 100644 --- a/src/DependencyResolver/ResolveDependencyWithComposer.php +++ b/src/DependencyResolver/ResolveDependencyWithComposer.php @@ -4,45 +4,74 @@ namespace Php\Pie\DependencyResolver; +use Composer\Composer; use Composer\Package\CompletePackageInterface; use Composer\Package\Version\VersionSelector; +use Composer\Repository\CompositeRepository; use Composer\Repository\RepositorySet; use Php\Pie\ExtensionType; use Php\Pie\Platform\TargetPhp\ResolveTargetPhpToPlatformRepository; use Php\Pie\Platform\TargetPlatform; use function preg_match; +use function str_starts_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 { public function __construct( - private readonly RepositorySet $repositorySet, + private readonly Composer $composer, private readonly ResolveTargetPhpToPlatformRepository $resolveTargetPhpToPlatformRepository, ) { } - public function __invoke(TargetPlatform $targetPlatform, string $packageName, string|null $requestedVersion): Package + private function factoryRepositorySet(string|null $requestedVersion): RepositorySet { - $preferredStability = 'stable'; - $repoSetFlags = 0; + $minimumStability = 'stable'; /** Stability options from {@see https://getcomposer.org/doc/04-schema.md#minimum-stability} */ - if ($requestedVersion !== null && preg_match('#@(dev|alpha|beta|RC|stable)$#', $requestedVersion, $matches)) { - $preferredStability = $matches[1]; - $repoSetFlags |= RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES; + if ($requestedVersion !== null) { + if (preg_match('#@(dev|alpha|beta|RC|stable)$#', $requestedVersion, $matches)) { + $minimumStability = $matches[1]; + } + + // If a specific stability was not requested, but the version requested was `dev-` something, change to dev min stability + if (! $matches && str_starts_with($requestedVersion, 'dev-')) { + $minimumStability = 'dev'; + } } + $repositorySet = new RepositorySet($minimumStability); + $repositorySet->addRepository(new CompositeRepository($this->composer->getRepositoryManager()->getRepositories())); + + return $repositorySet; + } + + public function __invoke(TargetPlatform $targetPlatform, string $packageName, string|null $requestedVersion): Package + { $package = (new VersionSelector( - $this->repositorySet, + $this->factoryRepositorySet($requestedVersion), ($this->resolveTargetPhpToPlatformRepository)($targetPlatform->phpBinaryPath), )) - ->findBestCandidate($packageName, $requestedVersion, $preferredStability, null, $repoSetFlags); + ->findBestCandidate($packageName, $requestedVersion); if (! $package instanceof CompletePackageInterface) { throw UnableToResolveRequirement::fromRequirement($packageName, $requestedVersion); } + /** + * If a specific commit hash is requested, override the references in the package. This is approximately what + * Composer does anyway: + * + * > ArrayLoader::parseLinks is in charge of this, it drops commit refs, for package resolution purposes we + * > only use dev-main, but we ensure in the PoolBuilder that root references (#...) are set so the dev-main + * > package has its source and dist refs overridden to be whatever you specify and that applies at install + * > time then but package metadata is only read from the branch's head + */ + if ($requestedVersion !== null && preg_match('/#([a-f0-9]{40})$/', $requestedVersion, $matches)) { + $package->setSourceDistReferences($matches[1]); + } + if (! ExtensionType::isValid($package->getType())) { throw UnableToResolveRequirement::toPhpOrZendExtension($package, $packageName, $requestedVersion); } diff --git a/test/integration/Command/DownloadCommandTest.php b/test/integration/Command/DownloadCommandTest.php index 80e8804f..8cc5127d 100644 --- a/test/integration/Command/DownloadCommandTest.php +++ b/test/integration/Command/DownloadCommandTest.php @@ -34,6 +34,11 @@ public function setUp(): void } /** + * Note: this data provider is not intended to provide a fully comprehensive list of supported version mappings + * since it is slightly slower to run (as it actually downloads). For a fuller list of version resolution tests, + * please see {@see \Php\PieIntegrationTest\DependencyResolver\ResolveDependencyWithComposerTest}, which is much + * faster to execute! + * * @return array * * @psalm-suppress PossiblyUnusedMethod https://github.com/psalm/psalm-plugin-phpunit/issues/131 @@ -46,11 +51,7 @@ public static function validVersionsList(): array [self::TEST_PACKAGE . ':1.0.1-alpha.3@alpha', self::TEST_PACKAGE . ':1.0.1-alpha.3'], [self::TEST_PACKAGE . ':*', self::TEST_PACKAGE . ':1.0.1'], [self::TEST_PACKAGE . ':~1.0.0@alpha', self::TEST_PACKAGE . ':1.0.1'], - [self::TEST_PACKAGE . ':^1.1.0@alpha', self::TEST_PACKAGE . ':1.1.0-alpha.4'], [self::TEST_PACKAGE . ':~1.0.0', self::TEST_PACKAGE . ':1.0.1'], - // @todo https://github.com/php/pie/issues/13 - in theory, these could work, on NonWindows at least - // [self::TEST_PACKAGE . ':dev-main', self::TEST_PACKAGE . ':???'], - // [self::TEST_PACKAGE . ':dev-main#769f906413d6d1e12152f6d34134cbcd347ca253', self::TEST_PACKAGE . ':???'], ]; return array_combine( @@ -60,8 +61,10 @@ public static function validVersionsList(): array } #[DataProvider('validVersionsList')] - public function testDownloadCommandWillDownloadCompatibleExtension(string $requestedVersion, string $expectedVersion): void - { + public function testDownloadCommandWillDownloadCompatibleExtension( + string $requestedVersion, + string $expectedVersion, + ): void { if (PHP_VERSION_ID < 80300 || PHP_VERSION_ID >= 80400) { self::markTestSkipped('This test can only run on PHP 8.3 - you are running ' . PHP_VERSION); } @@ -75,6 +78,28 @@ public function testDownloadCommandWillDownloadCompatibleExtension(string $reque self::assertStringContainsString('Extracted ' . $expectedVersion . ' source to', $outputString); } + public function testDownloadCommandWillDownloadSpecificCommits(): void + { + if (Platform::isWindows()) { + self::markTestSkipped('This test can only run on non-Windows systems'); + } + + if (PHP_VERSION_ID < 80300 || PHP_VERSION_ID >= 80400) { + self::markTestSkipped('This test can only run on PHP 8.3 - you are running ' . PHP_VERSION); + } + + $this->commandTester->execute(['requested-package-and-version' => 'asgrim/example-pie-extension:dev-main#9b5e6c80a1e05556e4e6824f0c112a4992cee001']); + + $this->commandTester->assertCommandIsSuccessful(); + + $outputString = $this->commandTester->getDisplay(); + self::assertStringContainsString('Found package: asgrim/example-pie-extension:dev-main which provides', $outputString); + self::assertStringMatchesFormat( + '%AExtracted asgrim/example-pie-extension:dev-main source to: /tmp/%s/asgrim-example-pie-extension-9b5e6c8%A', + $outputString, + ); + } + #[DataProvider('validVersionsList')] public function testDownloadingWithPhpConfig(string $requestedVersion, string $expectedVersion): void { diff --git a/test/integration/DependencyResolver/ResolveDependencyWithComposerTest.php b/test/integration/DependencyResolver/ResolveDependencyWithComposerTest.php new file mode 100644 index 00000000..3be40741 --- /dev/null +++ b/test/integration/DependencyResolver/ResolveDependencyWithComposerTest.php @@ -0,0 +1,82 @@ + + * + * @psalm-suppress PossiblyUnusedMethod https://github.com/psalm/psalm-plugin-phpunit/issues/131 + */ + public static function validVersionsList(): array + { + $versionsAndExpected = [ + [null, '1.0.1', self::DOWNLOAD_URL_ANY], + ['*', '1.0.1', self::DOWNLOAD_URL_ANY], + ['1.0.1-alpha.3@alpha', '1.0.1-alpha.3', self::DOWNLOAD_URL_1_0_1_ALPHA_3], + ['^1.0', '1.0.1', self::DOWNLOAD_URL_1_0_1], + ['^1.1.0@alpha', '1.1.0-beta.1', self::DOWNLOAD_URL_1_1_0_BETA_1], + ['^1.0@beta', '1.0.1', self::DOWNLOAD_URL_1_0_1], + ['^1.1@beta', '1.1.0-beta.1', self::DOWNLOAD_URL_1_1_0_BETA_1], + ['~1.0.0', '1.0.1', self::DOWNLOAD_URL_1_0_1], + ['~1.0.0@alpha', '1.0.1', self::DOWNLOAD_URL_1_0_1], + ['~1.0.0@beta', '1.0.1', self::DOWNLOAD_URL_1_0_1], + ['~1.0@beta', '1.0.1', self::DOWNLOAD_URL_1_0_1], + ['dev-main', 'dev-main', self::DOWNLOAD_URL_ANY], + ['dev-main#769f906413d6d1e12152f6d34134cbcd347ca253', 'dev-main', self::DOWNLOAD_URL_1_0_1], + ]; + + return array_combine( + array_map(static fn ($item) => $item[0] ?? 'null', $versionsAndExpected), + $versionsAndExpected, + ); + } + + #[DataProvider('validVersionsList')] + public function testDependenciesAreResolvedToExpectedVersions( + string|null $requestedVersion, + string $expectedVersion, + string $expectedDownloadUrl, + ): void { + if (PHP_VERSION_ID < 80300 || PHP_VERSION_ID >= 80400) { + self::markTestSkipped('This test can only run on PHP 8.3 - you are running ' . PHP_VERSION); + } + + $container = Container::factory(); + $resolve = $container->get(DependencyResolver::class); + + $package = $resolve->__invoke( + TargetPlatform::fromPhpBinaryPath(PhpBinaryPath::fromCurrentProcess()), + 'asgrim/example-pie-extension', + $requestedVersion, + ); + + self::assertSame($expectedVersion, $package->version); + self::assertNotNull($package->downloadUrl); + self::assertStringMatchesFormat($expectedDownloadUrl, $package->downloadUrl); + } +} diff --git a/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php b/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php index 604911ad..e4d02017 100644 --- a/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php +++ b/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php @@ -4,10 +4,11 @@ namespace Php\PieUnitTest\DependencyResolver; +use Composer\Composer; use Composer\IO\NullIO; use Composer\Repository\CompositeRepository; use Composer\Repository\RepositoryFactory; -use Composer\Repository\RepositorySet; +use Composer\Repository\RepositoryManager; use Php\Pie\DependencyResolver\ResolveDependencyWithComposer; use Php\Pie\DependencyResolver\UnableToResolveRequirement; use Php\Pie\Platform\Architecture; @@ -23,15 +24,20 @@ #[CoversClass(ResolveDependencyWithComposer::class)] final class ResolveDependencyWithComposerTest extends TestCase { - private RepositorySet $repositorySet; + private Composer $composer; private ResolveTargetPhpToPlatformRepository $resolveTargetPhpToPlatformRepository; public function setUp(): void { parent::setUp(); - $this->repositorySet = new RepositorySet(); - $this->repositorySet->addRepository(new CompositeRepository(RepositoryFactory::defaultReposWithDefaultManager(new NullIO()))); + $repoManager = $this->createMock(RepositoryManager::class); + $repoManager->method('getRepositories') + ->willReturn([new CompositeRepository(RepositoryFactory::defaultReposWithDefaultManager(new NullIO()))]); + + $this->composer = $this->createMock(Composer::class); + $this->composer->method('getRepositoryManager') + ->willReturn($repoManager); $this->resolveTargetPhpToPlatformRepository = new ResolveTargetPhpToPlatformRepository(); } @@ -52,7 +58,7 @@ public function testPackageThatCanBeResolved(): void ); $package = (new ResolveDependencyWithComposer( - $this->repositorySet, + $this->composer, $this->resolveTargetPhpToPlatformRepository, ))($targetPlatform, 'asgrim/example-pie-extension', '^1.0'); @@ -94,7 +100,7 @@ public function testPackageThatCannotBeResolvedThrowsException(array $platformOv $this->expectException(UnableToResolveRequirement::class); (new ResolveDependencyWithComposer( - $this->repositorySet, + $this->composer, $this->resolveTargetPhpToPlatformRepository, ))( $targetPlatform,