Skip to content

Commit

Permalink
Ensure target PHP platform is used to resolve package version
Browse files Browse the repository at this point in the history
  • Loading branch information
asgrim committed Apr 1, 2024
1 parent 52d6b65 commit 892c536
Show file tree
Hide file tree
Showing 11 changed files with 106 additions and 32 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
19 changes: 11 additions & 8 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 21 additions & 1 deletion src/Command/DownloadCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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,
Expand All @@ -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('<info>You are running PHP %s</info>', PHP_VERSION));
$output->writeln(sprintf('<info>Target PHP installation: %s (from %s)</info>', $phpBinaryPath->version(), $phpBinaryPath->phpBinaryPath));

$requestedNameAndVersionPair = $this->requestedNameAndVersionPair($input);

$package = ($this->dependencyResolver)(
$phpBinaryPath,
$requestedNameAndVersionPair['name'],
$requestedNameAndVersionPair['version'],
);

$output->writeln(sprintf('<info>You are running PHP %s</info>', PHP_VERSION));
$output->writeln(sprintf('<info>Found package:</info> %s (version: %s)', $package->name, $package->version));
$output->writeln(sprintf('<info>Dist download URL:</info> %s', $package->downloadUrl ?? '(none)'));

Expand Down
13 changes: 6 additions & 7 deletions src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -51,21 +49,22 @@ 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;
});

$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(),
);
},
);
Expand Down
4 changes: 3 additions & 1 deletion src/DependencyResolver/DependencyResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
14 changes: 10 additions & 4 deletions src/DependencyResolver/ResolveDependencyWithComposer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
9 changes: 7 additions & 2 deletions src/TargetPhp/PhpBinaryPath.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -22,6 +24,7 @@ public function version(): string
->mustRun()
->getOutput());
Assert::stringNotEmpty($phpVersion, 'Could not determine PHP version');

return $phpVersion;
}

Expand All @@ -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);
}
}
16 changes: 16 additions & 0 deletions src/TargetPhp/ResolveTargetPhpToPlatformRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Php\Pie\TargetPhp;

use Composer\Repository\PlatformRepository;

class ResolveTargetPhpToPlatformRepository
{
public function __invoke(PhpBinaryPath $phpBinaryPath): PlatformRepository
{
// @todo I expect we also need to map the extensions for the given PHP binary, somehow?
return new PlatformRepository([], ['php' => $phpBinaryPath->version()]);
}
}
14 changes: 10 additions & 4 deletions test/integration/Command/DownloadCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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']);
}
}
23 changes: 19 additions & 4 deletions test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,21 +20,29 @@
final class ResolveDependencyWithComposerTest extends TestCase
{
private RepositorySet $repositorySet;
private ResolveTargetPhpToPlatformRepository $resolveTargetPhpToPlatformRepository;

public function setUp(): void
{
parent::setUp();

$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);
}
Expand All @@ -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,
);
Expand Down
2 changes: 2 additions & 0 deletions test/unit/TargetPhp/PhpBinaryPathTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down

0 comments on commit 892c536

Please sign in to comment.