diff --git a/src/Command/BuildCommand.php b/src/Command/BuildCommand.php index 278b6936..8fd323dc 100644 --- a/src/Command/BuildCommand.php +++ b/src/Command/BuildCommand.php @@ -147,6 +147,7 @@ public function execute(InputInterface $input, OutputInterface $output): int PieOperation::Build, $configureOptionsValues, false, // setting up INI not needed for build + suppressedDownloadUrlMethods: CommandHelper::determineSuppressedDownloadUrlMethods($input), ), ); diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index 4974dae8..86b1e0b9 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -23,6 +23,7 @@ use Php\Pie\DependencyResolver\RequestedPackageAndVersion; use Php\Pie\DependencyResolver\ResolvedPackageRequest; use Php\Pie\DependencyResolver\UnableToResolveRequirement; +use Php\Pie\Downloading\DownloadUrlMethod; use Php\Pie\ExtensionName; use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages; use Php\Pie\Platform as PiePlatform; @@ -36,6 +37,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Throwable; +use ValueError; use Webmozart\Assert\Assert; use function array_key_exists; @@ -44,6 +46,7 @@ use function assert; use function count; use function explode; +use function implode; use function is_array; use function is_dir; use function is_string; @@ -75,6 +78,7 @@ final class CommandHelper private const OPTION_MAKE_PARALLEL_JOBS = 'make-parallel-jobs'; private const OPTION_SKIP_ENABLE_EXTENSION = 'skip-enable-extension'; private const OPTION_FORCE = 'force'; + private const OPTION_SUPPRESS_DOWNLOAD_URL_METHOD = 'suppress-download-url-method'; private const OPTION_NO_CACHE = 'no-cache'; private const OPTION_AUTO_INSTALL_BUILD_TOOLS = 'auto-install-build-tools'; private const OPTION_SUPPRESS_BUILD_TOOLS_CHECK = 'no-build-tools-check'; @@ -152,6 +156,14 @@ public static function configureDownloadBuildInstallOptions(Command $command, bo 'To attempt to install a version that doesn\'t match the version constraints from the meta-data, for instance to install an older version than recommended, or when the signature is not available.', ); + $command->addOption( + self::OPTION_SUPPRESS_DOWNLOAD_URL_METHOD, + null, + InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'Do not use the specified download URL methods if they are supported by the extension. May be specified multiple times. Valid values: ' + . implode(', ', array_map(static fn (DownloadUrlMethod $downloadUrlMethod): string => $downloadUrlMethod->value, DownloadUrlMethod::cases())), + ); + $command->addOption( self::OPTION_WORKING_DIRECTORY, 'd', @@ -293,6 +305,35 @@ public static function determineForceInstallingPackageVersion(InputInterface $in return $input->hasOption(self::OPTION_FORCE) && $input->getOption(self::OPTION_FORCE); } + /** @return list */ + public static function determineSuppressedDownloadUrlMethods(InputInterface $input): array + { + if (! $input->hasOption(self::OPTION_SUPPRESS_DOWNLOAD_URL_METHOD)) { + return []; + } + + $suppressedDownloadUrlMethods = $input->getOption(self::OPTION_SUPPRESS_DOWNLOAD_URL_METHOD); + assert(is_array($suppressedDownloadUrlMethods)); + + return array_values(array_map( + static function (mixed $suppressedDownloadUrlMethod): DownloadUrlMethod { + assert(is_string($suppressedDownloadUrlMethod) && $suppressedDownloadUrlMethod !== ''); + + try { + return DownloadUrlMethod::from($suppressedDownloadUrlMethod); + } catch (ValueError) { + throw new InvalidArgumentException(sprintf( + 'Invalid value "%s" for --%s; valid values are: %s', + $suppressedDownloadUrlMethod, + self::OPTION_SUPPRESS_DOWNLOAD_URL_METHOD, + implode(', ', array_map(static fn (DownloadUrlMethod $downloadUrlMethod): string => $downloadUrlMethod->value, DownloadUrlMethod::cases())), + )); + } + }, + $suppressedDownloadUrlMethods, + )); + } + public static function autoInstallBuildTools(InputInterface $input): bool { return $input->hasOption(self::OPTION_AUTO_INSTALL_BUILD_TOOLS) diff --git a/src/Command/DownloadCommand.php b/src/Command/DownloadCommand.php index b8781ed8..b102cf35 100644 --- a/src/Command/DownloadCommand.php +++ b/src/Command/DownloadCommand.php @@ -73,6 +73,7 @@ public function execute(InputInterface $input, OutputInterface $output): int PieOperation::Download, [], // Configure options are not needed for download only false, // setting up INI not needed for download + suppressedDownloadUrlMethods: CommandHelper::determineSuppressedDownloadUrlMethods($input), ), ); diff --git a/src/Command/InstallCommand.php b/src/Command/InstallCommand.php index d23ec4d3..c978c0fe 100644 --- a/src/Command/InstallCommand.php +++ b/src/Command/InstallCommand.php @@ -195,6 +195,7 @@ public function execute(InputInterface $input, OutputInterface $output): int $configureOptionsValues, CommandHelper::determineAttemptToSetupIniFile($input), installAllPackages: $installFromLock, + suppressedDownloadUrlMethods: CommandHelper::determineSuppressedDownloadUrlMethods($input), ), ); diff --git a/src/Command/UpgradeCommand.php b/src/Command/UpgradeCommand.php index c67aee22..d644d4a1 100644 --- a/src/Command/UpgradeCommand.php +++ b/src/Command/UpgradeCommand.php @@ -102,6 +102,7 @@ public function execute(InputInterface $input, OutputInterface $output): int $configureOptions, CommandHelper::determineAttemptToSetupIniFile($input), installAllPackages: true, + suppressedDownloadUrlMethods: CommandHelper::determineSuppressedDownloadUrlMethods($input), ), ); diff --git a/src/ComposerIntegration/Listeners/AllDownloadUrlMethodsSuppressed.php b/src/ComposerIntegration/Listeners/AllDownloadUrlMethodsSuppressed.php new file mode 100644 index 00000000..825b7ea0 --- /dev/null +++ b/src/ComposerIntegration/Listeners/AllDownloadUrlMethodsSuppressed.php @@ -0,0 +1,21 @@ +name(), + )); + } +} diff --git a/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php b/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php index 086413f7..46272f47 100644 --- a/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php +++ b/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php @@ -21,6 +21,7 @@ use Throwable; use function array_walk; +use function in_array; use function pathinfo; use const PATHINFO_EXTENSION; @@ -75,6 +76,25 @@ function (OperationInterface $operation): void { $targetPlatform = $this->composerRequest->targetPlatform; $downloadUrlMethods = DownloadUrlMethod::possibleDownloadUrlMethodsForPackage($piePackage, $targetPlatform); + if ($this->composerRequest->suppressedDownloadUrlMethods !== []) { + $remainingDownloadUrlMethods = []; + + foreach ($downloadUrlMethods as $downloadUrlMethod) { + if (in_array($downloadUrlMethod, $this->composerRequest->suppressedDownloadUrlMethods, true)) { + $this->io->write('Suppressing download method: ' . $downloadUrlMethod->value, verbosity: IOInterface::VERBOSE); + continue; + } + + $remainingDownloadUrlMethods[] = $downloadUrlMethod; + } + + if ($remainingDownloadUrlMethods === []) { + throw AllDownloadUrlMethodsSuppressed::forPackage($piePackage); + } + + $downloadUrlMethods = $remainingDownloadUrlMethods; + } + $selectedDownloadUrlMethod = null; $downloadMethodFailures = []; diff --git a/src/ComposerIntegration/PieComposerRequest.php b/src/ComposerIntegration/PieComposerRequest.php index fd024ec0..90fe4a9c 100644 --- a/src/ComposerIntegration/PieComposerRequest.php +++ b/src/ComposerIntegration/PieComposerRequest.php @@ -6,6 +6,7 @@ use Composer\IO\IOInterface; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; +use Php\Pie\Downloading\DownloadUrlMethod; use Php\Pie\Platform\TargetPlatform; use function array_map; @@ -23,7 +24,8 @@ final class PieComposerRequest /** * @param list $requestedPackages - * @param array> $configureOptions Keyed by package name + * @param array> $configureOptions Keyed by package name + * @param list $suppressedDownloadUrlMethods */ public function __construct( public readonly IOInterface $pieOutput, @@ -33,6 +35,7 @@ public function __construct( public readonly array $configureOptions, public readonly bool $attemptToSetupIniFile, public readonly bool $installAllPackages = false, + public readonly array $suppressedDownloadUrlMethods = [], ) { $this->requestedPackageNames = array_map(static fn (RequestedPackageAndVersion $request) => $request->package, $this->requestedPackages); } diff --git a/test/unit/Command/CommandHelperTest.php b/test/unit/Command/CommandHelperTest.php index 084661d8..eeb3c985 100644 --- a/test/unit/Command/CommandHelperTest.php +++ b/test/unit/Command/CommandHelperTest.php @@ -23,6 +23,7 @@ use Php\Pie\DependencyResolver\RequestedPackageAndVersion; use Php\Pie\DependencyResolver\ResolvedPackageRequest; use Php\Pie\DependencyResolver\UnableToResolveRequirement; +use Php\Pie\Downloading\DownloadUrlMethod; use Php\Pie\Platform\TargetPlatform; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; @@ -340,6 +341,41 @@ public function testWindowsMachinesCannotUseWithPhpizePathOption(): void CommandHelper::determineTargetPlatformFromInputs($input, $io); } + public function testDetermineSuppressedDownloadUrlMethodsDefaultsToEmpty(): void + { + $command = new Command(); + $input = new ArrayInput([]); + CommandHelper::configureDownloadBuildInstallOptions($command); + CommandHelper::validateInput($input, $command); + + self::assertSame([], CommandHelper::determineSuppressedDownloadUrlMethods($input)); + } + + public function testDetermineSuppressedDownloadUrlMethodsParsesGivenValues(): void + { + $command = new Command(); + $input = new ArrayInput(['--suppress-download-url-method' => ['composer-default', 'pre-packaged-source']]); + CommandHelper::configureDownloadBuildInstallOptions($command); + CommandHelper::validateInput($input, $command); + + self::assertSame( + [DownloadUrlMethod::ComposerDefaultDownload, DownloadUrlMethod::PrePackagedSourceDownload], + CommandHelper::determineSuppressedDownloadUrlMethods($input), + ); + } + + public function testDetermineSuppressedDownloadUrlMethodsThrowsForInvalidValue(): void + { + $command = new Command(); + $input = new ArrayInput(['--suppress-download-url-method' => ['not-a-real-method']]); + CommandHelper::configureDownloadBuildInstallOptions($command); + CommandHelper::validateInput($input, $command); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid value "not-a-real-method" for --suppress-download-url-method; valid values are: composer-default, windows-binary, pre-packaged-source, pre-packaged-binary'); + CommandHelper::determineSuppressedDownloadUrlMethods($input); + } + public function testListRepositories(): void { $io = new BufferIO(); diff --git a/test/unit/ComposerIntegration/Listeners/AllDownloadUrlMethodsSuppressedTest.php b/test/unit/ComposerIntegration/Listeners/AllDownloadUrlMethodsSuppressedTest.php new file mode 100644 index 00000000..ba000b22 --- /dev/null +++ b/test/unit/ComposerIntegration/Listeners/AllDownloadUrlMethodsSuppressedTest.php @@ -0,0 +1,32 @@ +createMock(CompletePackageInterface::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('bar'), + 'foo/bar', + '1.2.3', + null, + ))->getMessage(), + ); + } +} diff --git a/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php index 022b05b1..f7ce589a 100644 --- a/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php +++ b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php @@ -13,6 +13,7 @@ use Composer\IO\IOInterface; use Composer\Package\CompletePackage; use Composer\Package\Package; +use Php\Pie\ComposerIntegration\Listeners\AllDownloadUrlMethodsSuppressed; use Php\Pie\ComposerIntegration\Listeners\CouldNotDetermineDownloadUrlMethod; use Php\Pie\ComposerIntegration\Listeners\OverrideDownloadUrlInstallListener; use Php\Pie\ComposerIntegration\PieComposerRequest; @@ -677,4 +678,124 @@ public function testNoSelectedDownloadUrlMethodWillThrowException(): void $this->expectExceptionMessage('Could not download foo/bar using pre-packaged-binary method: nope not found'); $listener($installerEvent); } + + public function testSuppressedDownloadUrlMethodIsSkipped(): void + { + $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); + $composerPackage->setDistType('zip'); + $composerPackage->setDistUrl('https://example.com/git-archive-zip-url'); + $composerPackage->setPhpExt([ + 'extension-name' => 'foobar', + 'download-url-method' => ['pre-packaged-binary', 'composer-default'], + ]); + + $installerEvent = new InstallerEvent( + InstallerEvents::PRE_OPERATIONS_EXEC, + $this->composer, + $this->io, + false, + true, + new Transaction([], [$composerPackage]), + ); + + $this->container + ->expects(self::never()) + ->method('get'); + + /** @var list> $writtenAtVerbose */ + $writtenAtVerbose = []; + $this->io + ->method('write') + ->willReturnCallback( + static function (string|array $messages, bool $newline = true, int $verbosity = IOInterface::NORMAL) use (&$writtenAtVerbose): void { + if ($verbosity !== IOInterface::VERBOSE) { + return; + } + + $writtenAtVerbose[] = $messages; + }, + ); + + (new OverrideDownloadUrlInstallListener( + $this->composer, + $this->io, + $this->container, + new PieComposerRequest( + $this->createMock(IOInterface::class), + new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + WindowsCompiler::VC15, + null, + ), + [new RequestedPackageAndVersion('foo/bar', '^1.1')], + PieOperation::Install, + [], + false, + suppressedDownloadUrlMethods: [DownloadUrlMethod::PrePackagedBinary], + ), + ))($installerEvent); + + self::assertSame( + 'https://example.com/git-archive-zip-url', + $composerPackage->getDistUrl(), + ); + self::assertSame(DownloadUrlMethod::ComposerDefaultDownload, DownloadUrlMethod::fromComposerPackage($composerPackage)); + self::assertContains('Suppressing download method: pre-packaged-binary', $writtenAtVerbose); + } + + public function testSuppressingAllDownloadUrlMethodsWillThrowException(): void + { + $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); + $composerPackage->setDistType('zip'); + $composerPackage->setDistUrl('https://example.com/git-archive-zip-url'); + $composerPackage->setPhpExt([ + 'extension-name' => 'foobar', + 'download-url-method' => ['pre-packaged-binary', 'composer-default'], + ]); + + $installerEvent = new InstallerEvent( + InstallerEvents::PRE_OPERATIONS_EXEC, + $this->composer, + $this->io, + false, + true, + new Transaction([], [$composerPackage]), + ); + + $this->container + ->expects(self::never()) + ->method('get'); + + $listener = new OverrideDownloadUrlInstallListener( + $this->composer, + $this->io, + $this->container, + new PieComposerRequest( + $this->createMock(IOInterface::class), + new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + WindowsCompiler::VC15, + null, + ), + [new RequestedPackageAndVersion('foo/bar', '^1.1')], + PieOperation::Install, + [], + false, + suppressedDownloadUrlMethods: [DownloadUrlMethod::PrePackagedBinary, DownloadUrlMethod::ComposerDefaultDownload], + ), + ); + + $this->expectException(AllDownloadUrlMethodsSuppressed::class); + $listener($installerEvent); + } }