diff --git a/composer.json b/composer.json index 3542a07..43e1b56 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "require": { "php": "8.1.*||8.2.*||8.3.*", "ext-zip": "*", - "composer/composer": "^2.7", + "composer/composer": "dev-main@dev", "guzzlehttp/guzzle": "^7.8", "guzzlehttp/psr7": "^2.6", "illuminate/container": "^10.47", diff --git a/composer.lock b/composer.lock index e8d4935..1189dc7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "694d5bf1b96fcceae6af19c03eb55095", + "content-hash": "fb31ac50e2d4c78585e5dc67fbef9477", "packages": [ { "name": "composer/ca-bundle", @@ -157,16 +157,16 @@ }, { "name": "composer/composer", - "version": "2.7.1", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "aaf6ed5ccd27c23f79a545e351b4d7842a99d0bc" + "reference": "b12a88b7f3313e9dbae5b58085323b8328d10296" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/aaf6ed5ccd27c23f79a545e351b4d7842a99d0bc", - "reference": "aaf6ed5ccd27c23f79a545e351b4d7842a99d0bc", + "url": "https://api.github.com/repos/composer/composer/zipball/b12a88b7f3313e9dbae5b58085323b8328d10296", + "reference": "b12a88b7f3313e9dbae5b58085323b8328d10296", "shasum": "" }, "require": { @@ -205,6 +205,7 @@ "ext-zip": "Enabling the zip extension allows you to unzip archives", "ext-zlib": "Allow gzip compression of HTTP requests" }, + "default-branch": true, "bin": [ "bin/composer" ], @@ -251,7 +252,7 @@ "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/composer/issues", "security": "https://github.com/composer/composer/security/policy", - "source": "https://github.com/composer/composer/tree/2.7.1" + "source": "https://github.com/composer/composer/tree/main" }, "funding": [ { @@ -267,7 +268,7 @@ "type": "tidelift" } ], - "time": "2024-02-09T14:26:28+00:00" + "time": "2024-03-22T08:29:43+00:00" }, { "name": "composer/metadata-minifier", @@ -5740,7 +5741,9 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "composer/composer": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/src/Command/DownloadCommand.php b/src/Command/DownloadCommand.php index 5f8cef2..9fe973a 100644 --- a/src/Command/DownloadCommand.php +++ b/src/Command/DownloadCommand.php @@ -8,10 +8,12 @@ use InvalidArgumentException; use Php\Pie\DependencyResolver\DependencyResolver; use Php\Pie\Downloading\DownloadAndExtract; +use Php\Pie\TargetPhp\PhpBinaryPath; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Webmozart\Assert\Assert; @@ -30,6 +32,7 @@ final class DownloadCommand extends Command { private const ARG_REQUESTED_PACKAGE_AND_VERSION = 'requested-package-and-version'; + private const OPTION_WITH_PHP_CONFIG = 'with-php-config'; public function __construct( private readonly DependencyResolver $dependencyResolver, @@ -47,18 +50,35 @@ public function configure(): void InputArgument::REQUIRED, 'The extension name and version constraint to use, in the format {ext-name}{?:version-constraint}{?@dev-branch-name}, for example `ext-debug:^1.0`', ); + $this->addOption( + self::OPTION_WITH_PHP_CONFIG, + null, + InputOption::VALUE_OPTIONAL, + 'The path to `php-config` to use', + ); } public function execute(InputInterface $input, OutputInterface $output): int { + $phpBinaryPath = PhpBinaryPath::fromCurrentProcess(); + + /** @var mixed $withPhpConfig */ + $withPhpConfig = $input->getOption(self::OPTION_WITH_PHP_CONFIG); + if (is_string($withPhpConfig) && $withPhpConfig !== '') { + $phpBinaryPath = PhpBinaryPath::fromPhpConfigExecutable($withPhpConfig); + } + + $output->writeln(sprintf('You are running PHP %s', PHP_VERSION)); + $output->writeln(sprintf('Target PHP installation: %s (from %s)', $phpBinaryPath->version(), $phpBinaryPath->phpBinaryPath)); + $requestedNameAndVersionPair = $this->requestedNameAndVersionPair($input); $package = ($this->dependencyResolver)( + $phpBinaryPath, $requestedNameAndVersionPair['name'], $requestedNameAndVersionPair['version'], ); - $output->writeln(sprintf('You are running PHP %s', PHP_VERSION)); $output->writeln(sprintf('Found package: %s (version: %s)', $package->name, $package->version)); $output->writeln(sprintf('Dist download URL: %s', $package->downloadUrl ?? '(none)')); diff --git a/src/Container.php b/src/Container.php index 73c6238..a84ce4c 100644 --- a/src/Container.php +++ b/src/Container.php @@ -8,10 +8,7 @@ use Composer\Factory as ComposerFactory; use Composer\IO\ConsoleIO; use Composer\IO\IOInterface; -use Composer\IO\NullIO; use Composer\Repository\CompositeRepository; -use Composer\Repository\PlatformRepository; -use Composer\Repository\RepositoryFactory; use Composer\Repository\RepositorySet; use Composer\Util\AuthHelper; use Composer\Util\Platform; @@ -24,6 +21,7 @@ use Php\Pie\Downloading\DownloadZip; use Php\Pie\Downloading\ExtractZip; use Php\Pie\Downloading\UnixDownloadAndExtract; +use Php\Pie\TargetPhp\ResolveTargetPhpToPlatformRepository; use Psr\Container\ContainerInterface; use RuntimeException; use Symfony\Component\Console\Helper\HelperSet; @@ -51,7 +49,7 @@ public static function factory(): ContainerInterface }); $container->singleton(Composer::class, static function (ContainerInterface $container): Composer { $io = $container->get(IOInterface::class); - $composer = ComposerFactory::create($io); + $composer = (new ComposerFactory())->createComposer($io, [], true); $io->loadConfiguration($composer->getConfig()); return $composer; @@ -59,13 +57,14 @@ public static function factory(): ContainerInterface $container->singleton( DependencyResolver::class, - static function (): DependencyResolver { + static function (ContainerInterface $container): DependencyResolver { + $composer = $container->get(Composer::class); $repositorySet = new RepositorySet(); - $repositorySet->addRepository(new CompositeRepository(RepositoryFactory::defaultReposWithDefaultManager(new NullIO()))); + $repositorySet->addRepository(new CompositeRepository($composer->getRepositoryManager()->getRepositories())); return new ResolveDependencyWithComposer( - new PlatformRepository(), $repositorySet, + new ResolveTargetPhpToPlatformRepository(), ); }, ); diff --git a/src/DependencyResolver/DependencyResolver.php b/src/DependencyResolver/DependencyResolver.php index 908888a..57a4316 100644 --- a/src/DependencyResolver/DependencyResolver.php +++ b/src/DependencyResolver/DependencyResolver.php @@ -4,9 +4,11 @@ namespace Php\Pie\DependencyResolver; +use Php\Pie\TargetPhp\PhpBinaryPath; + /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ interface DependencyResolver { /** @throws UnableToResolveRequirement */ - public function __invoke(string $packageName, string|null $requestedVersion): Package; + public function __invoke(PhpBinaryPath $phpBinaryPath, string $packageName, string|null $requestedVersion): Package; } diff --git a/src/DependencyResolver/ResolveDependencyWithComposer.php b/src/DependencyResolver/ResolveDependencyWithComposer.php index 995b67e..f70c186 100644 --- a/src/DependencyResolver/ResolveDependencyWithComposer.php +++ b/src/DependencyResolver/ResolveDependencyWithComposer.php @@ -6,23 +6,29 @@ use Composer\Package\CompletePackageInterface; use Composer\Package\Version\VersionSelector; -use Composer\Repository\PlatformRepository; use Composer\Repository\RepositorySet; +use Php\Pie\TargetPhp\PhpBinaryPath; +use Php\Pie\TargetPhp\ResolveTargetPhpToPlatformRepository; /** @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 PlatformRepository $platformRepository, private readonly RepositorySet $repositorySet, + private readonly ResolveTargetPhpToPlatformRepository $resolveTargetPhpToPlatformRepository, ) { } - public function __invoke(string $packageName, string|null $requestedVersion): Package + public function __invoke(PhpBinaryPath $phpBinaryPath, string $packageName, string|null $requestedVersion): Package { - $package = (new VersionSelector($this->repositorySet, $this->platformRepository)) + $package = (new VersionSelector( + $this->repositorySet, + ($this->resolveTargetPhpToPlatformRepository)($phpBinaryPath), + )) ->findBestCandidate($packageName, $requestedVersion); + // @todo check it is a `php-ext` or `php-ext-zend` + if (! $package instanceof CompletePackageInterface) { throw UnableToResolveRequirement::fromRequirement($packageName, $requestedVersion); } diff --git a/src/TargetPhp/PhpBinaryPath.php b/src/TargetPhp/PhpBinaryPath.php index d57aa5a..9d364c0 100644 --- a/src/TargetPhp/PhpBinaryPath.php +++ b/src/TargetPhp/PhpBinaryPath.php @@ -8,8 +8,10 @@ use Symfony\Component\Process\Process; use Webmozart\Assert\Assert; +use function trim; + /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ -final class PhpBinaryPath +class PhpBinaryPath { /** @param non-empty-string $phpBinaryPath */ private function __construct(readonly string $phpBinaryPath) @@ -22,6 +24,7 @@ public function version(): string ->mustRun() ->getOutput()); Assert::stringNotEmpty($phpVersion, 'Could not determine PHP version'); + return $phpVersion; } @@ -32,13 +35,15 @@ public static function fromPhpConfigExecutable(string $phpConfig): self ->mustRun() ->getOutput()); Assert::stringNotEmpty($phpExecutable, 'Could not find path to PHP executable.'); + return new self($phpExecutable); } public static function fromCurrentProcess(): self { - $phpExecutable = trim((new PhpExecutableFinder())->find()); + $phpExecutable = trim((string) (new PhpExecutableFinder())->find()); Assert::stringNotEmpty($phpExecutable, 'Could not find path to PHP executable.'); + return new self($phpExecutable); } } diff --git a/src/TargetPhp/ResolveTargetPhpToPlatformRepository.php b/src/TargetPhp/ResolveTargetPhpToPlatformRepository.php new file mode 100644 index 0000000..9e3eaac --- /dev/null +++ b/src/TargetPhp/ResolveTargetPhpToPlatformRepository.php @@ -0,0 +1,16 @@ + $phpBinaryPath->version()]); + } +} diff --git a/test/integration/Command/DownloadCommandTest.php b/test/integration/Command/DownloadCommandTest.php index 1261236..e58aa7e 100644 --- a/test/integration/Command/DownloadCommandTest.php +++ b/test/integration/Command/DownloadCommandTest.php @@ -26,13 +26,18 @@ public function setUp(): void public function testDownloadCommand(): void { - $this->commandTester->execute(['requested-package-and-version' => 'ramsey/uuid']); + 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); + } + + // 1.0.0 is only compatible with PHP 8.3.0 + $this->commandTester->execute(['requested-package-and-version' => 'asgrim/example-pie-extension:1.0.0']); $this->commandTester->assertCommandIsSuccessful(); $outputString = $this->commandTester->getDisplay(); - self::assertStringContainsString('Found package: ramsey/uuid (version: ', $outputString); - self::assertStringContainsString('Dist download URL: https://api.github.com/repos/ramsey/uuid/zipball/', $outputString); + self::assertStringContainsString('Found package: asgrim/example-pie-extension (version: 1.0.0)', $outputString); + self::assertStringContainsString('Dist download URL: https://api.github.com/repos/asgrim/example-pie-extension/zipball/', $outputString); } public function testDownloadCommandFailsWhenUsingIncompatiblePhpVersion(): void @@ -42,6 +47,7 @@ public function testDownloadCommandFailsWhenUsingIncompatiblePhpVersion(): void } $this->expectException(UnableToResolveRequirement::class); - $this->commandTester->execute(['requested-package-and-version' => 'phpunit/phpunit:^11.0']); + // 1.0.0 is only compatible with PHP 8.3.0 + $this->commandTester->execute(['requested-package-and-version' => 'asgrim/example-pie-extension:1.0.0']); } } diff --git a/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php b/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php index 1aca5a1..0e24daa 100644 --- a/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php +++ b/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php @@ -6,11 +6,12 @@ use Composer\IO\NullIO; use Composer\Repository\CompositeRepository; -use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryFactory; use Composer\Repository\RepositorySet; use Php\Pie\DependencyResolver\ResolveDependencyWithComposer; use Php\Pie\DependencyResolver\UnableToResolveRequirement; +use Php\Pie\TargetPhp\PhpBinaryPath; +use Php\Pie\TargetPhp\ResolveTargetPhpToPlatformRepository; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; @@ -19,6 +20,7 @@ final class ResolveDependencyWithComposerTest extends TestCase { private RepositorySet $repositorySet; + private ResolveTargetPhpToPlatformRepository $resolveTargetPhpToPlatformRepository; public function setUp(): void { @@ -26,14 +28,21 @@ public function setUp(): void $this->repositorySet = new RepositorySet(); $this->repositorySet->addRepository(new CompositeRepository(RepositoryFactory::defaultReposWithDefaultManager(new NullIO()))); + + $this->resolveTargetPhpToPlatformRepository = new ResolveTargetPhpToPlatformRepository(); } public function testPackageThatCanBeResolved(): void { + $phpBinaryPath = $this->createMock(PhpBinaryPath::class); + $phpBinaryPath->expects(self::once()) + ->method('version') + ->willReturn('8.2.0'); + $package = (new ResolveDependencyWithComposer( - new PlatformRepository([], ['php' => '8.2.0']), $this->repositorySet, - ))('phpunit/phpunit', '^11.0'); + $this->resolveTargetPhpToPlatformRepository, + ))($phpBinaryPath, 'phpunit/phpunit', '^11.0'); self::assertSame('phpunit/phpunit', $package->name); } @@ -55,12 +64,18 @@ public static function unresolvableDependencies(): array #[DataProvider('unresolvableDependencies')] public function testPackageThatCannotBeResolvedThrowsException(array $platformOverrides, string $package, string $version): void { + $phpBinaryPath = $this->createMock(PhpBinaryPath::class); + $phpBinaryPath->expects(self::once()) + ->method('version') + ->willReturn($platformOverrides['php']); + $this->expectException(UnableToResolveRequirement::class); (new ResolveDependencyWithComposer( - new PlatformRepository([], $platformOverrides), $this->repositorySet, + $this->resolveTargetPhpToPlatformRepository, ))( + $phpBinaryPath, $package, $version, ); diff --git a/test/unit/TargetPhp/PhpBinaryPathTest.php b/test/unit/TargetPhp/PhpBinaryPathTest.php index b59fead..05c7551 100644 --- a/test/unit/TargetPhp/PhpBinaryPathTest.php +++ b/test/unit/TargetPhp/PhpBinaryPathTest.php @@ -8,6 +8,8 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; +use const PHP_VERSION; + #[CoversClass(PhpBinaryPath::class)] final class PhpBinaryPathTest extends TestCase {