From b36c23251527768685630ea0033b21b664c952e4 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 23 Dec 2025 15:09:10 +0000 Subject: [PATCH 01/16] 436: proof of concept using prebuilt binaries --- src/Building/UnixBuild.php | 41 +++++++++++++++++++ .../InstallAndBuildProcess.php | 2 + src/Downloading/DownloadUrlMethod.php | 3 ++ src/Installing/Install.php | 1 + src/Installing/UnixInstall.php | 21 ++++++++-- src/Installing/WindowsInstall.php | 1 + src/Platform/DebugBuild.php | 12 ++++++ src/Platform/LibcFlavour.php | 12 ++++++ src/Platform/PrePackagedBinaryAssetName.php | 35 ++++++++++++++++ .../Installing/UnixInstallTest.php | 3 +- .../Installing/WindowsInstallTest.php | 2 +- 11 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 src/Platform/DebugBuild.php create mode 100644 src/Platform/LibcFlavour.php create mode 100644 src/Platform/PrePackagedBinaryAssetName.php diff --git a/src/Building/UnixBuild.php b/src/Building/UnixBuild.php index ef05661e..6c2cd196 100644 --- a/src/Building/UnixBuild.php +++ b/src/Building/UnixBuild.php @@ -5,8 +5,10 @@ namespace Php\Pie\Building; use Composer\IO\IOInterface; +use LogicException; use Php\Pie\ComposerIntegration\BundledPhpExtensionsRepository; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\Downloading\DownloadUrlMethod; use Php\Pie\File\BinaryFile; use Php\Pie\Platform\TargetPhp\PhpizePath; use Php\Pie\Platform\TargetPlatform; @@ -33,6 +35,45 @@ public function __invoke( array $configureOptions, IOInterface $io, PhpizePath|null $phpizePath, + ): BinaryFile { + switch (DownloadUrlMethod::fromPackage($downloadedPackage->package, $targetPlatform)) { + case DownloadUrlMethod::PrePackagedBinary: + return $this->prePackagedBinary($downloadedPackage, $io); + + case DownloadUrlMethod::ComposerDefaultDownload: + case DownloadUrlMethod::PrePackagedSourceDownload: + return $this->buildFromSource($downloadedPackage, $targetPlatform, $configureOptions, $io, $phpizePath); + + default: + throw new LogicException('Unknown download method'); + } + } + + private function prePackagedBinary( + DownloadedPackage $downloadedPackage, + IOInterface $io, + ): BinaryFile { + $expectedSoFile = $downloadedPackage->extractedSourcePath . '/' . $downloadedPackage->package->extensionName()->name() . '.so'; + + if (! file_exists($expectedSoFile)) { + throw ExtensionBinaryNotFound::fromExpectedBinary($expectedSoFile); + } + + $io->write(sprintf( + 'Pre-packaged binary found: %s', + $expectedSoFile, + )); + + return BinaryFile::fromFileWithSha256Checksum($expectedSoFile); + } + + /** @param list $configureOptions */ + private function buildFromSource( + DownloadedPackage $downloadedPackage, + TargetPlatform $targetPlatform, + array $configureOptions, + IOInterface $io, + PhpizePath|null $phpizePath, ): BinaryFile { $outputCallback = null; if ($io->isVerbose()) { diff --git a/src/ComposerIntegration/InstallAndBuildProcess.php b/src/ComposerIntegration/InstallAndBuildProcess.php index b036b24c..d64a4d79 100644 --- a/src/ComposerIntegration/InstallAndBuildProcess.php +++ b/src/ComposerIntegration/InstallAndBuildProcess.php @@ -48,6 +48,7 @@ public function __invoke( $composerPackage, ); + $builtBinaryFile = null; if ($composerRequest->operation->shouldBuild()) { $builtBinaryFile = ($this->pieBuild)( $downloadedPackage, @@ -75,6 +76,7 @@ public function __invoke( ($this->pieInstall)( $downloadedPackage, $composerRequest->targetPlatform, + $builtBinaryFile, $io, $composerRequest->attemptToSetupIniFile, ), diff --git a/src/Downloading/DownloadUrlMethod.php b/src/Downloading/DownloadUrlMethod.php index 6a5a95d8..2ee05fce 100644 --- a/src/Downloading/DownloadUrlMethod.php +++ b/src/Downloading/DownloadUrlMethod.php @@ -6,6 +6,7 @@ use Php\Pie\DependencyResolver\Package; use Php\Pie\Platform\OperatingSystem; +use Php\Pie\Platform\PrePackagedBinaryAssetName; use Php\Pie\Platform\PrePackagedSourceAssetName; use Php\Pie\Platform\TargetPlatform; use Php\Pie\Platform\WindowsExtensionAssetName; @@ -16,6 +17,7 @@ enum DownloadUrlMethod: string case ComposerDefaultDownload = 'composer-default'; case WindowsBinaryDownload = 'windows-binary'; case PrePackagedSourceDownload = 'pre-packaged-source'; + case PrePackagedBinary = 'pre-packaged-binary'; /** @return non-empty-list|null */ public function possibleAssetNames(Package $package, TargetPlatform $targetPlatform): array|null @@ -24,6 +26,7 @@ public function possibleAssetNames(Package $package, TargetPlatform $targetPlatf self::WindowsBinaryDownload => WindowsExtensionAssetName::zipNames($targetPlatform, $package), self::PrePackagedSourceDownload => PrePackagedSourceAssetName::packageNames($package), self::ComposerDefaultDownload => null, + self::PrePackagedBinary => PrePackagedBinaryAssetName::packageNames($targetPlatform, $package), }; } diff --git a/src/Installing/Install.php b/src/Installing/Install.php index ebdda216..81724d0a 100644 --- a/src/Installing/Install.php +++ b/src/Installing/Install.php @@ -19,6 +19,7 @@ interface Install public function __invoke( DownloadedPackage $downloadedPackage, TargetPlatform $targetPlatform, + BinaryFile|null $builtBinaryFile, IOInterface $io, bool $attemptToSetupIniFile, ): BinaryFile; diff --git a/src/Installing/UnixInstall.php b/src/Installing/UnixInstall.php index 02e32489..d63af4ec 100644 --- a/src/Installing/UnixInstall.php +++ b/src/Installing/UnixInstall.php @@ -6,11 +6,13 @@ use Composer\IO\IOInterface; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\Downloading\DownloadUrlMethod; use Php\Pie\File\BinaryFile; use Php\Pie\File\Sudo; use Php\Pie\Platform\TargetPlatform; use Php\Pie\Util\Process; use RuntimeException; +use Webmozart\Assert\Assert; use function array_unshift; use function file_exists; @@ -29,6 +31,7 @@ public function __construct(private readonly SetupIniFile $setupIniFile) public function __invoke( DownloadedPackage $downloadedPackage, TargetPlatform $targetPlatform, + BinaryFile|null $builtBinaryFile, IOInterface $io, bool $attemptToSetupIniFile, ): BinaryFile { @@ -41,7 +44,19 @@ public function __invoke( $sharedObjectName, ); - $makeInstallCommand = ['make', 'install']; + switch (DownloadUrlMethod::fromPackage($downloadedPackage->package, $targetPlatform)) { + case DownloadUrlMethod::PrePackagedBinary: + Assert::notNull($builtBinaryFile); + $installCommand = [ + 'cp', + $builtBinaryFile->filePath, + $targetExtensionPath, + ]; + break; + + default: + $installCommand = ['make', 'install']; + } // If the target directory isn't writable, or a .so file already exists and isn't writable, try to use sudo if ( @@ -55,11 +70,11 @@ public function __invoke( 'Cannot write to %s, so using sudo to elevate privileges.', $targetExtensionPath, )); - array_unshift($makeInstallCommand, Sudo::find()); + array_unshift($installCommand, Sudo::find()); } $makeInstallOutput = Process::run( - $makeInstallCommand, + $installCommand, $downloadedPackage->extractedSourcePath, self::MAKE_INSTALL_TIMEOUT_SECS, ); diff --git a/src/Installing/WindowsInstall.php b/src/Installing/WindowsInstall.php index 97eae97e..e674a561 100644 --- a/src/Installing/WindowsInstall.php +++ b/src/Installing/WindowsInstall.php @@ -37,6 +37,7 @@ public function __construct(private readonly SetupIniFile $setupIniFile) public function __invoke( DownloadedPackage $downloadedPackage, TargetPlatform $targetPlatform, + BinaryFile|null $builtBinaryFile, IOInterface $io, bool $attemptToSetupIniFile, ): BinaryFile { diff --git a/src/Platform/DebugBuild.php b/src/Platform/DebugBuild.php new file mode 100644 index 00000000..691dd50d --- /dev/null +++ b/src/Platform/DebugBuild.php @@ -0,0 +1,12 @@ + */ + public static function packageNames(TargetPlatform $targetPlatform, Package $package): array + { + return [ + strtolower(sprintf( // @todo 436 - confirm naming; check if compatible with existing packages + 'php_%s-%s_php%s-%s-%s-%s-%s.tgz', + $package->extensionName()->name(), + $package->version(), + $targetPlatform->phpBinaryPath->majorMinorVersion(), + $targetPlatform->architecture->name, + LibcFlavour::Gnu->value, // @todo 436 - detect libc flavour + DebugBuild::Debug->value, // @todo 436 - detect debug mode + $targetPlatform->threadSafety->asShort(), + )), + ]; + } +} diff --git a/test/integration/Installing/UnixInstallTest.php b/test/integration/Installing/UnixInstallTest.php index cae63683..f82be3a3 100644 --- a/test/integration/Installing/UnixInstallTest.php +++ b/test/integration/Installing/UnixInstallTest.php @@ -91,7 +91,7 @@ public function testUnixInstallCanInstallExtension(string $phpConfig): void self::TEST_EXTENSION_PATH, ); - (new UnixBuild())->__invoke( + $built = (new UnixBuild())->__invoke( $downloadedPackage, $targetPlatform, ['--enable-pie_test_ext'], @@ -102,6 +102,7 @@ public function testUnixInstallCanInstallExtension(string $phpConfig): void $installedSharedObject = (new UnixInstall(new SetupIniFile(new PickBestSetupIniApproach([]))))->__invoke( $downloadedPackage, $targetPlatform, + $built, $output, true, ); diff --git a/test/integration/Installing/WindowsInstallTest.php b/test/integration/Installing/WindowsInstallTest.php index 086fe026..a4142da6 100644 --- a/test/integration/Installing/WindowsInstallTest.php +++ b/test/integration/Installing/WindowsInstallTest.php @@ -71,7 +71,7 @@ public function testWindowsInstallCanInstallExtension(): void $installer = new WindowsInstall(new SetupIniFile(new PickBestSetupIniApproach([]))); - $installedDll = $installer->__invoke($downloadedPackage, $targetPlatform, $output, true); + $installedDll = $installer->__invoke($downloadedPackage, $targetPlatform, null, $output, true); self::assertSame($extensionPath . '\php_pie_test_ext.dll', $installedDll->filePath); $outputString = $output->getOutput(); From d7f1402a16c49c8240774b76b5aa4584bc3da066 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 31 Dec 2025 14:21:10 +0000 Subject: [PATCH 02/16] 436: handle a list of possible download methods --- resources/composer-json-php-ext-schema.json | 25 +++++- src/Building/UnixBuild.php | 2 +- .../OverrideDownloadUrlInstallListener.php | 85 +++++++++++++------ src/DependencyResolver/Package.php | 26 +++--- src/Downloading/DownloadUrlMethod.php | 40 ++++++--- .../Exception/CouldNotFindReleaseAsset.php | 46 +++++----- src/Installing/UnixInstall.php | 2 +- 7 files changed, 147 insertions(+), 79 deletions(-) diff --git a/resources/composer-json-php-ext-schema.json b/resources/composer-json-php-ext-schema.json index 55ac0820..2162d763 100644 --- a/resources/composer-json-php-ext-schema.json +++ b/resources/composer-json-php-ext-schema.json @@ -41,10 +41,27 @@ "default": null }, "download-url-method": { - "type": "string", - "description": "If specified, this technique will be used to override the URL that PIE uses to download the asset. The default, if not specified, is composer-default.", - "enum": ["composer-default", "pre-packaged-source"], - "example": "composer-default" + "oneOf": [ + { + "type": "string", + "description": "If specified, this technique will be used to override the URL that PIE uses to download the asset. The default, if not specified, is composer-default.", + "deprecated": true, + "enum": ["composer-default", "pre-packaged-source", "pre-packaged-binary"], + "example": "composer-default", + "default": "composer-default" + }, + { + "type": "array", + "description": "Multiple techniques can be specified, in which case PIE will try each in turn until one succeeds. The first technique that succeeds will be used.", + "items": { + "type": "string", + "description": "If specified, this technique will be used to override the URL that PIE uses to download the asset. The default, if not specified, is composer-default.", + "enum": ["composer-default", "pre-packaged-source", "pre-packaged-binary"], + "example": ["pre-packaged-binary", "composer-default"] + }, + "default": ["composer-default"] + } + ] }, "os-families": { "type": "array", diff --git a/src/Building/UnixBuild.php b/src/Building/UnixBuild.php index 6c2cd196..18de92a0 100644 --- a/src/Building/UnixBuild.php +++ b/src/Building/UnixBuild.php @@ -36,7 +36,7 @@ public function __invoke( IOInterface $io, PhpizePath|null $phpizePath, ): BinaryFile { - switch (DownloadUrlMethod::fromPackage($downloadedPackage->package, $targetPlatform)) { + switch (DownloadUrlMethod::fromDownloadedPackage($downloadedPackage)) { case DownloadUrlMethod::PrePackagedBinary: return $this->prePackagedBinary($downloadedPackage, $io); diff --git a/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php b/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php index 12118fb9..24a6a3bc 100644 --- a/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php +++ b/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php @@ -17,6 +17,8 @@ use Php\Pie\Downloading\DownloadUrlMethod; use Php\Pie\Downloading\PackageReleaseAssets; use Psr\Container\ContainerInterface; +use RuntimeException; +use Throwable; use function array_walk; use function pathinfo; @@ -69,38 +71,65 @@ function (OperationInterface $operation): void { return; } - $piePackage = Package::fromComposerCompletePackage($composerPackage); - $targetPlatform = $this->composerRequest->targetPlatform; - $downloadUrlMethod = DownloadUrlMethod::fromPackage($piePackage, $targetPlatform); - - // Exit early if we should just use Composer's normal download - if ($downloadUrlMethod === DownloadUrlMethod::ComposerDefaultDownload) { - return; - } - - $possibleAssetNames = $downloadUrlMethod->possibleAssetNames($piePackage, $targetPlatform); - if ($possibleAssetNames === null) { - return; + $piePackage = Package::fromComposerCompletePackage($composerPackage); + $targetPlatform = $this->composerRequest->targetPlatform; + $downloadUrlMethods = DownloadUrlMethod::possibleDownloadUrlMethodsForPackage($piePackage, $targetPlatform); + + $selectedDownloadUrlMethod = null; + + foreach ($downloadUrlMethods as $downloadUrlMethod) { + $this->io->write('Trying: ' . $downloadUrlMethod->value); // @todo 436 verbosity + + // Exit early if we should just use Composer's normal download + if ($downloadUrlMethod === DownloadUrlMethod::ComposerDefaultDownload) { + $selectedDownloadUrlMethod = $downloadUrlMethod; + break; + } + + try { + $possibleAssetNames = $downloadUrlMethod->possibleAssetNames($piePackage, $targetPlatform); + } catch (Throwable $t) { + $this->io->write('Failed fetching asset names [' . $downloadUrlMethod->value . ']: ' . $t->getMessage()); // @todo 436 verbosity + continue; + } + + if ($possibleAssetNames === null) { + $this->io->write('Failed fetching asset names [' . $downloadUrlMethod->value . ']: No asset names'); // @todo 436 verbosity + continue; + } + + // @todo https://github.com/php/pie/issues/138 will need to depend on the repo type (GH/GL/BB/etc.) + $packageReleaseAssets = $this->container->get(PackageReleaseAssets::class); + + try { + $url = $packageReleaseAssets->findMatchingReleaseAssetUrl( + $targetPlatform, + $piePackage, + new HttpDownloader($this->io, $this->composer->getConfig()), + $possibleAssetNames, + ); + } catch (Throwable $t) { + $this->io->write('Failed locating asset [' . $downloadUrlMethod->value . ']: ' . $t->getMessage()); // @todo 436 verbosity + continue; + } + + $this->composerRequest->pieOutput->write('Found prebuilt archive: ' . $url); + $composerPackage->setDistUrl($url); + + if (pathinfo($url, PATHINFO_EXTENSION) === 'tgz') { + $composerPackage->setDistType('tar'); + } + + $selectedDownloadUrlMethod = $downloadUrlMethod; + break; } - // @todo https://github.com/php/pie/issues/138 will need to depend on the repo type (GH/GL/BB/etc.) - $packageReleaseAssets = $this->container->get(PackageReleaseAssets::class); - - $url = $packageReleaseAssets->findMatchingReleaseAssetUrl( - $targetPlatform, - $piePackage, - new HttpDownloader($this->io, $this->composer->getConfig()), - $possibleAssetNames, - ); - - $this->composerRequest->pieOutput->write('Found prebuilt archive: ' . $url); - $composerPackage->setDistUrl($url); - - if (pathinfo($url, PATHINFO_EXTENSION) !== 'tgz') { - return; + if ($selectedDownloadUrlMethod === null) { + throw new RuntimeException('No download method could be found for ' . $piePackage->name()); // @todo 436 improve message, will need to give more info! } - $composerPackage->setDistType('tar'); + $selectedDownloadUrlMethod->writeToComposerPackage($composerPackage); + $this->io->write('FINALLY SETTLED on using download URL method: ' . $selectedDownloadUrlMethod->value . ''); // @todo 436 verbosity }, ); } diff --git a/src/DependencyResolver/Package.php b/src/DependencyResolver/Package.php index bfd7eba7..d7ed4360 100644 --- a/src/DependencyResolver/Package.php +++ b/src/DependencyResolver/Package.php @@ -42,7 +42,8 @@ final class Package private array|null $incompatibleOsFamilies = null; private bool $supportZts = true; private bool $supportNts = true; - private DownloadUrlMethod|null $downloadUrlMethod = null; + /** @var non-empty-list|null */ + private array|null $supportedDownloadUrlMethods = null; public function __construct( private readonly CompletePackageInterface $composerPackage, @@ -91,17 +92,15 @@ public static function fromComposerCompletePackage(CompletePackageInterface $com $package->priority = $phpExtOptions['priority'] ?? 80; if ($phpExtOptions !== null && array_key_exists('download-url-method', $phpExtOptions)) { - /** @var string|list $method */ - $method = $phpExtOptions['download-url-method']; - if (is_array($method)) { - if (count($method) !== 1) { - throw new InvalidArgumentException('This extension requires a newer version of PIE. Multiple download-url-methods are not supported until PIE 1.4.0.'); - } - - $method = $method[0]; + /** @var string|list $extOptionValue */ + $extOptionValue = $phpExtOptions['download-url-method']; + $methods = is_array($extOptionValue) ? $extOptionValue : [$extOptionValue]; + if (count($methods) > 0) { + $package->supportedDownloadUrlMethods = array_map( + static fn (string $method): DownloadUrlMethod => DownloadUrlMethod::from($method), + $methods, + ); } - - $package->downloadUrlMethod = DownloadUrlMethod::tryFrom($method); } return $package; @@ -231,8 +230,9 @@ public function supportNts(): bool return $this->supportNts; } - public function downloadUrlMethod(): DownloadUrlMethod|null + /** @return non-empty-list|null */ + public function supportedDownloadUrlMethods(): array|null { - return $this->downloadUrlMethod; + return $this->supportedDownloadUrlMethods; } } diff --git a/src/Downloading/DownloadUrlMethod.php b/src/Downloading/DownloadUrlMethod.php index 2ee05fce..2f7e9851 100644 --- a/src/Downloading/DownloadUrlMethod.php +++ b/src/Downloading/DownloadUrlMethod.php @@ -4,6 +4,7 @@ namespace Php\Pie\Downloading; +use Composer\Package\CompletePackageInterface; use Php\Pie\DependencyResolver\Package; use Php\Pie\Platform\OperatingSystem; use Php\Pie\Platform\PrePackagedBinaryAssetName; @@ -11,9 +12,17 @@ use Php\Pie\Platform\TargetPlatform; use Php\Pie\Platform\WindowsExtensionAssetName; +use function array_key_exists; +use function array_merge; +use function assert; +use function is_string; +use function method_exists; + /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ enum DownloadUrlMethod: string { + public const COMPOSER_PACKAGE_EXTRA_KEY = 'download-url-method'; + case ComposerDefaultDownload = 'composer-default'; case WindowsBinaryDownload = 'windows-binary'; case PrePackagedSourceDownload = 'pre-packaged-source'; @@ -30,25 +39,36 @@ public function possibleAssetNames(Package $package, TargetPlatform $targetPlatf }; } - public static function fromPackage(Package $package, TargetPlatform $targetPlatform): self + public static function fromDownloadedPackage(DownloadedPackage $downloadedPackage): self + { + $extra = $downloadedPackage->package->composerPackage()->getExtra(); + + return self::from(array_key_exists(self::COMPOSER_PACKAGE_EXTRA_KEY, $extra) && is_string($extra[self::COMPOSER_PACKAGE_EXTRA_KEY]) ? $extra[self::COMPOSER_PACKAGE_EXTRA_KEY] : ''); + } + + public function writeToComposerPackage(CompletePackageInterface $composerPackage): void + { + assert(method_exists($composerPackage, 'setExtra')); + + $composerPackage->setExtra(array_merge($composerPackage->getExtra(), [self::COMPOSER_PACKAGE_EXTRA_KEY => $this->value])); + } + + /** @return non-empty-list */ + public static function possibleDownloadUrlMethodsForPackage(Package $package, TargetPlatform $targetPlatform): array { /** * PIE does not support building on Windows (yet, at least). Maintainers * should provide pre-built Windows binaries. */ if ($targetPlatform->operatingSystem === OperatingSystem::Windows) { - return self::WindowsBinaryDownload; + return [self::WindowsBinaryDownload]; } - /** - * Some packages pre-package source code (e.g. mongodb) as there are - * external dependencies in Git submodules that otherwise aren't - * included in GitHub/Gitlab/etc "dist" downloads - */ - if ($package->downloadUrlMethod() === DownloadUrlMethod::PrePackagedSourceDownload) { - return self::PrePackagedSourceDownload; + $configuredSupportedMethods = $package->supportedDownloadUrlMethods(); + if ($configuredSupportedMethods === null) { + return [self::ComposerDefaultDownload]; } - return self::ComposerDefaultDownload; + return $configuredSupportedMethods; } } diff --git a/src/Downloading/Exception/CouldNotFindReleaseAsset.php b/src/Downloading/Exception/CouldNotFindReleaseAsset.php index 815086bd..fed62237 100644 --- a/src/Downloading/Exception/CouldNotFindReleaseAsset.php +++ b/src/Downloading/Exception/CouldNotFindReleaseAsset.php @@ -19,16 +19,17 @@ class CouldNotFindReleaseAsset extends RuntimeException /** @param non-empty-list $expectedAssetNames */ public static function forPackage(TargetPlatform $targetPlatform, Package $package, array $expectedAssetNames): self { - $downloadUrlMethod = DownloadUrlMethod::fromPackage($package, $targetPlatform); - - if ($downloadUrlMethod === DownloadUrlMethod::WindowsBinaryDownload) { - return new self(sprintf( - 'Windows archive with prebuilt extension for %s was not attached on release %s - looked for one of "%s"', - $package->name(), - $package->version(), - implode(', ', $expectedAssetNames), - )); - } + // @todo 436 - add this back in +// $downloadUrlMethod = DownloadUrlMethod::fromPackage($package, $targetPlatform); +// +// if ($downloadUrlMethod === DownloadUrlMethod::WindowsBinaryDownload) { +// return new self(sprintf( +// 'Windows archive with prebuilt extension for %s was not attached on release %s - looked for one of "%s"', +// $package->name(), +// $package->version(), +// implode(', ', $expectedAssetNames), +// )); +// } return new self(sprintf( 'Could not find release asset for %s named one of "%s"', @@ -39,18 +40,19 @@ public static function forPackage(TargetPlatform $targetPlatform, Package $packa public static function forPackageWithMissingTag(Package $package): self { - if ( - $package->downloadUrlMethod() === DownloadUrlMethod::PrePackagedSourceDownload - && $package->composerPackage()->isDev() - ) { - return new self(sprintf( - 'The package %s uses pre-packaged source archives, which are not available for branch aliases such as %s. You should either omit the version constraint to use the latest compatible version, or use a tagged version instead. You can find a list of tagged versions on:%shttps://packagist.org/packages/%s', - $package->name(), - $package->version(), - PHP_EOL . PHP_EOL, - $package->name(), - )); - } + // @todo 436 - add this back in +// if ( +// $package->downloadUrlMethod() === DownloadUrlMethod::PrePackagedSourceDownload +// && $package->composerPackage()->isDev() +// ) { +// return new self(sprintf( +// 'The package %s uses pre-packaged source archives, which are not available for branch aliases such as %s. You should either omit the version constraint to use the latest compatible version, or use a tagged version instead. You can find a list of tagged versions on:%shttps://packagist.org/packages/%s', +// $package->name(), +// $package->version(), +// PHP_EOL . PHP_EOL, +// $package->name(), +// )); +// } return new self(sprintf( 'Could not find release by tag name for %s', diff --git a/src/Installing/UnixInstall.php b/src/Installing/UnixInstall.php index d63af4ec..94099122 100644 --- a/src/Installing/UnixInstall.php +++ b/src/Installing/UnixInstall.php @@ -44,7 +44,7 @@ public function __invoke( $sharedObjectName, ); - switch (DownloadUrlMethod::fromPackage($downloadedPackage->package, $targetPlatform)) { + switch (DownloadUrlMethod::fromDownloadedPackage($downloadedPackage)) { case DownloadUrlMethod::PrePackagedBinary: Assert::notNull($builtBinaryFile); $installCommand = [ From db3f7fe215e8725c716bbd9a6f0be8c2bddcac62 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Fri, 2 Jan 2026 14:21:40 +0000 Subject: [PATCH 03/16] 436: add back in downloadUrlMethod-specific exception messages --- .../OverrideDownloadUrlInstallListener.php | 1 + .../Exception/CouldNotFindReleaseAsset.php | 48 +++++++++---------- .../GithubPackageReleaseAssets.php | 10 ++-- src/Downloading/PackageReleaseAssets.php | 1 + 4 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php b/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php index 24a6a3bc..9970f636 100644 --- a/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php +++ b/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php @@ -106,6 +106,7 @@ function (OperationInterface $operation): void { $targetPlatform, $piePackage, new HttpDownloader($this->io, $this->composer->getConfig()), + $downloadUrlMethod, $possibleAssetNames, ); } catch (Throwable $t) { diff --git a/src/Downloading/Exception/CouldNotFindReleaseAsset.php b/src/Downloading/Exception/CouldNotFindReleaseAsset.php index fed62237..e52d0101 100644 --- a/src/Downloading/Exception/CouldNotFindReleaseAsset.php +++ b/src/Downloading/Exception/CouldNotFindReleaseAsset.php @@ -17,19 +17,16 @@ class CouldNotFindReleaseAsset extends RuntimeException { /** @param non-empty-list $expectedAssetNames */ - public static function forPackage(TargetPlatform $targetPlatform, Package $package, array $expectedAssetNames): self + public static function forPackage(TargetPlatform $targetPlatform, Package $package, DownloadUrlMethod $downloadUrlMethod, array $expectedAssetNames): self { - // @todo 436 - add this back in -// $downloadUrlMethod = DownloadUrlMethod::fromPackage($package, $targetPlatform); -// -// if ($downloadUrlMethod === DownloadUrlMethod::WindowsBinaryDownload) { -// return new self(sprintf( -// 'Windows archive with prebuilt extension for %s was not attached on release %s - looked for one of "%s"', -// $package->name(), -// $package->version(), -// implode(', ', $expectedAssetNames), -// )); -// } + if ($downloadUrlMethod === DownloadUrlMethod::WindowsBinaryDownload) { + return new self(sprintf( + 'Windows archive with prebuilt extension for %s was not attached on release %s - looked for one of "%s"', + $package->name(), + $package->version(), + implode(', ', $expectedAssetNames), + )); + } return new self(sprintf( 'Could not find release asset for %s named one of "%s"', @@ -38,21 +35,20 @@ public static function forPackage(TargetPlatform $targetPlatform, Package $packa )); } - public static function forPackageWithMissingTag(Package $package): self + public static function forPackageWithMissingTag(Package $package, DownloadUrlMethod $downloadUrlMethod): self { - // @todo 436 - add this back in -// if ( -// $package->downloadUrlMethod() === DownloadUrlMethod::PrePackagedSourceDownload -// && $package->composerPackage()->isDev() -// ) { -// return new self(sprintf( -// 'The package %s uses pre-packaged source archives, which are not available for branch aliases such as %s. You should either omit the version constraint to use the latest compatible version, or use a tagged version instead. You can find a list of tagged versions on:%shttps://packagist.org/packages/%s', -// $package->name(), -// $package->version(), -// PHP_EOL . PHP_EOL, -// $package->name(), -// )); -// } + if ( + $downloadUrlMethod === DownloadUrlMethod::PrePackagedSourceDownload + && $package->composerPackage()->isDev() + ) { + return new self(sprintf( + 'The package %s uses pre-packaged source archives, which are not available for branch aliases such as %s. You should either omit the version constraint to use the latest compatible version, or use a tagged version instead. You can find a list of tagged versions on:%shttps://packagist.org/packages/%s', + $package->name(), + $package->version(), + PHP_EOL . PHP_EOL, + $package->name(), + )); + } return new self(sprintf( 'Could not find release by tag name for %s', diff --git a/src/Downloading/GithubPackageReleaseAssets.php b/src/Downloading/GithubPackageReleaseAssets.php index 04c7aed9..ad1bd13a 100644 --- a/src/Downloading/GithubPackageReleaseAssets.php +++ b/src/Downloading/GithubPackageReleaseAssets.php @@ -31,12 +31,14 @@ public function findMatchingReleaseAssetUrl( TargetPlatform $targetPlatform, Package $package, HttpDownloader $httpDownloader, + DownloadUrlMethod $downloadUrlMethod, array $possibleReleaseAssetNames, ): string { $releaseAsset = $this->selectMatchingReleaseAsset( $targetPlatform, $package, - $this->getReleaseAssetsForPackage($package, $httpDownloader), + $this->getReleaseAssetsForPackage($package, $httpDownloader, $downloadUrlMethod), + $downloadUrlMethod, $possibleReleaseAssetNames, ); @@ -56,6 +58,7 @@ private function selectMatchingReleaseAsset( TargetPlatform $targetPlatform, Package $package, array $releaseAssets, + DownloadUrlMethod $downloadUrlMethod, array $possibleReleaseAssetNames, ): array { foreach ($releaseAssets as $releaseAsset) { @@ -64,13 +67,14 @@ private function selectMatchingReleaseAsset( } } - throw Exception\CouldNotFindReleaseAsset::forPackage($targetPlatform, $package, $possibleReleaseAssetNames); + throw Exception\CouldNotFindReleaseAsset::forPackage($targetPlatform, $package, $downloadUrlMethod, $possibleReleaseAssetNames); } /** @return list */ private function getReleaseAssetsForPackage( Package $package, HttpDownloader $httpDownloader, + DownloadUrlMethod $downloadUrlMethod, ): array { Assert::notNull($package->downloadUrl()); @@ -88,7 +92,7 @@ private function getReleaseAssetsForPackage( } catch (TransportException $t) { /** @link https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#get-a-release-by-tag-name */ if ($t->getStatusCode() === 404) { - throw Exception\CouldNotFindReleaseAsset::forPackageWithMissingTag($package); + throw Exception\CouldNotFindReleaseAsset::forPackageWithMissingTag($package, $downloadUrlMethod); } throw $t; diff --git a/src/Downloading/PackageReleaseAssets.php b/src/Downloading/PackageReleaseAssets.php index 87a903f2..622bebdb 100644 --- a/src/Downloading/PackageReleaseAssets.php +++ b/src/Downloading/PackageReleaseAssets.php @@ -20,6 +20,7 @@ public function findMatchingReleaseAssetUrl( TargetPlatform $targetPlatform, Package $package, HttpDownloader $httpDownloader, + DownloadUrlMethod $downloadUrlMethod, array $possibleReleaseAssetNames, ): string; } From a9e720801d698abd8b2576e78af72013c8cf3274 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Fri, 2 Jan 2026 14:43:02 +0000 Subject: [PATCH 04/16] 436: updating tests for multiple download URL methods support --- phpstan-baseline.neon | 10 +- src/DependencyResolver/Package.php | 6 +- src/Downloading/DownloadUrlMethod.php | 2 +- test/integration/Building/UnixBuildTest.php | 35 +++++- .../GithubPackageReleaseAssetsTest.php | 2 + .../Installing/UnixInstallTest.php | 11 +- .../Downloading/DownloadUrlMethodTest.php | 107 +++++++++++++++++- .../CouldNotFindReleaseAssetTest.php | 5 +- .../GithubPackageReleaseAssetsTest.php | 4 + 9 files changed, 162 insertions(+), 20 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 68ee1b1e..1299aa47 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -175,31 +175,31 @@ parameters: path: src/DependencyResolver/Package.php - - message: '#^@readonly property Php\\Pie\\DependencyResolver\\Package\:\:\$downloadUrlMethod is assigned outside of the constructor\.$#' + message: '#^@readonly property Php\\Pie\\DependencyResolver\\Package\:\:\$incompatibleOsFamilies is assigned outside of the constructor\.$#' identifier: property.readOnlyByPhpDocAssignNotInConstructor count: 1 path: src/DependencyResolver/Package.php - - message: '#^@readonly property Php\\Pie\\DependencyResolver\\Package\:\:\$incompatibleOsFamilies is assigned outside of the constructor\.$#' + message: '#^@readonly property Php\\Pie\\DependencyResolver\\Package\:\:\$priority is assigned outside of the constructor\.$#' identifier: property.readOnlyByPhpDocAssignNotInConstructor count: 1 path: src/DependencyResolver/Package.php - - message: '#^@readonly property Php\\Pie\\DependencyResolver\\Package\:\:\$priority is assigned outside of the constructor\.$#' + message: '#^@readonly property Php\\Pie\\DependencyResolver\\Package\:\:\$supportNts is assigned outside of the constructor\.$#' identifier: property.readOnlyByPhpDocAssignNotInConstructor count: 1 path: src/DependencyResolver/Package.php - - message: '#^@readonly property Php\\Pie\\DependencyResolver\\Package\:\:\$supportNts is assigned outside of the constructor\.$#' + message: '#^@readonly property Php\\Pie\\DependencyResolver\\Package\:\:\$supportZts is assigned outside of the constructor\.$#' identifier: property.readOnlyByPhpDocAssignNotInConstructor count: 1 path: src/DependencyResolver/Package.php - - message: '#^@readonly property Php\\Pie\\DependencyResolver\\Package\:\:\$supportZts is assigned outside of the constructor\.$#' + message: '#^@readonly property Php\\Pie\\DependencyResolver\\Package\:\:\$supportedDownloadUrlMethods is assigned outside of the constructor\.$#' identifier: property.readOnlyByPhpDocAssignNotInConstructor count: 1 path: src/DependencyResolver/Package.php diff --git a/src/DependencyResolver/Package.php b/src/DependencyResolver/Package.php index d7ed4360..154c01ec 100644 --- a/src/DependencyResolver/Package.php +++ b/src/DependencyResolver/Package.php @@ -39,9 +39,9 @@ final class Package /** @var non-empty-list|null */ private array|null $compatibleOsFamilies = null; /** @var non-empty-list|null */ - private array|null $incompatibleOsFamilies = null; - private bool $supportZts = true; - private bool $supportNts = true; + private array|null $incompatibleOsFamilies = null; + private bool $supportZts = true; + private bool $supportNts = true; /** @var non-empty-list|null */ private array|null $supportedDownloadUrlMethods = null; diff --git a/src/Downloading/DownloadUrlMethod.php b/src/Downloading/DownloadUrlMethod.php index 2f7e9851..9c42cefe 100644 --- a/src/Downloading/DownloadUrlMethod.php +++ b/src/Downloading/DownloadUrlMethod.php @@ -21,7 +21,7 @@ /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ enum DownloadUrlMethod: string { - public const COMPOSER_PACKAGE_EXTRA_KEY = 'download-url-method'; + private const COMPOSER_PACKAGE_EXTRA_KEY = 'download-url-method'; case ComposerDefaultDownload = 'composer-default'; case WindowsBinaryDownload = 'windows-binary'; diff --git a/test/integration/Building/UnixBuildTest.php b/test/integration/Building/UnixBuildTest.php index 73d1c80f..1f4c41e4 100644 --- a/test/integration/Building/UnixBuildTest.php +++ b/test/integration/Building/UnixBuildTest.php @@ -11,6 +11,7 @@ use Php\Pie\Building\UnixBuild; use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\Downloading\DownloadUrlMethod; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; use Php\Pie\Platform\TargetPhp\PhpBinaryPath; @@ -25,7 +26,8 @@ #[CoversClass(UnixBuild::class)] final class UnixBuildTest extends TestCase { - private const TEST_EXTENSION_PATH = __DIR__ . '/../../assets/pie_test_ext'; + private const COMPOSER_PACKAGE_EXTRA_KEY = 'download-url-method'; + private const TEST_EXTENSION_PATH = __DIR__ . '/../../assets/pie_test_ext'; public function testUnixBuildCanBuildExtension(): void { @@ -35,9 +37,14 @@ public function testUnixBuildCanBuildExtension(): void $output = new BufferIO(); + $composerPackage = $this->createMock(CompletePackageInterface::class); + $composerPackage + ->method('getExtra') + ->willReturn([self::COMPOSER_PACKAGE_EXTRA_KEY => DownloadUrlMethod::ComposerDefaultDownload->value]); + $downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( new Package( - $this->createMock(CompletePackageInterface::class), + $composerPackage, ExtensionType::PhpModule, ExtensionName::normaliseFromString('pie_test_ext'), 'pie_test_ext', @@ -84,9 +91,14 @@ public function testUnixBuildWillThrowExceptionWhenExpectedBinaryNameMismatches( $output = new BufferIO(); + $composerPackage = $this->createMock(CompletePackageInterface::class); + $composerPackage + ->method('getExtra') + ->willReturn([self::COMPOSER_PACKAGE_EXTRA_KEY => DownloadUrlMethod::ComposerDefaultDownload->value]); + $downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( new Package( - $this->createMock(CompletePackageInterface::class), + $composerPackage, ExtensionType::PhpModule, ExtensionName::normaliseFromString('mismatched_name'), 'pie_test_ext', @@ -126,6 +138,9 @@ public function testUnixBuildCanBuildExtensionWithBuildPath(): void $composerPackage->method('getPrettyVersion')->willReturn('0.1.0'); $composerPackage->method('getType')->willReturn('php-ext'); $composerPackage->method('getPhpExt')->willReturn(['build-path' => 'pie_test_ext']); + $composerPackage + ->method('getExtra') + ->willReturn([self::COMPOSER_PACKAGE_EXTRA_KEY => DownloadUrlMethod::ComposerDefaultDownload->value]); $downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( Package::fromComposerCompletePackage($composerPackage), @@ -172,9 +187,14 @@ public function testCleanupDoesNotCleanWhenConfigureIsMissing(): void $output = new BufferIO(verbosity: OutputInterface::VERBOSITY_VERBOSE); + $composerPackage = $this->createMock(CompletePackageInterface::class); + $composerPackage + ->method('getExtra') + ->willReturn([self::COMPOSER_PACKAGE_EXTRA_KEY => DownloadUrlMethod::ComposerDefaultDownload->value]); + $downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( new Package( - $this->createMock(CompletePackageInterface::class), + $composerPackage, ExtensionType::PhpModule, ExtensionName::normaliseFromString('pie_test_ext'), 'pie_test_ext', @@ -209,9 +229,14 @@ public function testVerboseOutputShowsCleanupMessages(): void $output = new BufferIO(verbosity: OutputInterface::VERBOSITY_VERBOSE); + $composerPackage = $this->createMock(CompletePackageInterface::class); + $composerPackage + ->method('getExtra') + ->willReturn([self::COMPOSER_PACKAGE_EXTRA_KEY => DownloadUrlMethod::ComposerDefaultDownload->value]); + $downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( new Package( - $this->createMock(CompletePackageInterface::class), + $composerPackage, ExtensionType::PhpModule, ExtensionName::normaliseFromString('pie_test_ext'), 'pie_test_ext', diff --git a/test/integration/Downloading/GithubPackageReleaseAssetsTest.php b/test/integration/Downloading/GithubPackageReleaseAssetsTest.php index cd9cb4b3..f26b0ef7 100644 --- a/test/integration/Downloading/GithubPackageReleaseAssetsTest.php +++ b/test/integration/Downloading/GithubPackageReleaseAssetsTest.php @@ -9,6 +9,7 @@ use Composer\Package\CompletePackageInterface; use Composer\Util\HttpDownloader; use Php\Pie\DependencyResolver\Package; +use Php\Pie\Downloading\DownloadUrlMethod; use Php\Pie\Downloading\GithubPackageReleaseAssets; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; @@ -65,6 +66,7 @@ public function testDeterminingReleaseAssetUrlForWindows(): void $targetPlatform, $package, new HttpDownloader($io, $config), + DownloadUrlMethod::WindowsBinaryDownload, WindowsExtensionAssetName::zipNames( $targetPlatform, $package, diff --git a/test/integration/Installing/UnixInstallTest.php b/test/integration/Installing/UnixInstallTest.php index f82be3a3..cd045421 100644 --- a/test/integration/Installing/UnixInstallTest.php +++ b/test/integration/Installing/UnixInstallTest.php @@ -10,6 +10,7 @@ use Php\Pie\Building\UnixBuild; use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\Downloading\DownloadUrlMethod; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; use Php\Pie\Installing\Ini\PickBestSetupIniApproach; @@ -34,7 +35,8 @@ #[CoversClass(UnixInstall::class)] final class UnixInstallTest extends TestCase { - private const TEST_EXTENSION_PATH = __DIR__ . '/../../assets/pie_test_ext'; + private const COMPOSER_PACKAGE_EXTRA_KEY = 'download-url-method'; + private const TEST_EXTENSION_PATH = __DIR__ . '/../../assets/pie_test_ext'; /** @return array */ public static function phpPathProvider(): array @@ -79,9 +81,14 @@ public function testUnixInstallCanInstallExtension(string $phpConfig): void $targetPlatform = TargetPlatform::fromPhpBinaryPath(PhpBinaryPath::fromPhpConfigExecutable($phpConfig), null); $extensionPath = $targetPlatform->phpBinaryPath->extensionPath(); + $composerPackage = $this->createMock(CompletePackageInterface::class); + $composerPackage + ->method('getExtra') + ->willReturn([self::COMPOSER_PACKAGE_EXTRA_KEY => DownloadUrlMethod::ComposerDefaultDownload->value]); + $downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( new Package( - $this->createMock(CompletePackageInterface::class), + $composerPackage, ExtensionType::PhpModule, ExtensionName::normaliseFromString('pie_test_ext'), 'pie_test_ext', diff --git a/test/unit/Downloading/DownloadUrlMethodTest.php b/test/unit/Downloading/DownloadUrlMethodTest.php index 26712bec..418c33cd 100644 --- a/test/unit/Downloading/DownloadUrlMethodTest.php +++ b/test/unit/Downloading/DownloadUrlMethodTest.php @@ -19,6 +19,8 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; +use function array_key_first; + #[CoversClass(DownloadUrlMethod::class)] final class DownloadUrlMethodTest extends TestCase { @@ -48,7 +50,10 @@ public function testWindowsPackages(): void WindowsCompiler::VC15, ); - $downloadUrlMethod = DownloadUrlMethod::fromPackage($package, $targetPlatform); + $downloadUrlMethods = DownloadUrlMethod::possibleDownloadUrlMethodsForPackage($package, $targetPlatform); + + self::assertCount(1, $downloadUrlMethods); + $downloadUrlMethod = $downloadUrlMethods[array_key_first($downloadUrlMethods)]; self::assertSame(DownloadUrlMethod::WindowsBinaryDownload, $downloadUrlMethod); @@ -81,7 +86,10 @@ public function testPrePackagedSourceDownloads(): void null, ); - $downloadUrlMethod = DownloadUrlMethod::fromPackage($package, $targetPlatform); + $downloadUrlMethods = DownloadUrlMethod::possibleDownloadUrlMethodsForPackage($package, $targetPlatform); + + self::assertCount(1, $downloadUrlMethods); + $downloadUrlMethod = $downloadUrlMethods[array_key_first($downloadUrlMethods)]; self::assertSame(DownloadUrlMethod::PrePackagedSourceDownload, $downloadUrlMethod); @@ -95,6 +103,44 @@ public function testPrePackagedSourceDownloads(): void ); } + public function testPrePackagedBinaryDownloads(): void + { + $composerPackage = $this->createMock(CompletePackageInterface::class); + $composerPackage->method('getPrettyName')->willReturn('foo/bar'); + $composerPackage->method('getPrettyVersion')->willReturn('1.2.3'); + $composerPackage->method('getType')->willReturn('php-ext'); + $composerPackage->method('getPhpExt')->willReturn(['download-url-method' => ['pre-packaged-binary']]); + + $package = Package::fromComposerCompletePackage($composerPackage); + + $phpBinaryPath = $this->createMock(PhpBinaryPath::class); + $phpBinaryPath->expects(self::any()) + ->method('majorMinorVersion') + ->willReturn('8.3'); + + $targetPlatform = new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + $phpBinaryPath, + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + null, + ); + + $downloadUrlMethods = DownloadUrlMethod::possibleDownloadUrlMethodsForPackage($package, $targetPlatform); + + self::assertCount(1, $downloadUrlMethods); + $downloadUrlMethod = $downloadUrlMethods[array_key_first($downloadUrlMethods)]; + + self::assertSame(DownloadUrlMethod::PrePackagedBinary, $downloadUrlMethod); + + self::assertSame( + ['php_bar-1.2.3_php8.3-x86_64-glibc-debug-nts.tgz'], + $downloadUrlMethod->possibleAssetNames($package, $targetPlatform), + ); + } + public function testComposerDefaultDownload(): void { $package = new Package( @@ -116,10 +162,65 @@ public function testComposerDefaultDownload(): void null, ); - $downloadUrlMethod = DownloadUrlMethod::fromPackage($package, $targetPlatform); + $downloadUrlMethods = DownloadUrlMethod::possibleDownloadUrlMethodsForPackage($package, $targetPlatform); + + self::assertCount(1, $downloadUrlMethods); + $downloadUrlMethod = $downloadUrlMethods[array_key_first($downloadUrlMethods)]; self::assertSame(DownloadUrlMethod::ComposerDefaultDownload, $downloadUrlMethod); self::assertNull($downloadUrlMethod->possibleAssetNames($package, $targetPlatform)); } + + public function testMultipleDownloadUrlMethods(): void + { + $composerPackage = $this->createMock(CompletePackageInterface::class); + $composerPackage->method('getPrettyName')->willReturn('foo/bar'); + $composerPackage->method('getPrettyVersion')->willReturn('1.2.3'); + $composerPackage->method('getType')->willReturn('php-ext'); + $composerPackage->method('getPhpExt')->willReturn(['download-url-method' => ['pre-packaged-binary', 'pre-packaged-source', 'composer-default']]); + + $package = Package::fromComposerCompletePackage($composerPackage); + + $phpBinaryPath = $this->createMock(PhpBinaryPath::class); + $phpBinaryPath->expects(self::any()) + ->method('majorMinorVersion') + ->willReturn('8.3'); + + $targetPlatform = new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + $phpBinaryPath, + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + null, + ); + + $downloadUrlMethods = DownloadUrlMethod::possibleDownloadUrlMethodsForPackage($package, $targetPlatform); + + self::assertCount(3, $downloadUrlMethods); + + $firstMethod = $downloadUrlMethods[0]; + self::assertSame(DownloadUrlMethod::PrePackagedBinary, $firstMethod); + self::assertSame( + ['php_bar-1.2.3_php8.3-x86_64-glibc-debug-nts.tgz'], + $firstMethod->possibleAssetNames($package, $targetPlatform), + ); + + $secondMethod = $downloadUrlMethods[1]; + self::assertSame(DownloadUrlMethod::PrePackagedSourceDownload, $secondMethod); + self::assertSame( + [ + 'php_bar-1.2.3-src.tgz', + 'php_bar-1.2.3-src.zip', + 'bar-1.2.3.tgz', + ], + $secondMethod->possibleAssetNames($package, $targetPlatform), + ); + + $thirdMethod = $downloadUrlMethods[2]; + self::assertSame(DownloadUrlMethod::ComposerDefaultDownload, $thirdMethod); + self::assertNull($thirdMethod->possibleAssetNames($package, $targetPlatform)); + } } diff --git a/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php b/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php index d7b153d3..32bd161c 100644 --- a/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php +++ b/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php @@ -6,6 +6,7 @@ use Composer\Package\CompletePackageInterface; use Php\Pie\DependencyResolver\Package; +use Php\Pie\Downloading\DownloadUrlMethod; use Php\Pie\Downloading\Exception\CouldNotFindReleaseAsset; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; @@ -44,6 +45,7 @@ public function testForPackageWithRegularPackage(): void null, ), $package, + DownloadUrlMethod::PrePackagedSourceDownload, ['something.zip', 'something2.zip'], ); @@ -72,6 +74,7 @@ public function testForPackageWithWindowsPackage(): void WindowsCompiler::VS17, ), $package, + DownloadUrlMethod::WindowsBinaryDownload, ['something.zip', 'something2.zip'], ); @@ -89,7 +92,7 @@ public function testForPackageWithMissingTag(): void null, ); - $exception = CouldNotFindReleaseAsset::forPackageWithMissingTag($package); + $exception = CouldNotFindReleaseAsset::forPackageWithMissingTag($package, DownloadUrlMethod::PrePackagedSourceDownload); self::assertSame('Could not find release by tag name for foo/bar:1.2.3', $exception->getMessage()); } diff --git a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php index eb2985fd..4002a366 100644 --- a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php +++ b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php @@ -9,6 +9,7 @@ use Composer\Util\Http\Response; use Composer\Util\HttpDownloader; use Php\Pie\DependencyResolver\Package; +use Php\Pie\Downloading\DownloadUrlMethod; use Php\Pie\Downloading\Exception\CouldNotFindReleaseAsset; use Php\Pie\Downloading\GithubPackageReleaseAssets; use Php\Pie\ExtensionName; @@ -86,6 +87,7 @@ public function testUrlIsReturnedWhenFindingWindowsDownloadUrl(): void $targetPlatform, $package, $httpDownloader, + DownloadUrlMethod::WindowsBinaryDownload, WindowsExtensionAssetName::zipNames( $targetPlatform, $package, @@ -151,6 +153,7 @@ public function testUrlIsReturnedWhenFindingWindowsDownloadUrlWithCompilerAndThr $targetPlatform, $package, $httpDownloader, + DownloadUrlMethod::WindowsBinaryDownload, WindowsExtensionAssetName::zipNames( $targetPlatform, $package, @@ -196,6 +199,7 @@ public function testFindWindowsDownloadUrlForPackageThrowsExceptionWhenAssetNotF $targetPlatform, $package, $httpDownloader, + DownloadUrlMethod::WindowsBinaryDownload, WindowsExtensionAssetName::zipNames( $targetPlatform, $package, From a000e4e07e07c8f8aca143b42710bbd3d7018a7d Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 7 Jan 2026 15:00:09 +0000 Subject: [PATCH 05/16] 436: detect which flavour of glibc is present --- src/Platform/LibcFlavour.php | 26 +++++++++++++++++++++ src/Platform/PrePackagedBinaryAssetName.php | 2 +- src/Platform/TargetPlatform.php | 11 +++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/Platform/LibcFlavour.php b/src/Platform/LibcFlavour.php index a9a89e79..b12bfa66 100644 --- a/src/Platform/LibcFlavour.php +++ b/src/Platform/LibcFlavour.php @@ -4,9 +4,35 @@ namespace Php\Pie\Platform; +use Php\Pie\Util\Process; +use Symfony\Component\Process\ExecutableFinder; +use Throwable; + +use function str_contains; + /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ enum LibcFlavour: string { case Gnu = 'glibc'; case Musl = 'musl'; + + public static function detect(): self + { + $executableFinder = new ExecutableFinder(); + + $lddPath = $executableFinder->find('ldd'); + $lsPath = $executableFinder->find('ls'); + + if ($lddPath === null || $lsPath === null) { + return self::Gnu; + } + + try { + $linkResult = Process::run([$lddPath, $lsPath]); + } catch (Throwable) { + return self::Gnu; + } + + return str_contains($linkResult, 'musl') ? self::Musl : self::Gnu; + } } diff --git a/src/Platform/PrePackagedBinaryAssetName.php b/src/Platform/PrePackagedBinaryAssetName.php index 8fcddfee..4c94eeb5 100644 --- a/src/Platform/PrePackagedBinaryAssetName.php +++ b/src/Platform/PrePackagedBinaryAssetName.php @@ -26,7 +26,7 @@ public static function packageNames(TargetPlatform $targetPlatform, Package $pac $package->version(), $targetPlatform->phpBinaryPath->majorMinorVersion(), $targetPlatform->architecture->name, - LibcFlavour::Gnu->value, // @todo 436 - detect libc flavour + $targetPlatform->libcFlavour()->value, DebugBuild::Debug->value, // @todo 436 - detect debug mode $targetPlatform->threadSafety->asShort(), )), diff --git a/src/Platform/TargetPlatform.php b/src/Platform/TargetPlatform.php index c6cf5f62..05b78cad 100644 --- a/src/Platform/TargetPlatform.php +++ b/src/Platform/TargetPlatform.php @@ -21,6 +21,8 @@ */ class TargetPlatform { + private static LibcFlavour|null $libcFlavour; + public function __construct( public readonly OperatingSystem $operatingSystem, public readonly OperatingSystemFamily $operatingSystemFamily, @@ -32,6 +34,15 @@ public function __construct( ) { } + public function libcFlavour(): LibcFlavour + { + if (! isset(self::$libcFlavour)) { + self::$libcFlavour = LibcFlavour::detect(); + } + + return self::$libcFlavour; + } + public static function isRunningAsRoot(): bool { return function_exists('posix_getuid') && posix_getuid() === 0; From 6163e04e21d55c59c1331dfa7e16a0d2787cd183 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 7 Jan 2026 15:04:13 +0000 Subject: [PATCH 06/16] 436: detect if PHP is in Debug mode --- src/Platform/PrePackagedBinaryAssetName.php | 2 +- src/Platform/TargetPhp/PhpBinaryPath.php | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Platform/PrePackagedBinaryAssetName.php b/src/Platform/PrePackagedBinaryAssetName.php index 4c94eeb5..6f03943b 100644 --- a/src/Platform/PrePackagedBinaryAssetName.php +++ b/src/Platform/PrePackagedBinaryAssetName.php @@ -27,7 +27,7 @@ public static function packageNames(TargetPlatform $targetPlatform, Package $pac $targetPlatform->phpBinaryPath->majorMinorVersion(), $targetPlatform->architecture->name, $targetPlatform->libcFlavour()->value, - DebugBuild::Debug->value, // @todo 436 - detect debug mode + $targetPlatform->phpBinaryPath->debugMode()->value, $targetPlatform->threadSafety->asShort(), )), ]; diff --git a/src/Platform/TargetPhp/PhpBinaryPath.php b/src/Platform/TargetPhp/PhpBinaryPath.php index c6abc55f..32108921 100644 --- a/src/Platform/TargetPhp/PhpBinaryPath.php +++ b/src/Platform/TargetPhp/PhpBinaryPath.php @@ -9,6 +9,7 @@ use Composer\Util\Platform; use Php\Pie\ExtensionName; use Php\Pie\Platform\Architecture; +use Php\Pie\Platform\DebugBuild; use Php\Pie\Platform\OperatingSystem; use Php\Pie\Platform\OperatingSystemFamily; use Php\Pie\Util\Process; @@ -89,6 +90,18 @@ public function phpApiVersion(): string throw new RuntimeException('Failed to find PHP API version...'); } + public function debugMode(): DebugBuild + { + if ( + preg_match('/Debug Build([ =>\t]*)(.*)/', $this->phpinfo(), $m) + && $m[2] !== '' + ) { + return $m[2] === 'yes' ? DebugBuild::Debug : DebugBuild::NoDebug; + } + + throw new RuntimeException('Failed to find PHP API version...'); + } + /** @return non-empty-string */ public function extensionPath(): string { From 694ce3e189e6e94f4de6ca4d24156ad5c1b0c1c4 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 7 Jan 2026 16:40:34 +0000 Subject: [PATCH 07/16] 436: define formats for pre-packaged binary asset names --- src/Platform/PrePackagedBinaryAssetName.php | 44 +++++- .../PrePackagedBinaryAssetNameTest.php | 136 ++++++++++++++++++ 2 files changed, 174 insertions(+), 6 deletions(-) create mode 100644 test/unit/Platform/PrePackagedBinaryAssetNameTest.php diff --git a/src/Platform/PrePackagedBinaryAssetName.php b/src/Platform/PrePackagedBinaryAssetName.php index 6f03943b..2332ba25 100644 --- a/src/Platform/PrePackagedBinaryAssetName.php +++ b/src/Platform/PrePackagedBinaryAssetName.php @@ -6,6 +6,8 @@ use Php\Pie\DependencyResolver\Package; +use function array_unique; +use function array_values; use function sprintf; use function strtolower; @@ -19,17 +21,47 @@ private function __construct() /** @return non-empty-list */ public static function packageNames(TargetPlatform $targetPlatform, Package $package): array { - return [ - strtolower(sprintf( // @todo 436 - confirm naming; check if compatible with existing packages - 'php_%s-%s_php%s-%s-%s-%s-%s.tgz', + return array_values(array_unique([ + strtolower(sprintf( + 'php_%s-%s_php%s-%s-%s%s%s.zip', $package->extensionName()->name(), $package->version(), $targetPlatform->phpBinaryPath->majorMinorVersion(), $targetPlatform->architecture->name, $targetPlatform->libcFlavour()->value, - $targetPlatform->phpBinaryPath->debugMode()->value, - $targetPlatform->threadSafety->asShort(), + $targetPlatform->phpBinaryPath->debugMode() === DebugBuild::Debug ? '-debug' : '', + $targetPlatform->threadSafety === ThreadSafetyMode::ThreadSafe ? '-zts' : '', )), - ]; + strtolower(sprintf( + 'php_%s-%s_php%s-%s-%s%s%s.tgz', + $package->extensionName()->name(), + $package->version(), + $targetPlatform->phpBinaryPath->majorMinorVersion(), + $targetPlatform->architecture->name, + $targetPlatform->libcFlavour()->value, + $targetPlatform->phpBinaryPath->debugMode() === DebugBuild::Debug ? '-debug' : '', + $targetPlatform->threadSafety === ThreadSafetyMode::ThreadSafe ? '-zts' : '', + )), + strtolower(sprintf( + 'php_%s-%s_php%s-%s-%s%s%s.zip', + $package->extensionName()->name(), + $package->version(), + $targetPlatform->phpBinaryPath->majorMinorVersion(), + $targetPlatform->architecture->name, + $targetPlatform->libcFlavour()->value, + $targetPlatform->phpBinaryPath->debugMode() === DebugBuild::Debug ? '-debug' : '', + $targetPlatform->threadSafety === ThreadSafetyMode::ThreadSafe ? '-zts' : '-nts', + )), + strtolower(sprintf( + 'php_%s-%s_php%s-%s-%s%s%s.tgz', + $package->extensionName()->name(), + $package->version(), + $targetPlatform->phpBinaryPath->majorMinorVersion(), + $targetPlatform->architecture->name, + $targetPlatform->libcFlavour()->value, + $targetPlatform->phpBinaryPath->debugMode() === DebugBuild::Debug ? '-debug' : '', + $targetPlatform->threadSafety === ThreadSafetyMode::ThreadSafe ? '-zts' : '-nts', + )), + ])); } } diff --git a/test/unit/Platform/PrePackagedBinaryAssetNameTest.php b/test/unit/Platform/PrePackagedBinaryAssetNameTest.php new file mode 100644 index 00000000..fce2d46d --- /dev/null +++ b/test/unit/Platform/PrePackagedBinaryAssetNameTest.php @@ -0,0 +1,136 @@ +createMock(PhpBinaryPath::class); + $php->method('debugMode')->willReturn(DebugBuild::NoDebug); + $php->method('majorMinorVersion')->willReturn('8.2'); + + $targetPlatform = new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + $php, + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + null, + ); + + $libc = $targetPlatform->libcFlavour(); + self::assertSame( + [ + 'php_foobar-1.2.3_php8.2-x86_64-' . $libc->value . '.zip', + 'php_foobar-1.2.3_php8.2-x86_64-' . $libc->value . '.tgz', + 'php_foobar-1.2.3_php8.2-x86_64-' . $libc->value . '-nts.zip', + 'php_foobar-1.2.3_php8.2-x86_64-' . $libc->value . '-nts.tgz', + ], + PrePackagedBinaryAssetName::packageNames( + $targetPlatform, + new Package( + $this->createMock(CompletePackageInterface::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foobar'), + 'foo/bar', + '1.2.3', + null, + ), + ), + ); + } + + public function testPackageNamesZts(): void + { + $php = $this->createMock(PhpBinaryPath::class); + $php->method('debugMode')->willReturn(DebugBuild::NoDebug); + $php->method('majorMinorVersion')->willReturn('8.3'); + + $targetPlatform = new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + $php, + Architecture::x86_64, + ThreadSafetyMode::ThreadSafe, + 1, + null, + ); + + $libc = $targetPlatform->libcFlavour(); + self::assertSame( + [ + 'php_foobar-1.2.3_php8.3-x86_64-' . $libc->value . '-zts.zip', + 'php_foobar-1.2.3_php8.3-x86_64-' . $libc->value . '-zts.tgz', + ], + PrePackagedBinaryAssetName::packageNames( + $targetPlatform, + new Package( + $this->createMock(CompletePackageInterface::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foobar'), + 'foo/bar', + '1.2.3', + null, + ), + ), + ); + } + + public function testPackageNamesDebug(): void + { + $php = $this->createMock(PhpBinaryPath::class); + $php->method('debugMode')->willReturn(DebugBuild::Debug); + $php->method('majorMinorVersion')->willReturn('8.4'); + + $targetPlatform = new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + $php, + Architecture::arm64, + ThreadSafetyMode::NonThreadSafe, + 1, + null, + ); + + $libc = $targetPlatform->libcFlavour(); + self::assertSame( + [ + 'php_foobar-1.2.3_php8.4-arm64-' . $libc->value . '-debug.zip', + 'php_foobar-1.2.3_php8.4-arm64-' . $libc->value . '-debug.tgz', + 'php_foobar-1.2.3_php8.4-arm64-' . $libc->value . '-debug-nts.zip', + 'php_foobar-1.2.3_php8.4-arm64-' . $libc->value . '-debug-nts.tgz', + ], + PrePackagedBinaryAssetName::packageNames( + $targetPlatform, + new Package( + $this->createMock(CompletePackageInterface::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foobar'), + 'foo/bar', + '1.2.3', + null, + ), + ), + ); + } +} From 78e651f6274700e4ce0748ffada7a0f7707ccaef Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 7 Jan 2026 16:56:36 +0000 Subject: [PATCH 08/16] 436: added basic documentation of new download-url-method list --- docs/extension-maintainers.md | 49 +++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/docs/extension-maintainers.md b/docs/extension-maintainers.md index 8d279c63..1952009a 100644 --- a/docs/extension-maintainers.md +++ b/docs/extension-maintainers.md @@ -228,9 +228,11 @@ The `build-path` may contain some templated values which are replaced: ##### `download-url-method` The `download-url-method` directive allows extension maintainers to -change the behaviour of downloading the source package. +change the behaviour of downloading the source package. This should be defined +as a list of supported methods, but for backwards compatibility a single +string may be used. - * Setting this to `composer-default`, which is the default value if not + * Setting this to `composer-default`, which is the default value if nothing is specified, will use the default behaviour implemented by Composer, which is to use the standard ZIP archive from the GitHub API (or other source control system). @@ -240,6 +242,49 @@ change the behaviour of downloading the source package. * `php_{ExtensionName}-{Version}-src.zip` (e.g. `php_myext-1.20.1-src.zip`) * `{ExtensionName}-{Version}.tgz` (this is intended for backwards compatibility with PECL packages) + * Using `pre-packaged-binary` will locate a tgz or zip archive in the release + assets list based on matching one of the following naming conventions: + * `php_{ExtensionName}-{Version}_php{PhpVersion}-{Arch}-{Libc}-{Debug}-{TSMode}.{Format}` + * The replacements are: + * `{ExtensionName}` the name of your extension, e.g. `xdebug` (hint: this + is not your Composer package name!) + * `{PhpVersion}` the major and minor version of PHP, e.g. `8.5` + * `{Version}` the version of your extension, e.g. `1.20.1` + * `{Arch}` the architecture of the binary, one of `x86`, `x86_64`, `arm64` + * `{Libc}` the libc flavour, one of `glibc`, `musl` + * `{Debug}` the debug mode, one of `debug`, `nodebug` (or omitted) + * `{TSMode}` the thread safety mode, one of `zts`, `nts` (or omitted) + * `{Format}` the archive format, one of `zip`, `tgz` + * Some examples of valid asset names: + * `php_xdebug-4.1_php8.4-x86_64-glibc.tgz` (or `php_xdebug-4.1_php8.4-x86_64-glibc-nts.tgz`) + * `php_xdebug-4.1_php8.4-x86_64-musl.tgz` (or `php_xdebug-4.1_php8.4-x86_64-musl-nts.tgz`) + * `php_xdebug-4.1_php8.4-arm64-glibc.tgz` (or `php_xdebug-4.1_php8.4-arm64-glibc-nts.tgz`) + * `php_xdebug-4.1_php8.4-arm64-musl.tgz` (or `php_xdebug-4.1_php8.4-arm64-musl-nts.tgz`) + * `php_xdebug-4.1_php8.4-x86_64-glibc-zts.tgz` + * `php_xdebug-4.1_php8.4-x86_64-musl-zts.tgz` + * `php_xdebug-4.1_php8.4-arm64-glibc-zts.tgz` + * `php_xdebug-4.1_php8.4-arm64-musl-zts.tgz` + * `php_xdebug-4.1_php8.4-x86_64-glibc-debug.tgz` + * `php_xdebug-4.1_php8.4-x86_64-musl-debug.tgz` + * `php_xdebug-4.1_php8.4-arm64-glibc-debug.tgz` + * `php_xdebug-4.1_php8.4-arm64-musl-debug.tgz` + * It is recommended that `pre-packaged-binary` is combined with `composer-default` + as a fallback mechanism, if a particular combination is supported, but not + pre-packaged on the release, e.g. `"download-url-method": ["pre-packaged-binary", "composer-default"]`. + PIE will try to find a pre-packaged binary asset first, but if it cannot + find an appropriate binary, it will download the source code and build it + in the traditional manner. + +###### Example of using `pre-packaged-binary` with `composer-default` fallback + +```json +{ + "name": "myvendor/myext", + "php-ext": { + "download-url-method": ["pre-packaged-binary", "composer-default"] + } +} +``` ##### `os-families` restrictions From 4f495922855ef3fba9346231e3aa5479cd328397 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 8 Jan 2026 15:38:42 +0000 Subject: [PATCH 09/16] 436: fix up test expectations for pre-packaged binary DownloadUrlMethod --- src/Platform/TargetPlatform.php | 2 +- .../Downloading/DownloadUrlMethodTest.php | 25 +++++++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/Platform/TargetPlatform.php b/src/Platform/TargetPlatform.php index 05b78cad..9a50542c 100644 --- a/src/Platform/TargetPlatform.php +++ b/src/Platform/TargetPlatform.php @@ -21,7 +21,7 @@ */ class TargetPlatform { - private static LibcFlavour|null $libcFlavour; + private static LibcFlavour $libcFlavour; public function __construct( public readonly OperatingSystem $operatingSystem, diff --git a/test/unit/Downloading/DownloadUrlMethodTest.php b/test/unit/Downloading/DownloadUrlMethodTest.php index 418c33cd..b499f76d 100644 --- a/test/unit/Downloading/DownloadUrlMethodTest.php +++ b/test/unit/Downloading/DownloadUrlMethodTest.php @@ -10,6 +10,7 @@ use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; use Php\Pie\Platform\Architecture; +use Php\Pie\Platform\DebugBuild; use Php\Pie\Platform\OperatingSystem; use Php\Pie\Platform\OperatingSystemFamily; use Php\Pie\Platform\TargetPhp\PhpBinaryPath; @@ -114,16 +115,19 @@ public function testPrePackagedBinaryDownloads(): void $package = Package::fromComposerCompletePackage($composerPackage); $phpBinaryPath = $this->createMock(PhpBinaryPath::class); - $phpBinaryPath->expects(self::any()) + $phpBinaryPath ->method('majorMinorVersion') ->willReturn('8.3'); + $phpBinaryPath + ->method('debugMode') + ->willReturn(DebugBuild::Debug); $targetPlatform = new TargetPlatform( OperatingSystem::NonWindows, OperatingSystemFamily::Linux, $phpBinaryPath, Architecture::x86_64, - ThreadSafetyMode::NonThreadSafe, + ThreadSafetyMode::ThreadSafe, 1, null, ); @@ -136,7 +140,10 @@ public function testPrePackagedBinaryDownloads(): void self::assertSame(DownloadUrlMethod::PrePackagedBinary, $downloadUrlMethod); self::assertSame( - ['php_bar-1.2.3_php8.3-x86_64-glibc-debug-nts.tgz'], + [ + 'php_bar-1.2.3_php8.3-x86_64-glibc-debug-zts.zip', + 'php_bar-1.2.3_php8.3-x86_64-glibc-debug-zts.tgz', + ], $downloadUrlMethod->possibleAssetNames($package, $targetPlatform), ); } @@ -183,9 +190,12 @@ public function testMultipleDownloadUrlMethods(): void $package = Package::fromComposerCompletePackage($composerPackage); $phpBinaryPath = $this->createMock(PhpBinaryPath::class); - $phpBinaryPath->expects(self::any()) + $phpBinaryPath ->method('majorMinorVersion') ->willReturn('8.3'); + $phpBinaryPath + ->method('debugMode') + ->willReturn(DebugBuild::Debug); $targetPlatform = new TargetPlatform( OperatingSystem::NonWindows, @@ -204,7 +214,12 @@ public function testMultipleDownloadUrlMethods(): void $firstMethod = $downloadUrlMethods[0]; self::assertSame(DownloadUrlMethod::PrePackagedBinary, $firstMethod); self::assertSame( - ['php_bar-1.2.3_php8.3-x86_64-glibc-debug-nts.tgz'], + [ + 'php_bar-1.2.3_php8.3-x86_64-glibc-debug.zip', + 'php_bar-1.2.3_php8.3-x86_64-glibc-debug.tgz', + 'php_bar-1.2.3_php8.3-x86_64-glibc-debug-nts.zip', + 'php_bar-1.2.3_php8.3-x86_64-glibc-debug-nts.tgz', + ], $firstMethod->possibleAssetNames($package, $targetPlatform), ); From 95687005c411082c8ea52763200e747be7f7d730 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Fri, 9 Jan 2026 15:49:09 +0000 Subject: [PATCH 10/16] 436: added OverrideDownloadUrlInstallListener test coverage for pre-packaged-binary and a bunch of tests to be implemented --- src/Downloading/DownloadUrlMethod.php | 7 +- test/integration/Building/UnixBuildTest.php | 16 ++- .../Installing/UnixInstallTest.php | 7 +- ...OverrideDownloadUrlInstallListenerTest.php | 132 ++++++++++++++++++ test/unit/DependencyResolver/PackageTest.php | 15 ++ .../Downloading/DownloadUrlMethodTest.php | 20 +++ test/unit/Platform/LibcFlavourTest.php | 23 +++ .../Platform/TargetPhp/PhpBinaryPathTest.php | 10 ++ test/unit/Platform/TargetPlatformTest.php | 5 + 9 files changed, 230 insertions(+), 5 deletions(-) create mode 100644 test/unit/Platform/LibcFlavourTest.php diff --git a/src/Downloading/DownloadUrlMethod.php b/src/Downloading/DownloadUrlMethod.php index 9c42cefe..4b1d99d8 100644 --- a/src/Downloading/DownloadUrlMethod.php +++ b/src/Downloading/DownloadUrlMethod.php @@ -41,7 +41,12 @@ public function possibleAssetNames(Package $package, TargetPlatform $targetPlatf public static function fromDownloadedPackage(DownloadedPackage $downloadedPackage): self { - $extra = $downloadedPackage->package->composerPackage()->getExtra(); + return self::fromComposerPackage($downloadedPackage->package->composerPackage()); + } + + public static function fromComposerPackage(CompletePackageInterface $completePackage): self + { + $extra = $completePackage->getExtra(); return self::from(array_key_exists(self::COMPOSER_PACKAGE_EXTRA_KEY, $extra) && is_string($extra[self::COMPOSER_PACKAGE_EXTRA_KEY]) ? $extra[self::COMPOSER_PACKAGE_EXTRA_KEY] : ''); } diff --git a/test/integration/Building/UnixBuildTest.php b/test/integration/Building/UnixBuildTest.php index 1f4c41e4..a4efc1c3 100644 --- a/test/integration/Building/UnixBuildTest.php +++ b/test/integration/Building/UnixBuildTest.php @@ -29,7 +29,7 @@ final class UnixBuildTest extends TestCase private const COMPOSER_PACKAGE_EXTRA_KEY = 'download-url-method'; private const TEST_EXTENSION_PATH = __DIR__ . '/../../assets/pie_test_ext'; - public function testUnixBuildCanBuildExtension(): void + public function testUnixSourceBuildCanBuildExtension(): void { if (Platform::isWindows()) { self::markTestSkipped('Unix build test cannot be run on Windows'); @@ -83,7 +83,7 @@ public function testUnixBuildCanBuildExtension(): void (new Process(['phpize', '--clean'], $downloadedPackage->extractedSourcePath))->mustRun(); } - public function testUnixBuildWillThrowExceptionWhenExpectedBinaryNameMismatches(): void + public function testUnixSourceBuildWillThrowExceptionWhenExpectedBinaryNameMismatches(): void { if (Platform::isWindows()) { self::markTestSkipped('Unix build test cannot be run on Windows'); @@ -125,7 +125,7 @@ public function testUnixBuildWillThrowExceptionWhenExpectedBinaryNameMismatches( } } - public function testUnixBuildCanBuildExtensionWithBuildPath(): void + public function testUnixSourceBuildCanBuildExtensionWithBuildPath(): void { if (Platform::isWindows()) { self::markTestSkipped('Unix build test cannot be run on Windows'); @@ -176,6 +176,16 @@ public function testUnixBuildCanBuildExtensionWithBuildPath(): void (new Process(['phpize', '--clean'], $downloadedPackage->extractedSourcePath))->mustRun(); } + public function testUnixBinaryBuildThrowsErrorWhenBinaryFileNotFound(): void + { + self::fail('todo'); // @todo 436 + } + + public function testUnixBinaryBuildReturnsBinaryFile(): void + { + self::fail('todo'); // @todo 436 + } + public function testCleanupDoesNotCleanWhenConfigureIsMissing(): void { if (Platform::isWindows()) { diff --git a/test/integration/Installing/UnixInstallTest.php b/test/integration/Installing/UnixInstallTest.php index cd045421..10be98b7 100644 --- a/test/integration/Installing/UnixInstallTest.php +++ b/test/integration/Installing/UnixInstallTest.php @@ -70,7 +70,7 @@ public static function phpPathProvider(): array } #[DataProvider('phpPathProvider')] - public function testUnixInstallCanInstallExtension(string $phpConfig): void + public function testUnixInstallCanInstallExtensionBuiltFromSource(string $phpConfig): void { assert($phpConfig !== ''); if (Platform::isWindows()) { @@ -130,4 +130,9 @@ public function testUnixInstallCanInstallExtension(string $phpConfig): void (new Process(['make', 'clean'], $downloadedPackage->extractedSourcePath))->mustRun(); (new Process(['phpize', '--clean'], $downloadedPackage->extractedSourcePath))->mustRun(); } + + public function testUnixInstallCanInstallPrePackagedBinary(): void + { + self::fail('todo'); // @todo 436 + } } diff --git a/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php index 5a356b8e..9e8bb48d 100644 --- a/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php +++ b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php @@ -16,6 +16,8 @@ use Php\Pie\ComposerIntegration\PieComposerRequest; use Php\Pie\ComposerIntegration\PieOperation; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; +use Php\Pie\Downloading\DownloadUrlMethod; +use Php\Pie\Downloading\Exception\CouldNotFindReleaseAsset; use Php\Pie\Downloading\PackageReleaseAssets; use Php\Pie\Platform\Architecture; use Php\Pie\Platform\OperatingSystem; @@ -256,6 +258,7 @@ public function testWindowsUrlInstallerDoesNotRunOnNonWindows(): void 'https://example.com/git-archive-zip-url', $composerPackage->getDistUrl(), ); + self::assertSame(DownloadUrlMethod::ComposerDefaultDownload, DownloadUrlMethod::fromComposerPackage($composerPackage)); } public function testDistUrlIsUpdatedForWindowsInstallers(): void @@ -310,6 +313,7 @@ public function testDistUrlIsUpdatedForWindowsInstallers(): void 'https://example.com/windows-download-url', $composerPackage->getDistUrl(), ); + self::assertSame(DownloadUrlMethod::WindowsBinaryDownload, DownloadUrlMethod::fromComposerPackage($composerPackage)); } public function testDistUrlIsUpdatedForPrePackagedTgzSource(): void @@ -369,6 +373,134 @@ public function testDistUrlIsUpdatedForPrePackagedTgzSource(): void 'https://example.com/pre-packaged-source-download-url.tgz', $composerPackage->getDistUrl(), ); + self::assertSame(DownloadUrlMethod::PrePackagedSourceDownload, DownloadUrlMethod::fromComposerPackage($composerPackage)); self::assertSame('tar', $composerPackage->getDistType()); } + + public function testDistUrlIsUpdatedForPrePackagedTgzBinaryWhenBinaryIsFound(): 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]), + ); + + $packageReleaseAssets = $this->createMock(PackageReleaseAssets::class); + $packageReleaseAssets + ->expects(self::once()) + ->method('findMatchingReleaseAssetUrl') + ->willReturn('https://example.com/pre-packaged-binary-download-url.tgz'); + + $this->container + ->method('get') + ->with(PackageReleaseAssets::class) + ->willReturn($packageReleaseAssets); + + (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, + ), + new RequestedPackageAndVersion('foo/bar', '^1.1'), + PieOperation::Install, + [], + null, + false, + ), + ))($installerEvent); + + self::assertSame( + 'https://example.com/pre-packaged-binary-download-url.tgz', + $composerPackage->getDistUrl(), + ); + self::assertSame(DownloadUrlMethod::PrePackagedBinary, DownloadUrlMethod::fromComposerPackage($composerPackage)); + self::assertSame('tar', $composerPackage->getDistType()); + } + + public function testDistUrlIsUpdatedForPrePackagedTgzBinaryWhenBinaryIsNotFound(): 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]), + ); + + $packageReleaseAssets = $this->createMock(PackageReleaseAssets::class); + $packageReleaseAssets + ->expects(self::once()) + ->method('findMatchingReleaseAssetUrl') + ->willThrowException(new CouldNotFindReleaseAsset('nope not found')); + + $this->container + ->method('get') + ->with(PackageReleaseAssets::class) + ->willReturn($packageReleaseAssets); + + (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, + ), + new RequestedPackageAndVersion('foo/bar', '^1.1'), + PieOperation::Install, + [], + null, + false, + ), + ))($installerEvent); + + self::assertSame( + 'https://example.com/git-archive-zip-url', + $composerPackage->getDistUrl(), + ); + self::assertSame(DownloadUrlMethod::ComposerDefaultDownload, DownloadUrlMethod::fromComposerPackage($composerPackage)); + self::assertSame('zip', $composerPackage->getDistType()); + } + + public function testNoSelectedDownloadUrlMethodWillThrowException(): void + { + self::fail('todo'); // @todo 436 + } } diff --git a/test/unit/DependencyResolver/PackageTest.php b/test/unit/DependencyResolver/PackageTest.php index eebb5a17..ead73b4e 100644 --- a/test/unit/DependencyResolver/PackageTest.php +++ b/test/unit/DependencyResolver/PackageTest.php @@ -177,4 +177,19 @@ public function testDownloadUrlMethodWithMultiItemListIsNotYetSupported(): void $this->expectExceptionMessage('This extension requires a newer version of PIE. Multiple download-url-methods are not supported until PIE 1.4.0.'); Package::fromComposerCompletePackage($composerCompletePackage); } + + public function testFromComposerCompletePackageWithStringDownloadUrlMethod(): void + { + self::fail('todo'); // @todo 436 + } + + public function testFromComposerCompletePackageWithListDownloadUrlMethods(): void + { + self::fail('todo'); // @todo 436 + } + + public function testFromComposerCompletePackageWithOmittedDownloadUrlMethod(): void + { + self::fail('todo'); // @todo 436 + } } diff --git a/test/unit/Downloading/DownloadUrlMethodTest.php b/test/unit/Downloading/DownloadUrlMethodTest.php index b499f76d..b3823d84 100644 --- a/test/unit/Downloading/DownloadUrlMethodTest.php +++ b/test/unit/Downloading/DownloadUrlMethodTest.php @@ -238,4 +238,24 @@ public function testMultipleDownloadUrlMethods(): void self::assertSame(DownloadUrlMethod::ComposerDefaultDownload, $thirdMethod); self::assertNull($thirdMethod->possibleAssetNames($package, $targetPlatform)); } + + public function testFromComposerPackageWhenPackageKeyWasDefined(): void + { + self::fail('todo'); // @todo 436 + } + + public function testFromComposerPackageWhenPackageKeyWasNotDefined(): void + { + self::fail('todo'); // @todo 436 + } + + public function testFromDownloadedPackage(): void + { + self::fail('todo'); // @todo 436 + } + + public function testWriteToComposerPackageStoresDownloadUrlMethod(): void + { + self::fail('todo'); // @todo 436 + } } diff --git a/test/unit/Platform/LibcFlavourTest.php b/test/unit/Platform/LibcFlavourTest.php new file mode 100644 index 00000000..9e5c898f --- /dev/null +++ b/test/unit/Platform/LibcFlavourTest.php @@ -0,0 +1,23 @@ +buildProvider()); } + + public function testDebugBuildModeReturnsDebugWhenYes(): void + { + self::fail('todo'); // @todo 436 + } + + public function testDebugBuildModeReturnsNoDebugWhenNo(): void + { + self::fail('todo'); // @todo 436 + } } diff --git a/test/unit/Platform/TargetPlatformTest.php b/test/unit/Platform/TargetPlatformTest.php index 3753181a..60e22d06 100644 --- a/test/unit/Platform/TargetPlatformTest.php +++ b/test/unit/Platform/TargetPlatformTest.php @@ -107,4 +107,9 @@ public function testLinuxPlatform(): void self::assertSame(ThreadSafetyMode::NonThreadSafe, $platform->threadSafety); self::assertSame(Architecture::x86_64, $platform->architecture); } + + public function testLibcFlavourIsMemoized(): void + { + self::fail('todo'); // @todo 436 + } } From 8f5cd0b756dc3cfa0248be4b4e5601f2d0d3954c Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 13 Jan 2026 16:37:32 +0000 Subject: [PATCH 11/16] 436: added a bunch of tests for pre-built binaries --- src/Building/ExtensionBinaryNotFound.php | 8 ++ src/Building/UnixBuild.php | 2 +- .../CouldNotDetermineDownloadUrlMethod.php | 42 +++++++++ .../OverrideDownloadUrlInstallListener.php | 17 ++-- src/Installing/UnixInstall.php | 3 + test/assets/fake-ldd/glibc/ldd | 8 ++ test/assets/fake-ldd/musl/ldd | 4 + .../invalid/wrong-name.so | 0 .../valid/pie_test_ext.so | 0 test/integration/Building/UnixBuildTest.php | 67 +++++++++++++- .../Installing/UnixInstallTest.php | 91 ++++++++++++++++++- ...CouldNotDetermineDownloadUrlMethodTest.php | 73 +++++++++++++++ ...OverrideDownloadUrlInstallListenerTest.php | 56 +++++++++++- test/unit/DependencyResolver/PackageTest.php | 20 +++- .../Downloading/DownloadUrlMethodTest.php | 32 +++++-- test/unit/Platform/LibcFlavourTest.php | 26 +++++- .../Platform/TargetPhp/PhpBinaryPathTest.php | 17 +++- test/unit/Platform/TargetPlatformTest.php | 5 +- 18 files changed, 443 insertions(+), 28 deletions(-) create mode 100644 src/ComposerIntegration/Listeners/CouldNotDetermineDownloadUrlMethod.php create mode 100755 test/assets/fake-ldd/glibc/ldd create mode 100755 test/assets/fake-ldd/musl/ldd create mode 100644 test/assets/pre-packaged-binary-examples/invalid/wrong-name.so create mode 100644 test/assets/pre-packaged-binary-examples/valid/pie_test_ext.so create mode 100644 test/unit/ComposerIntegration/Listeners/CouldNotDetermineDownloadUrlMethodTest.php diff --git a/src/Building/ExtensionBinaryNotFound.php b/src/Building/ExtensionBinaryNotFound.php index 1074361b..9974ee98 100644 --- a/src/Building/ExtensionBinaryNotFound.php +++ b/src/Building/ExtensionBinaryNotFound.php @@ -10,6 +10,14 @@ class ExtensionBinaryNotFound extends RuntimeException { + public static function fromPrePackagedBinary(string $expectedBinaryName): self + { + return new self(sprintf( + 'Expected pre-packaged binary does not exist: %s', + $expectedBinaryName, + )); + } + public static function fromExpectedBinary(string $expectedBinaryName): self { return new self(sprintf( diff --git a/src/Building/UnixBuild.php b/src/Building/UnixBuild.php index 18de92a0..f46f6df1 100644 --- a/src/Building/UnixBuild.php +++ b/src/Building/UnixBuild.php @@ -56,7 +56,7 @@ private function prePackagedBinary( $expectedSoFile = $downloadedPackage->extractedSourcePath . '/' . $downloadedPackage->package->extensionName()->name() . '.so'; if (! file_exists($expectedSoFile)) { - throw ExtensionBinaryNotFound::fromExpectedBinary($expectedSoFile); + throw ExtensionBinaryNotFound::fromPrePackagedBinary($expectedSoFile); } $io->write(sprintf( diff --git a/src/ComposerIntegration/Listeners/CouldNotDetermineDownloadUrlMethod.php b/src/ComposerIntegration/Listeners/CouldNotDetermineDownloadUrlMethod.php new file mode 100644 index 00000000..a5782c56 --- /dev/null +++ b/src/ComposerIntegration/Listeners/CouldNotDetermineDownloadUrlMethod.php @@ -0,0 +1,42 @@ + $downloadMethods + * @param array $failureReasons + */ + public static function fromDownloadUrlMethods(Package $piePackage, array $downloadMethods, array $failureReasons): self + { + $message = sprintf('Could not download %s', $piePackage->name()); + + if (count($downloadMethods) === 1) { + $first = array_key_first($downloadMethods); + $message .= sprintf(' using %s method: %s', $downloadMethods[$first]->value, $failureReasons[$downloadMethods[$first]->value] ?? '(unknown failure)'); + + return new self($message); + } + + $message .= ' using the following methods:' . PHP_EOL; + + foreach ($downloadMethods as $downloadMethod) { + $message .= sprintf(' - %s: %s%s', $downloadMethod->value, $failureReasons[$downloadMethod->value] ?? '(unknown failure)', PHP_EOL); + } + + return new self($message); + } +} diff --git a/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php b/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php index 9970f636..cf7b6e5a 100644 --- a/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php +++ b/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php @@ -17,7 +17,6 @@ use Php\Pie\Downloading\DownloadUrlMethod; use Php\Pie\Downloading\PackageReleaseAssets; use Psr\Container\ContainerInterface; -use RuntimeException; use Throwable; use function array_walk; @@ -76,9 +75,10 @@ function (OperationInterface $operation): void { $downloadUrlMethods = DownloadUrlMethod::possibleDownloadUrlMethodsForPackage($piePackage, $targetPlatform); $selectedDownloadUrlMethod = null; + $downloadMethodFailures = []; foreach ($downloadUrlMethods as $downloadUrlMethod) { - $this->io->write('Trying: ' . $downloadUrlMethod->value); // @todo 436 verbosity + $this->io->write('Trying to download using: ' . $downloadUrlMethod->value, verbosity: IOInterface::VERY_VERBOSE); // Exit early if we should just use Composer's normal download if ($downloadUrlMethod === DownloadUrlMethod::ComposerDefaultDownload) { @@ -89,12 +89,14 @@ function (OperationInterface $operation): void { try { $possibleAssetNames = $downloadUrlMethod->possibleAssetNames($piePackage, $targetPlatform); } catch (Throwable $t) { - $this->io->write('Failed fetching asset names [' . $downloadUrlMethod->value . ']: ' . $t->getMessage()); // @todo 436 verbosity + $downloadMethodFailures[$downloadUrlMethod->value] = $t->getMessage(); + $this->io->write('Failed fetching asset names [' . $downloadUrlMethod->value . ']: ' . $t->getMessage(), verbosity: IOInterface::VERBOSE); continue; } if ($possibleAssetNames === null) { - $this->io->write('Failed fetching asset names [' . $downloadUrlMethod->value . ']: No asset names'); // @todo 436 verbosity + $downloadMethodFailures[$downloadUrlMethod->value] = 'No asset names'; + $this->io->write('Failed fetching asset names [' . $downloadUrlMethod->value . ']: No asset names', verbosity: IOInterface::VERBOSE); continue; } @@ -110,7 +112,8 @@ function (OperationInterface $operation): void { $possibleAssetNames, ); } catch (Throwable $t) { - $this->io->write('Failed locating asset [' . $downloadUrlMethod->value . ']: ' . $t->getMessage()); // @todo 436 verbosity + $downloadMethodFailures[$downloadUrlMethod->value] = $t->getMessage(); + $this->io->write('Failed locating asset [' . $downloadUrlMethod->value . ']: ' . $t->getMessage(), verbosity: IOInterface::VERBOSE); continue; } @@ -126,11 +129,11 @@ function (OperationInterface $operation): void { } if ($selectedDownloadUrlMethod === null) { - throw new RuntimeException('No download method could be found for ' . $piePackage->name()); // @todo 436 improve message, will need to give more info! + throw CouldNotDetermineDownloadUrlMethod::fromDownloadUrlMethods($piePackage, $downloadUrlMethods, $downloadMethodFailures); } $selectedDownloadUrlMethod->writeToComposerPackage($composerPackage); - $this->io->write('FINALLY SETTLED on using download URL method: ' . $selectedDownloadUrlMethod->value . ''); // @todo 436 verbosity + $this->io->write('Selected download URL method: ' . $selectedDownloadUrlMethod->value . '', verbosity: IOInterface::VERBOSE); }, ); } diff --git a/src/Installing/UnixInstall.php b/src/Installing/UnixInstall.php index 94099122..2462dab6 100644 --- a/src/Installing/UnixInstall.php +++ b/src/Installing/UnixInstall.php @@ -16,6 +16,7 @@ use function array_unshift; use function file_exists; +use function implode; use function is_writable; use function sprintf; @@ -73,6 +74,8 @@ public function __invoke( array_unshift($installCommand, Sudo::find()); } + $io->write(sprintf('Install command is: %s', implode(' ', $installCommand)), verbosity: IOInterface::VERY_VERBOSE); + $makeInstallOutput = Process::run( $installCommand, $downloadedPackage->extractedSourcePath, diff --git a/test/assets/fake-ldd/glibc/ldd b/test/assets/fake-ldd/glibc/ldd new file mode 100755 index 00000000..0a6f9719 --- /dev/null +++ b/test/assets/fake-ldd/glibc/ldd @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +echo " linux-vdso.so.1 (0x000078fecccd6000) + libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x000078feccc57000) + libcap.so.2 => /lib/x86_64-linux-gnu/libcap.so.2 (0x000078feccc49000) + libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x000078fecca00000) + libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x000078fecc941000) + /lib64/ld-linux-x86-64.so.2 (0x000078fecccd8000)" diff --git a/test/assets/fake-ldd/musl/ldd b/test/assets/fake-ldd/musl/ldd new file mode 100755 index 00000000..1824d33d --- /dev/null +++ b/test/assets/fake-ldd/musl/ldd @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +echo " /lib/ld-musl-x86_64.so.1 (0x7146f93b1000) + libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7146f93b1000)" diff --git a/test/assets/pre-packaged-binary-examples/invalid/wrong-name.so b/test/assets/pre-packaged-binary-examples/invalid/wrong-name.so new file mode 100644 index 00000000..e69de29b diff --git a/test/assets/pre-packaged-binary-examples/valid/pie_test_ext.so b/test/assets/pre-packaged-binary-examples/valid/pie_test_ext.so new file mode 100644 index 00000000..e69de29b diff --git a/test/integration/Building/UnixBuildTest.php b/test/integration/Building/UnixBuildTest.php index a4efc1c3..bcf221e0 100644 --- a/test/integration/Building/UnixBuildTest.php +++ b/test/integration/Building/UnixBuildTest.php @@ -28,6 +28,8 @@ final class UnixBuildTest extends TestCase { private const COMPOSER_PACKAGE_EXTRA_KEY = 'download-url-method'; private const TEST_EXTENSION_PATH = __DIR__ . '/../../assets/pie_test_ext'; + private const TEST_PREBUILT_PATH_VALID = __DIR__ . '/../../assets/pre-packaged-binary-examples/valid'; + private const TEST_PREBUILT_PATH_INVALID = __DIR__ . '/../../assets/pre-packaged-binary-examples/invalid'; public function testUnixSourceBuildCanBuildExtension(): void { @@ -178,12 +180,73 @@ public function testUnixSourceBuildCanBuildExtensionWithBuildPath(): void public function testUnixBinaryBuildThrowsErrorWhenBinaryFileNotFound(): void { - self::fail('todo'); // @todo 436 + if (Platform::isWindows()) { + self::markTestSkipped('Unix build test cannot be run on Windows'); + } + + $output = new BufferIO(); + + $composerPackage = $this->createMock(CompletePackageInterface::class); + $composerPackage->method('getPrettyName')->willReturn('myvendor/pie_test_ext'); + $composerPackage->method('getPrettyVersion')->willReturn('0.1.0'); + $composerPackage->method('getType')->willReturn('php-ext'); + $composerPackage + ->method('getExtra') + ->willReturn([self::COMPOSER_PACKAGE_EXTRA_KEY => DownloadUrlMethod::PrePackagedBinary->value]); + + $downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( + Package::fromComposerCompletePackage($composerPackage), + self::TEST_PREBUILT_PATH_INVALID, + ); + + $targetPlatform = TargetPlatform::fromPhpBinaryPath(PhpBinaryPath::fromCurrentProcess(), null); + $unixBuilder = new UnixBuild(); + + $this->expectException(ExtensionBinaryNotFound::class); + $this->expectExceptionMessage('Expected pre-packaged binary does not exist'); + $unixBuilder->__invoke( + $downloadedPackage, + $targetPlatform, + ['--enable-pie_test_ext'], + $output, + null, + ); } public function testUnixBinaryBuildReturnsBinaryFile(): void { - self::fail('todo'); // @todo 436 + if (Platform::isWindows()) { + self::markTestSkipped('Unix build test cannot be run on Windows'); + } + + $output = new BufferIO(); + + $composerPackage = $this->createMock(CompletePackageInterface::class); + $composerPackage->method('getPrettyName')->willReturn('myvendor/pie_test_ext'); + $composerPackage->method('getPrettyVersion')->willReturn('0.1.0'); + $composerPackage->method('getType')->willReturn('php-ext'); + $composerPackage + ->method('getExtra') + ->willReturn([self::COMPOSER_PACKAGE_EXTRA_KEY => DownloadUrlMethod::PrePackagedBinary->value]); + + $downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( + Package::fromComposerCompletePackage($composerPackage), + self::TEST_PREBUILT_PATH_VALID, + ); + + $targetPlatform = TargetPlatform::fromPhpBinaryPath(PhpBinaryPath::fromCurrentProcess(), null); + $unixBuilder = new UnixBuild(); + + $binaryFile = $unixBuilder->__invoke( + $downloadedPackage, + $targetPlatform, + ['--enable-pie_test_ext'], + $output, + null, + ); + + self::assertSame('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', $binaryFile->checksum); + self::assertStringEndsWith('pre-packaged-binary-examples/valid/pie_test_ext.so', $binaryFile->filePath); } public function testCleanupDoesNotCleanWhenConfigureIsMissing(): void diff --git a/test/integration/Installing/UnixInstallTest.php b/test/integration/Installing/UnixInstallTest.php index 10be98b7..f80e21d8 100644 --- a/test/integration/Installing/UnixInstallTest.php +++ b/test/integration/Installing/UnixInstallTest.php @@ -13,6 +13,7 @@ use Php\Pie\Downloading\DownloadUrlMethod; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; +use Php\Pie\File\BinaryFile; use Php\Pie\Installing\Ini\PickBestSetupIniApproach; use Php\Pie\Installing\SetupIniFile; use Php\Pie\Installing\UnixInstall; @@ -31,12 +32,18 @@ use function file_exists; use function is_executable; use function is_writable; +use function mkdir; +use function rename; +use function unlink; + +use const DIRECTORY_SEPARATOR; #[CoversClass(UnixInstall::class)] final class UnixInstallTest extends TestCase { private const COMPOSER_PACKAGE_EXTRA_KEY = 'download-url-method'; private const TEST_EXTENSION_PATH = __DIR__ . '/../../assets/pie_test_ext'; + private const TEST_PREBUILT_PATH = __DIR__ . '/../../assets/pre-packaged-binary-examples/install'; /** @return array */ public static function phpPathProvider(): array @@ -131,8 +138,88 @@ public function testUnixInstallCanInstallExtensionBuiltFromSource(string $phpCon (new Process(['phpize', '--clean'], $downloadedPackage->extractedSourcePath))->mustRun(); } - public function testUnixInstallCanInstallPrePackagedBinary(): void + #[DataProvider('phpPathProvider')] + public function testUnixInstallCanInstallPrePackagedBinary(string $phpConfig): void { - self::fail('todo'); // @todo 436 + assert($phpConfig !== ''); + if (Platform::isWindows()) { + self::markTestSkipped('Unix build test cannot be run on Windows'); + } + + $output = new BufferIO(); + $targetPlatform = TargetPlatform::fromPhpBinaryPath(PhpBinaryPath::fromPhpConfigExecutable($phpConfig), null); + $extensionPath = $targetPlatform->phpBinaryPath->extensionPath(); + + // First build it (otherwise the test assets would need to have a binary for every test platform...) + $composerPackage = $this->createMock(CompletePackageInterface::class); + $composerPackage + ->method('getExtra') + ->willReturn([self::COMPOSER_PACKAGE_EXTRA_KEY => DownloadUrlMethod::ComposerDefaultDownload->value]); + + $built = (new UnixBuild())->__invoke( + DownloadedPackage::fromPackageAndExtractedPath( + new Package( + $composerPackage, + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('pie_test_ext'), + 'pie_test_ext', + '0.1.0', + null, + ), + self::TEST_EXTENSION_PATH, + ), + $targetPlatform, + ['--enable-pie_test_ext'], + $output, + null, + ); + + /** + * Move the built .so into a new path; this simulates a pre-packaged binary, which would not have Makefile etc + * so this ensures we're not accidentally relying on any build mechanism (`make install` or otherwise) + */ + mkdir(self::TEST_PREBUILT_PATH, 0777, true); + $prebuiltBinaryFilePath = self::TEST_PREBUILT_PATH . DIRECTORY_SEPARATOR . 'pie_test_ext.so'; + rename($built->filePath, $prebuiltBinaryFilePath); + + $prebuiltBinaryFile = BinaryFile::fromFileWithSha256Checksum($prebuiltBinaryFilePath); + + $composerPackage = $this->createMock(CompletePackageInterface::class); + $composerPackage + ->method('getExtra') + ->willReturn([self::COMPOSER_PACKAGE_EXTRA_KEY => DownloadUrlMethod::PrePackagedBinary->value]); + + $installedSharedObject = (new UnixInstall(new SetupIniFile(new PickBestSetupIniApproach([]))))->__invoke( + DownloadedPackage::fromPackageAndExtractedPath( + new Package( + $composerPackage, + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('pie_test_ext'), + 'pie_test_ext', + '0.1.0', + null, + ), + self::TEST_PREBUILT_PATH, + ), + $targetPlatform, + $prebuiltBinaryFile, + $output, + true, + ); + $outputString = $output->getOutput(); + + self::assertStringContainsString('Install complete: ' . $extensionPath . '/pie_test_ext.so', $outputString); + self::assertStringContainsString('You must now add "extension=pie_test_ext" to your php.ini', $outputString); + + self::assertSame($extensionPath . '/pie_test_ext.so', $installedSharedObject->filePath); + self::assertFileExists($installedSharedObject->filePath); + + $rmCommand = ['rm', $installedSharedObject->filePath]; + if (! is_writable($installedSharedObject->filePath)) { + array_unshift($rmCommand, 'sudo'); + } + + (new Process($rmCommand))->mustRun(); + unlink($prebuiltBinaryFile->filePath); } } diff --git a/test/unit/ComposerIntegration/Listeners/CouldNotDetermineDownloadUrlMethodTest.php b/test/unit/ComposerIntegration/Listeners/CouldNotDetermineDownloadUrlMethodTest.php new file mode 100644 index 00000000..7b0b0fce --- /dev/null +++ b/test/unit/ComposerIntegration/Listeners/CouldNotDetermineDownloadUrlMethodTest.php @@ -0,0 +1,73 @@ +createMock(CompletePackageInterface::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foo'), + 'foo/foo', + '1.2.3', + null, + ); + + $e = CouldNotDetermineDownloadUrlMethod::fromDownloadUrlMethods( + $package, + [DownloadUrlMethod::PrePackagedBinary], + [DownloadUrlMethod::PrePackagedBinary->value => 'A bad thing happened downloading the binary'], + ); + + self::assertSame( + 'Could not download foo/foo using pre-packaged-binary method: A bad thing happened downloading the binary', + $e->getMessage(), + ); + } + + public function testMultipleDownloadUrlMethods(): void + { + $package = new Package( + $this->createMock(CompletePackageInterface::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foo'), + 'foo/foo', + '1.2.3', + null, + ); + + $e = CouldNotDetermineDownloadUrlMethod::fromDownloadUrlMethods( + $package, + [ + DownloadUrlMethod::PrePackagedBinary, + DownloadUrlMethod::PrePackagedSourceDownload, + ], + [ + DownloadUrlMethod::PrePackagedBinary->value => 'A bad thing happened downloading the binary', + DownloadUrlMethod::PrePackagedSourceDownload->value => 'Another bad thing happened downloading the source', + ], + ); + + self::assertSame( + 'Could not download foo/foo using the following methods: + - pre-packaged-binary: A bad thing happened downloading the binary + - pre-packaged-source: Another bad thing happened downloading the source +', + $e->getMessage(), + ); + } +} diff --git a/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php index 9e8bb48d..657a2aab 100644 --- a/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php +++ b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php @@ -12,6 +12,7 @@ use Composer\IO\IOInterface; use Composer\Package\CompletePackage; use Composer\Package\Package; +use Php\Pie\ComposerIntegration\Listeners\CouldNotDetermineDownloadUrlMethod; use Php\Pie\ComposerIntegration\Listeners\OverrideDownloadUrlInstallListener; use Php\Pie\ComposerIntegration\PieComposerRequest; use Php\Pie\ComposerIntegration\PieOperation; @@ -501,6 +502,59 @@ public function testDistUrlIsUpdatedForPrePackagedTgzBinaryWhenBinaryIsNotFound( public function testNoSelectedDownloadUrlMethodWillThrowException(): void { - self::fail('todo'); // @todo 436 + $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'], + ]); + + $installerEvent = new InstallerEvent( + InstallerEvents::PRE_OPERATIONS_EXEC, + $this->composer, + $this->io, + false, + true, + new Transaction([], [$composerPackage]), + ); + + $packageReleaseAssets = $this->createMock(PackageReleaseAssets::class); + $packageReleaseAssets + ->expects(self::once()) + ->method('findMatchingReleaseAssetUrl') + ->willThrowException(new CouldNotFindReleaseAsset('nope not found')); + + $this->container + ->method('get') + ->with(PackageReleaseAssets::class) + ->willReturn($packageReleaseAssets); + + $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, + ), + new RequestedPackageAndVersion('foo/bar', '^1.1'), + PieOperation::Install, + [], + null, + false, + ), + ); + + $this->expectException(CouldNotDetermineDownloadUrlMethod::class); + $this->expectExceptionMessage('Could not download foo/bar using pre-packaged-binary method: nope not found'); + $listener($installerEvent); } } diff --git a/test/unit/DependencyResolver/PackageTest.php b/test/unit/DependencyResolver/PackageTest.php index ead73b4e..a4ae889e 100644 --- a/test/unit/DependencyResolver/PackageTest.php +++ b/test/unit/DependencyResolver/PackageTest.php @@ -180,16 +180,30 @@ public function testDownloadUrlMethodWithMultiItemListIsNotYetSupported(): void public function testFromComposerCompletePackageWithStringDownloadUrlMethod(): void { - self::fail('todo'); // @todo 436 + $composerCompletePackage = new CompletePackage('vendor/foo', '1.2.3.0', '1.2.3'); + $composerCompletePackage->setPhpExt(['download-url-method' => 'pre-packaged-binary']); + + self::assertSame( + [DownloadUrlMethod::PrePackagedBinary], + Package::fromComposerCompletePackage($composerCompletePackage)->supportedDownloadUrlMethods(), + ); } public function testFromComposerCompletePackageWithListDownloadUrlMethods(): void { - self::fail('todo'); // @todo 436 + $composerCompletePackage = new CompletePackage('vendor/foo', '1.2.3.0', '1.2.3'); + $composerCompletePackage->setPhpExt(['download-url-method' => ['pre-packaged-binary', 'composer-default']]); + + self::assertSame( + [DownloadUrlMethod::PrePackagedBinary, DownloadUrlMethod::ComposerDefaultDownload], + Package::fromComposerCompletePackage($composerCompletePackage)->supportedDownloadUrlMethods(), + ); } public function testFromComposerCompletePackageWithOmittedDownloadUrlMethod(): void { - self::fail('todo'); // @todo 436 + self::assertNull(Package::fromComposerCompletePackage( + new CompletePackage('vendor/foo', '1.2.3.0', '1.2.3'), + )->supportedDownloadUrlMethods()); } } diff --git a/test/unit/Downloading/DownloadUrlMethodTest.php b/test/unit/Downloading/DownloadUrlMethodTest.php index b3823d84..99e74c96 100644 --- a/test/unit/Downloading/DownloadUrlMethodTest.php +++ b/test/unit/Downloading/DownloadUrlMethodTest.php @@ -4,8 +4,10 @@ namespace Php\PieUnitTest\Downloading; +use Composer\Package\CompletePackage; use Composer\Package\CompletePackageInterface; use Php\Pie\DependencyResolver\Package; +use Php\Pie\Downloading\DownloadedPackage; use Php\Pie\Downloading\DownloadUrlMethod; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; @@ -19,6 +21,7 @@ use Php\Pie\Platform\WindowsCompiler; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; +use ValueError; use function array_key_first; @@ -241,21 +244,36 @@ public function testMultipleDownloadUrlMethods(): void public function testFromComposerPackageWhenPackageKeyWasDefined(): void { - self::fail('todo'); // @todo 436 + $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); + DownloadUrlMethod::PrePackagedBinary->writeToComposerPackage($composerPackage); + self::assertSame(DownloadUrlMethod::PrePackagedBinary, DownloadUrlMethod::fromComposerPackage($composerPackage)); } public function testFromComposerPackageWhenPackageKeyWasNotDefined(): void { - self::fail('todo'); // @todo 436 + $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); + + $this->expectException(ValueError::class); + DownloadUrlMethod::fromComposerPackage($composerPackage); } public function testFromDownloadedPackage(): void { - self::fail('todo'); // @todo 436 - } + $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); + DownloadUrlMethod::PrePackagedSourceDownload->writeToComposerPackage($composerPackage); + + $downloaded = DownloadedPackage::fromPackageAndExtractedPath( + new Package( + $composerPackage, + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foo'), + 'foo/bar', + '1.2.3', + null, + ), + '/path/to/extracted/source', + ); - public function testWriteToComposerPackageStoresDownloadUrlMethod(): void - { - self::fail('todo'); // @todo 436 + self::assertSame(DownloadUrlMethod::PrePackagedSourceDownload, DownloadUrlMethod::fromDownloadedPackage($downloaded)); } } diff --git a/test/unit/Platform/LibcFlavourTest.php b/test/unit/Platform/LibcFlavourTest.php index 9e5c898f..ea220e7f 100644 --- a/test/unit/Platform/LibcFlavourTest.php +++ b/test/unit/Platform/LibcFlavourTest.php @@ -6,18 +6,40 @@ use Php\Pie\Platform\LibcFlavour; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\RequiresOperatingSystemFamily; use PHPUnit\Framework\TestCase; +use function getenv; +use function putenv; +use function realpath; + +use const PATH_SEPARATOR; + #[CoversClass(LibcFlavour::class)] final class LibcFlavourTest extends TestCase { + private const GLIBC_PATH = __DIR__ . '/../../assets/fake-ldd/glibc'; + private const MUSL_PATH = __DIR__ . '/../../assets/fake-ldd/musl'; + + #[RequiresOperatingSystemFamily('Linux')] public function testGlibcFlavourIsDetected(): void { - self::fail('todo'); // @todo 436 + $oldPath = getenv('PATH'); + putenv('PATH=' . realpath(self::GLIBC_PATH) . PATH_SEPARATOR . $oldPath); + + self::assertSame(LibcFlavour::Gnu, LibcFlavour::detect()); + + putenv('PATH=' . $oldPath); } + #[RequiresOperatingSystemFamily('Linux')] public function testMuslFlavourIsDetected(): void { - self::fail('todo'); // @todo 436 + $oldPath = getenv('PATH'); + putenv('PATH=' . realpath(self::MUSL_PATH) . PATH_SEPARATOR . $oldPath); + + self::assertSame(LibcFlavour::Musl, LibcFlavour::detect()); + + putenv('PATH=' . $oldPath); } } diff --git a/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php b/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php index 61a43f97..321ac7d8 100644 --- a/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php +++ b/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php @@ -8,6 +8,7 @@ use Composer\Util\Platform; use Php\Pie\ExtensionName; use Php\Pie\Platform\Architecture; +use Php\Pie\Platform\DebugBuild; use Php\Pie\Platform\OperatingSystem; use Php\Pie\Platform\OperatingSystemFamily; use Php\Pie\Platform\TargetPhp\Exception\ExtensionIsNotLoaded; @@ -448,11 +449,23 @@ public function testBuildProviderNullWhenNotConfigured(): void public function testDebugBuildModeReturnsDebugWhenYes(): void { - self::fail('todo'); // @todo 436 + $phpBinary = $this->createPartialMock(PhpBinaryPath::class, ['phpinfo']); + + $phpBinary->expects(self::once()) + ->method('phpinfo') + ->willReturn('Debug Build => no'); + + self::assertSame(DebugBuild::NoDebug, $phpBinary->debugMode()); } public function testDebugBuildModeReturnsNoDebugWhenNo(): void { - self::fail('todo'); // @todo 436 + $phpBinary = $this->createPartialMock(PhpBinaryPath::class, ['phpinfo']); + + $phpBinary->expects(self::once()) + ->method('phpinfo') + ->willReturn('Debug Build => yes'); + + self::assertSame(DebugBuild::Debug, $phpBinary->debugMode()); } } diff --git a/test/unit/Platform/TargetPlatformTest.php b/test/unit/Platform/TargetPlatformTest.php index 60e22d06..48c79df3 100644 --- a/test/unit/Platform/TargetPlatformTest.php +++ b/test/unit/Platform/TargetPlatformTest.php @@ -110,6 +110,9 @@ public function testLinuxPlatform(): void public function testLibcFlavourIsMemoized(): void { - self::fail('todo'); // @todo 436 + self::assertSame( + TargetPlatform::fromPhpBinaryPath(PhpBinaryPath::fromCurrentProcess(), null)->libcFlavour(), + TargetPlatform::fromPhpBinaryPath(PhpBinaryPath::fromCurrentProcess(), null)->libcFlavour(), + ); } } From 27ac524b97d5ee24444a2d9b3dd6feb77d9b773b Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Fri, 16 Jan 2026 16:37:14 +0000 Subject: [PATCH 12/16] 436: include OS in pre-packaged-binary package names --- docs/extension-maintainers.md | 27 ++++++++++--------- src/Platform/PrePackagedBinaryAssetName.php | 12 ++++++--- .../Downloading/DownloadUrlMethodTest.php | 12 ++++----- .../PrePackagedBinaryAssetNameTest.php | 22 +++++++-------- 4 files changed, 39 insertions(+), 34 deletions(-) diff --git a/docs/extension-maintainers.md b/docs/extension-maintainers.md index 1952009a..1c24fcc8 100644 --- a/docs/extension-maintainers.md +++ b/docs/extension-maintainers.md @@ -244,30 +244,31 @@ string may be used. compatibility with PECL packages) * Using `pre-packaged-binary` will locate a tgz or zip archive in the release assets list based on matching one of the following naming conventions: - * `php_{ExtensionName}-{Version}_php{PhpVersion}-{Arch}-{Libc}-{Debug}-{TSMode}.{Format}` + * `php_{ExtensionName}-{Version}_php{PhpVersion}-{Arch}-{OS}-{Libc}-{Debug}-{TSMode}.{Format}` * The replacements are: * `{ExtensionName}` the name of your extension, e.g. `xdebug` (hint: this is not your Composer package name!) * `{PhpVersion}` the major and minor version of PHP, e.g. `8.5` * `{Version}` the version of your extension, e.g. `1.20.1` * `{Arch}` the architecture of the binary, one of `x86`, `x86_64`, `arm64` + * `{OS}` the operating system, one of `windows`, `darwin`, `linux`, `bsd`, `solaris`, `unknown` * `{Libc}` the libc flavour, one of `glibc`, `musl` * `{Debug}` the debug mode, one of `debug`, `nodebug` (or omitted) * `{TSMode}` the thread safety mode, one of `zts`, `nts` (or omitted) * `{Format}` the archive format, one of `zip`, `tgz` * Some examples of valid asset names: - * `php_xdebug-4.1_php8.4-x86_64-glibc.tgz` (or `php_xdebug-4.1_php8.4-x86_64-glibc-nts.tgz`) - * `php_xdebug-4.1_php8.4-x86_64-musl.tgz` (or `php_xdebug-4.1_php8.4-x86_64-musl-nts.tgz`) - * `php_xdebug-4.1_php8.4-arm64-glibc.tgz` (or `php_xdebug-4.1_php8.4-arm64-glibc-nts.tgz`) - * `php_xdebug-4.1_php8.4-arm64-musl.tgz` (or `php_xdebug-4.1_php8.4-arm64-musl-nts.tgz`) - * `php_xdebug-4.1_php8.4-x86_64-glibc-zts.tgz` - * `php_xdebug-4.1_php8.4-x86_64-musl-zts.tgz` - * `php_xdebug-4.1_php8.4-arm64-glibc-zts.tgz` - * `php_xdebug-4.1_php8.4-arm64-musl-zts.tgz` - * `php_xdebug-4.1_php8.4-x86_64-glibc-debug.tgz` - * `php_xdebug-4.1_php8.4-x86_64-musl-debug.tgz` - * `php_xdebug-4.1_php8.4-arm64-glibc-debug.tgz` - * `php_xdebug-4.1_php8.4-arm64-musl-debug.tgz` + * `php_xdebug-4.1_php8.4-x86_64-linux-glibc.tgz` (or `php_xdebug-4.1_php8.4-x86_64-glibc-nts.tgz`) + * `php_xdebug-4.1_php8.4-x86_64-linux-musl.tgz` (or `php_xdebug-4.1_php8.4-x86_64-musl-nts.tgz`) + * `php_xdebug-4.1_php8.4-arm64-linux-glibc.tgz` (or `php_xdebug-4.1_php8.4-arm64-glibc-nts.tgz`) + * `php_xdebug-4.1_php8.4-arm64-linux-musl.tgz` (or `php_xdebug-4.1_php8.4-arm64-musl-nts.tgz`) + * `php_xdebug-4.1_php8.4-x86_64-linux-glibc-zts.tgz` + * `php_xdebug-4.1_php8.4-x86_64-linux-musl-zts.tgz` + * `php_xdebug-4.1_php8.4-arm64-linux-glibc-zts.tgz` + * `php_xdebug-4.1_php8.4-arm64-linux-musl-zts.tgz` + * `php_xdebug-4.1_php8.4-x86_64-linux-glibc-debug.tgz` + * `php_xdebug-4.1_php8.4-x86_64-linux-musl-debug.tgz` + * `php_xdebug-4.1_php8.4-arm64-linux-glibc-debug.tgz` + * `php_xdebug-4.1_php8.4-arm64-linux-musl-debug.tgz` * It is recommended that `pre-packaged-binary` is combined with `composer-default` as a fallback mechanism, if a particular combination is supported, but not pre-packaged on the release, e.g. `"download-url-method": ["pre-packaged-binary", "composer-default"]`. diff --git a/src/Platform/PrePackagedBinaryAssetName.php b/src/Platform/PrePackagedBinaryAssetName.php index 2332ba25..eda3f947 100644 --- a/src/Platform/PrePackagedBinaryAssetName.php +++ b/src/Platform/PrePackagedBinaryAssetName.php @@ -23,41 +23,45 @@ public static function packageNames(TargetPlatform $targetPlatform, Package $pac { return array_values(array_unique([ strtolower(sprintf( - 'php_%s-%s_php%s-%s-%s%s%s.zip', + 'php_%s-%s_php%s-%s-%s-%s%s%s.zip', $package->extensionName()->name(), $package->version(), $targetPlatform->phpBinaryPath->majorMinorVersion(), $targetPlatform->architecture->name, + $targetPlatform->operatingSystemFamily->value, $targetPlatform->libcFlavour()->value, $targetPlatform->phpBinaryPath->debugMode() === DebugBuild::Debug ? '-debug' : '', $targetPlatform->threadSafety === ThreadSafetyMode::ThreadSafe ? '-zts' : '', )), strtolower(sprintf( - 'php_%s-%s_php%s-%s-%s%s%s.tgz', + 'php_%s-%s_php%s-%s-%s-%s%s%s.tgz', $package->extensionName()->name(), $package->version(), $targetPlatform->phpBinaryPath->majorMinorVersion(), $targetPlatform->architecture->name, + $targetPlatform->operatingSystemFamily->value, $targetPlatform->libcFlavour()->value, $targetPlatform->phpBinaryPath->debugMode() === DebugBuild::Debug ? '-debug' : '', $targetPlatform->threadSafety === ThreadSafetyMode::ThreadSafe ? '-zts' : '', )), strtolower(sprintf( - 'php_%s-%s_php%s-%s-%s%s%s.zip', + 'php_%s-%s_php%s-%s-%s-%s%s%s.zip', $package->extensionName()->name(), $package->version(), $targetPlatform->phpBinaryPath->majorMinorVersion(), $targetPlatform->architecture->name, + $targetPlatform->operatingSystemFamily->value, $targetPlatform->libcFlavour()->value, $targetPlatform->phpBinaryPath->debugMode() === DebugBuild::Debug ? '-debug' : '', $targetPlatform->threadSafety === ThreadSafetyMode::ThreadSafe ? '-zts' : '-nts', )), strtolower(sprintf( - 'php_%s-%s_php%s-%s-%s%s%s.tgz', + 'php_%s-%s_php%s-%s-%s-%s%s%s.tgz', $package->extensionName()->name(), $package->version(), $targetPlatform->phpBinaryPath->majorMinorVersion(), $targetPlatform->architecture->name, + $targetPlatform->operatingSystemFamily->value, $targetPlatform->libcFlavour()->value, $targetPlatform->phpBinaryPath->debugMode() === DebugBuild::Debug ? '-debug' : '', $targetPlatform->threadSafety === ThreadSafetyMode::ThreadSafe ? '-zts' : '-nts', diff --git a/test/unit/Downloading/DownloadUrlMethodTest.php b/test/unit/Downloading/DownloadUrlMethodTest.php index 99e74c96..b1113b9d 100644 --- a/test/unit/Downloading/DownloadUrlMethodTest.php +++ b/test/unit/Downloading/DownloadUrlMethodTest.php @@ -144,8 +144,8 @@ public function testPrePackagedBinaryDownloads(): void self::assertSame( [ - 'php_bar-1.2.3_php8.3-x86_64-glibc-debug-zts.zip', - 'php_bar-1.2.3_php8.3-x86_64-glibc-debug-zts.tgz', + 'php_bar-1.2.3_php8.3-x86_64-linux-glibc-debug-zts.zip', + 'php_bar-1.2.3_php8.3-x86_64-linux-glibc-debug-zts.tgz', ], $downloadUrlMethod->possibleAssetNames($package, $targetPlatform), ); @@ -218,10 +218,10 @@ public function testMultipleDownloadUrlMethods(): void self::assertSame(DownloadUrlMethod::PrePackagedBinary, $firstMethod); self::assertSame( [ - 'php_bar-1.2.3_php8.3-x86_64-glibc-debug.zip', - 'php_bar-1.2.3_php8.3-x86_64-glibc-debug.tgz', - 'php_bar-1.2.3_php8.3-x86_64-glibc-debug-nts.zip', - 'php_bar-1.2.3_php8.3-x86_64-glibc-debug-nts.tgz', + 'php_bar-1.2.3_php8.3-x86_64-linux-glibc-debug.zip', + 'php_bar-1.2.3_php8.3-x86_64-linux-glibc-debug.tgz', + 'php_bar-1.2.3_php8.3-x86_64-linux-glibc-debug-nts.zip', + 'php_bar-1.2.3_php8.3-x86_64-linux-glibc-debug-nts.tgz', ], $firstMethod->possibleAssetNames($package, $targetPlatform), ); diff --git a/test/unit/Platform/PrePackagedBinaryAssetNameTest.php b/test/unit/Platform/PrePackagedBinaryAssetNameTest.php index fce2d46d..022312c6 100644 --- a/test/unit/Platform/PrePackagedBinaryAssetNameTest.php +++ b/test/unit/Platform/PrePackagedBinaryAssetNameTest.php @@ -41,10 +41,10 @@ public function testPackageNamesNts(): void $libc = $targetPlatform->libcFlavour(); self::assertSame( [ - 'php_foobar-1.2.3_php8.2-x86_64-' . $libc->value . '.zip', - 'php_foobar-1.2.3_php8.2-x86_64-' . $libc->value . '.tgz', - 'php_foobar-1.2.3_php8.2-x86_64-' . $libc->value . '-nts.zip', - 'php_foobar-1.2.3_php8.2-x86_64-' . $libc->value . '-nts.tgz', + 'php_foobar-1.2.3_php8.2-x86_64-linux-' . $libc->value . '.zip', + 'php_foobar-1.2.3_php8.2-x86_64-linux-' . $libc->value . '.tgz', + 'php_foobar-1.2.3_php8.2-x86_64-linux-' . $libc->value . '-nts.zip', + 'php_foobar-1.2.3_php8.2-x86_64-linux-' . $libc->value . '-nts.tgz', ], PrePackagedBinaryAssetName::packageNames( $targetPlatform, @@ -79,8 +79,8 @@ public function testPackageNamesZts(): void $libc = $targetPlatform->libcFlavour(); self::assertSame( [ - 'php_foobar-1.2.3_php8.3-x86_64-' . $libc->value . '-zts.zip', - 'php_foobar-1.2.3_php8.3-x86_64-' . $libc->value . '-zts.tgz', + 'php_foobar-1.2.3_php8.3-x86_64-linux-' . $libc->value . '-zts.zip', + 'php_foobar-1.2.3_php8.3-x86_64-linux-' . $libc->value . '-zts.tgz', ], PrePackagedBinaryAssetName::packageNames( $targetPlatform, @@ -104,7 +104,7 @@ public function testPackageNamesDebug(): void $targetPlatform = new TargetPlatform( OperatingSystem::NonWindows, - OperatingSystemFamily::Linux, + OperatingSystemFamily::Darwin, $php, Architecture::arm64, ThreadSafetyMode::NonThreadSafe, @@ -115,10 +115,10 @@ public function testPackageNamesDebug(): void $libc = $targetPlatform->libcFlavour(); self::assertSame( [ - 'php_foobar-1.2.3_php8.4-arm64-' . $libc->value . '-debug.zip', - 'php_foobar-1.2.3_php8.4-arm64-' . $libc->value . '-debug.tgz', - 'php_foobar-1.2.3_php8.4-arm64-' . $libc->value . '-debug-nts.zip', - 'php_foobar-1.2.3_php8.4-arm64-' . $libc->value . '-debug-nts.tgz', + 'php_foobar-1.2.3_php8.4-arm64-darwin-' . $libc->value . '-debug.zip', + 'php_foobar-1.2.3_php8.4-arm64-darwin-' . $libc->value . '-debug.tgz', + 'php_foobar-1.2.3_php8.4-arm64-darwin-' . $libc->value . '-debug-nts.zip', + 'php_foobar-1.2.3_php8.4-arm64-darwin-' . $libc->value . '-debug-nts.tgz', ], PrePackagedBinaryAssetName::packageNames( $targetPlatform, From 87b2cd8eee6b75712620471367ded16fb0788701 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Fri, 16 Jan 2026 16:52:43 +0000 Subject: [PATCH 13/16] 436: detect bsdlibc on OSX --- docs/extension-maintainers.md | 2 +- src/Platform/LibcFlavour.php | 6 ++++++ test/assets/fake-ldd/bsdlibc/otool | 6 ++++++ test/unit/Platform/LibcFlavourTest.php | 18 ++++++++++++++++++ 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100755 test/assets/fake-ldd/bsdlibc/otool diff --git a/docs/extension-maintainers.md b/docs/extension-maintainers.md index 1c24fcc8..1b2f1e8c 100644 --- a/docs/extension-maintainers.md +++ b/docs/extension-maintainers.md @@ -252,7 +252,7 @@ string may be used. * `{Version}` the version of your extension, e.g. `1.20.1` * `{Arch}` the architecture of the binary, one of `x86`, `x86_64`, `arm64` * `{OS}` the operating system, one of `windows`, `darwin`, `linux`, `bsd`, `solaris`, `unknown` - * `{Libc}` the libc flavour, one of `glibc`, `musl` + * `{Libc}` the libc flavour, one of `glibc`, `musl`, `bsdlibc` * `{Debug}` the debug mode, one of `debug`, `nodebug` (or omitted) * `{TSMode}` the thread safety mode, one of `zts`, `nts` (or omitted) * `{Format}` the archive format, one of `zip`, `tgz` diff --git a/src/Platform/LibcFlavour.php b/src/Platform/LibcFlavour.php index b12bfa66..7d6fd39b 100644 --- a/src/Platform/LibcFlavour.php +++ b/src/Platform/LibcFlavour.php @@ -15,11 +15,17 @@ enum LibcFlavour: string { case Gnu = 'glibc'; case Musl = 'musl'; + case Bsd = 'bsdlibc'; public static function detect(): self { $executableFinder = new ExecutableFinder(); + $otool = $executableFinder->find('otool'); + if ($otool !== null) { + return self::Bsd; + } + $lddPath = $executableFinder->find('ldd'); $lsPath = $executableFinder->find('ls'); diff --git a/test/assets/fake-ldd/bsdlibc/otool b/test/assets/fake-ldd/bsdlibc/otool new file mode 100755 index 00000000..69ef7881 --- /dev/null +++ b/test/assets/fake-ldd/bsdlibc/otool @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +echo "/bin/ls: + /usr/lib/libutil.dylib (compatibility version 1.0.0, current version 1.0.0) + /usr/lib/libncurses.5.4.dylib (compatibility version 5.4.0, current version 5.4.0) + /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1356.0.0)" diff --git a/test/unit/Platform/LibcFlavourTest.php b/test/unit/Platform/LibcFlavourTest.php index ea220e7f..231462e8 100644 --- a/test/unit/Platform/LibcFlavourTest.php +++ b/test/unit/Platform/LibcFlavourTest.php @@ -20,6 +20,7 @@ final class LibcFlavourTest extends TestCase { private const GLIBC_PATH = __DIR__ . '/../../assets/fake-ldd/glibc'; private const MUSL_PATH = __DIR__ . '/../../assets/fake-ldd/musl'; + private const BSD_PATH = __DIR__ . '/../../assets/fake-ldd/bsdlibc'; #[RequiresOperatingSystemFamily('Linux')] public function testGlibcFlavourIsDetected(): void @@ -42,4 +43,21 @@ public function testMuslFlavourIsDetected(): void putenv('PATH=' . $oldPath); } + + #[RequiresOperatingSystemFamily('Linux')] + public function testBsdlibcFlavourIsDetected(): void + { + $oldPath = getenv('PATH'); + putenv('PATH=' . realpath(self::BSD_PATH) . PATH_SEPARATOR . $oldPath); + + self::assertSame(LibcFlavour::Bsd, LibcFlavour::detect()); + + putenv('PATH=' . $oldPath); + } + + #[RequiresOperatingSystemFamily('Darwin')] + public function testBsdlibcFlavourIsDetectedOnRealOsx(): void + { + self::assertSame(LibcFlavour::Bsd, LibcFlavour::detect()); + } } From e7880d42e3eec2a081c5915d13075fac86662829 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 26 Jan 2026 11:10:48 +0000 Subject: [PATCH 14/16] 436: download-url-methods should have at list one defined --- resources/composer-json-php-ext-schema.json | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/composer-json-php-ext-schema.json b/resources/composer-json-php-ext-schema.json index 2162d763..9095708b 100644 --- a/resources/composer-json-php-ext-schema.json +++ b/resources/composer-json-php-ext-schema.json @@ -59,6 +59,7 @@ "enum": ["composer-default", "pre-packaged-source", "pre-packaged-binary"], "example": ["pre-packaged-binary", "composer-default"] }, + "minItems": 1, "default": ["composer-default"] } ] From 77e79609a082ce1ae958f0db373c38a217e9df0a Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 29 Jan 2026 14:58:45 +0000 Subject: [PATCH 15/16] 436: removed now-redundant downloadUrlMethod() tests (method was removed) --- test/unit/DependencyResolver/PackageTest.php | 30 -------------------- 1 file changed, 30 deletions(-) diff --git a/test/unit/DependencyResolver/PackageTest.php b/test/unit/DependencyResolver/PackageTest.php index a4ae889e..14576c3b 100644 --- a/test/unit/DependencyResolver/PackageTest.php +++ b/test/unit/DependencyResolver/PackageTest.php @@ -148,36 +148,6 @@ public function testFromComposerCompletePackageWithBuildPath(): void self::assertSame('some/subdirectory/path/', $package->buildPath()); } - public function testDownloadUrlMethodWithStringHasValidDownloadUrlMethod(): void - { - $composerCompletePackage = new CompletePackage('vendor/foo', '1.2.3.0', '1.2.3'); - $composerCompletePackage->setPhpExt(['download-url-method' => 'composer-default']); - - $package = Package::fromComposerCompletePackage($composerCompletePackage); - - self::assertSame(DownloadUrlMethod::ComposerDefaultDownload, $package->downloadUrlMethod()); - } - - public function testDownloadUrlMethodWithSingleItemListHasValidDownloadUrlMethod(): void - { - $composerCompletePackage = new CompletePackage('vendor/foo', '1.2.3.0', '1.2.3'); - $composerCompletePackage->setPhpExt(['download-url-method' => ['composer-default']]); - - $package = Package::fromComposerCompletePackage($composerCompletePackage); - - self::assertSame(DownloadUrlMethod::ComposerDefaultDownload, $package->downloadUrlMethod()); - } - - public function testDownloadUrlMethodWithMultiItemListIsNotYetSupported(): void - { - $composerCompletePackage = new CompletePackage('vendor/foo', '1.2.3.0', '1.2.3'); - $composerCompletePackage->setPhpExt(['download-url-method' => ['pre-packaged-source', 'composer-default']]); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('This extension requires a newer version of PIE. Multiple download-url-methods are not supported until PIE 1.4.0.'); - Package::fromComposerCompletePackage($composerCompletePackage); - } - public function testFromComposerCompletePackageWithStringDownloadUrlMethod(): void { $composerCompletePackage = new CompletePackage('vendor/foo', '1.2.3.0', '1.2.3'); From 74b9986def43f49b0c3ffb60f3f47b80212f62e7 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 29 Jan 2026 14:59:54 +0000 Subject: [PATCH 16/16] 436: updated to Composer 2.9.5 --- composer.json | 14 ++-- composer.lock | 180 +++++++++++++++++++++--------------------- phpstan-baseline.neon | 14 +--- 3 files changed, 98 insertions(+), 110 deletions(-) diff --git a/composer.json b/composer.json index 1bba5487..d50e149f 100644 --- a/composer.json +++ b/composer.json @@ -28,26 +28,26 @@ ], "require": { "php": "8.1.*||8.2.*||8.3.*||8.4.*||8.5.*", - "composer/composer": "^2.9.2", + "composer/composer": "^2.9.5", "composer/pcre": "^3.3.2", "composer/semver": "^3.4.4", "fidry/cpu-core-counter": "^1.3.0", "illuminate/container": "^10.49.0", "psr/container": "^2.0.2", - "symfony/console": "^6.4.27", - "symfony/event-dispatcher": "^6.4.25", - "symfony/process": "^6.4.26", + "symfony/console": "^6.4.32", + "symfony/event-dispatcher": "^6.4.32", + "symfony/process": "^6.4.33", "thephpf/attestation": "^0.0.5", "webmozart/assert": "^1.12.1" }, "require-dev": { "ext-openssl": "*", - "behat/behat": "^3.27.0", + "behat/behat": "^3.29.0", "bnf/phpstan-psr-container": "^1.1", "doctrine/coding-standard": "^14.0.0", - "phpstan/phpstan": "^2.1.32", + "phpstan/phpstan": "^2.1.37", "phpstan/phpstan-webmozart-assert": "^2.0", - "phpunit/phpunit": "^10.5.58" + "phpunit/phpunit": "^10.5.63" }, "replace": { "symfony/polyfill-php81": "*", diff --git a/composer.lock b/composer.lock index 92f0923c..61893d85 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": "ab3e2edb8ee4fdb637409ca1149f7d0a", + "content-hash": "56317f73c3bc8df6b8b204c246666339", "packages": [ { "name": "composer/ca-bundle", @@ -149,16 +149,16 @@ }, { "name": "composer/composer", - "version": "2.9.3", + "version": "2.9.5", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "fb3bee27676fd852a8a11ebbb1de19b4dada5aba" + "reference": "72a8f8e653710e18d83e5dd531eb5a71fc3223e6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/fb3bee27676fd852a8a11ebbb1de19b4dada5aba", - "reference": "fb3bee27676fd852a8a11ebbb1de19b4dada5aba", + "url": "https://api.github.com/repos/composer/composer/zipball/72a8f8e653710e18d83e5dd531eb5a71fc3223e6", + "reference": "72a8f8e653710e18d83e5dd531eb5a71fc3223e6", "shasum": "" }, "require": { @@ -246,7 +246,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.9.3" + "source": "https://github.com/composer/composer/tree/2.9.5" }, "funding": [ { @@ -258,7 +258,7 @@ "type": "github" } ], - "time": "2025-12-30T12:40:17+00:00" + "time": "2026-01-29T10:40:53+00:00" }, { "name": "composer/metadata-minifier", @@ -793,16 +793,16 @@ }, { "name": "justinrainbow/json-schema", - "version": "6.6.3", + "version": "6.6.4", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "134e98916fa2f663afa623970af345cd788e8967" + "reference": "2eeb75d21cf73211335888e7f5e6fd7440723ec7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/134e98916fa2f663afa623970af345cd788e8967", - "reference": "134e98916fa2f663afa623970af345cd788e8967", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/2eeb75d21cf73211335888e7f5e6fd7440723ec7", + "reference": "2eeb75d21cf73211335888e7f5e6fd7440723ec7", "shasum": "" }, "require": { @@ -862,9 +862,9 @@ ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/6.6.3" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.6.4" }, - "time": "2025-12-02T10:21:33+00:00" + "time": "2025-12-19T15:01:32+00:00" }, { "name": "marc-mabe/php-enum", @@ -1391,16 +1391,16 @@ }, { "name": "symfony/console", - "version": "v6.4.31", + "version": "v6.4.32", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "f9f8a889f54c264f9abac3fc0f7a371ffca51997" + "reference": "0bc2199c6c1f05276b05956f1ddc63f6d7eb5fc3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/f9f8a889f54c264f9abac3fc0f7a371ffca51997", - "reference": "f9f8a889f54c264f9abac3fc0f7a371ffca51997", + "url": "https://api.github.com/repos/symfony/console/zipball/0bc2199c6c1f05276b05956f1ddc63f6d7eb5fc3", + "reference": "0bc2199c6c1f05276b05956f1ddc63f6d7eb5fc3", "shasum": "" }, "require": { @@ -1465,7 +1465,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.31" + "source": "https://github.com/symfony/console/tree/v6.4.32" }, "funding": [ { @@ -1485,7 +1485,7 @@ "type": "tidelift" } ], - "time": "2025-12-22T08:30:34+00:00" + "time": "2026-01-13T08:45:59+00:00" }, { "name": "symfony/deprecation-contracts", @@ -1556,16 +1556,16 @@ }, { "name": "symfony/event-dispatcher", - "version": "v6.4.25", + "version": "v6.4.32", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "b0cf3162020603587363f0551cd3be43958611ff" + "reference": "99d7e101826e6610606b9433248f80c1997cd20b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b0cf3162020603587363f0551cd3be43958611ff", - "reference": "b0cf3162020603587363f0551cd3be43958611ff", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/99d7e101826e6610606b9433248f80c1997cd20b", + "reference": "99d7e101826e6610606b9433248f80c1997cd20b", "shasum": "" }, "require": { @@ -1616,7 +1616,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.25" + "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.32" }, "funding": [ { @@ -1636,7 +1636,7 @@ "type": "tidelift" } ], - "time": "2025-08-13T09:41:44+00:00" + "time": "2026-01-05T11:13:48+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -1786,16 +1786,16 @@ }, { "name": "symfony/finder", - "version": "v6.4.31", + "version": "v6.4.33", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "5547f2e1f0ca8e2e7abe490156b62da778cfbe2b" + "reference": "24965ca011dac87431729640feef8bcf7b5523e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/5547f2e1f0ca8e2e7abe490156b62da778cfbe2b", - "reference": "5547f2e1f0ca8e2e7abe490156b62da778cfbe2b", + "url": "https://api.github.com/repos/symfony/finder/zipball/24965ca011dac87431729640feef8bcf7b5523e0", + "reference": "24965ca011dac87431729640feef8bcf7b5523e0", "shasum": "" }, "require": { @@ -1830,7 +1830,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.4.31" + "source": "https://github.com/symfony/finder/tree/v6.4.33" }, "funding": [ { @@ -1850,7 +1850,7 @@ "type": "tidelift" } ], - "time": "2025-12-11T14:52:17+00:00" + "time": "2026-01-26T13:03:48+00:00" }, { "name": "symfony/polyfill-ctype", @@ -2269,16 +2269,16 @@ }, { "name": "symfony/process", - "version": "v6.4.31", + "version": "v6.4.33", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "8541b7308fca001320e90bca8a73a28aa5604a6e" + "reference": "c46e854e79b52d07666e43924a20cb6dc546644e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/8541b7308fca001320e90bca8a73a28aa5604a6e", - "reference": "8541b7308fca001320e90bca8a73a28aa5604a6e", + "url": "https://api.github.com/repos/symfony/process/zipball/c46e854e79b52d07666e43924a20cb6dc546644e", + "reference": "c46e854e79b52d07666e43924a20cb6dc546644e", "shasum": "" }, "require": { @@ -2310,7 +2310,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.4.31" + "source": "https://github.com/symfony/process/tree/v6.4.33" }, "funding": [ { @@ -2330,7 +2330,7 @@ "type": "tidelift" } ], - "time": "2025-12-15T19:26:35+00:00" + "time": "2026-01-23T16:02:12+00:00" }, { "name": "symfony/service-contracts", @@ -3264,16 +3264,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "2.3.0", + "version": "2.3.2", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495" + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1e0cd5370df5dd2e556a36b9c62f62e555870495", - "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a", "shasum": "" }, "require": { @@ -3305,17 +3305,17 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2" }, - "time": "2025-08-30T15:50:23+00:00" + "time": "2026-01-25T14:56:51+00:00" }, { "name": "phpstan/phpstan", - "version": "2.1.33", + "version": "2.1.37", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9e800e6bee7d5bd02784d4c6069b48032d16224f", - "reference": "9e800e6bee7d5bd02784d4c6069b48032d16224f", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/28cd424c5ea984128c95cfa7ea658808e8954e49", + "reference": "28cd424c5ea984128c95cfa7ea658808e8954e49", "shasum": "" }, "require": { @@ -3360,7 +3360,7 @@ "type": "github" } ], - "time": "2025-12-05T10:24:31+00:00" + "time": "2026-01-24T08:21:55+00:00" }, { "name": "phpstan/phpstan-webmozart-assert", @@ -3736,16 +3736,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.58", + "version": "10.5.63", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca" + "reference": "33198268dad71e926626b618f3ec3966661e4d90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e24fb46da450d8e6a5788670513c1af1424f16ca", - "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/33198268dad71e926626b618f3ec3966661e4d90", + "reference": "33198268dad71e926626b618f3ec3966661e4d90", "shasum": "" }, "require": { @@ -3766,7 +3766,7 @@ "phpunit/php-timer": "^6.0.0", "sebastian/cli-parser": "^2.0.1", "sebastian/code-unit": "^2.0.0", - "sebastian/comparator": "^5.0.4", + "sebastian/comparator": "^5.0.5", "sebastian/diff": "^5.1.1", "sebastian/environment": "^6.1.0", "sebastian/exporter": "^5.1.4", @@ -3817,7 +3817,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.58" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.63" }, "funding": [ { @@ -3841,7 +3841,7 @@ "type": "tidelift" } ], - "time": "2025-09-28T12:04:46+00:00" + "time": "2026-01-27T05:48:37+00:00" }, { "name": "sebastian/cli-parser", @@ -4013,16 +4013,16 @@ }, { "name": "sebastian/comparator", - "version": "5.0.4", + "version": "5.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e" + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e8e53097718d2b53cfb2aa859b06a41abf58c62e", - "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d", "shasum": "" }, "require": { @@ -4078,7 +4078,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.4" + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.5" }, "funding": [ { @@ -4098,7 +4098,7 @@ "type": "tidelift" } ], - "time": "2025-09-07T05:25:07+00:00" + "time": "2026-01-24T09:25:16+00:00" }, { "name": "sebastian/complexity", @@ -4798,32 +4798,32 @@ }, { "name": "slevomat/coding-standard", - "version": "8.25.1", + "version": "8.27.1", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "4caa5ec5a30b84b2305e80159c710d437f40cc40" + "reference": "29bdaee8b65e7ed2b8e702b01852edba8bae1769" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/4caa5ec5a30b84b2305e80159c710d437f40cc40", - "reference": "4caa5ec5a30b84b2305e80159c710d437f40cc40", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/29bdaee8b65e7ed2b8e702b01852edba8bae1769", + "reference": "29bdaee8b65e7ed2b8e702b01852edba8bae1769", "shasum": "" }, "require": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.2.0", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.2.0", "php": "^7.4 || ^8.0", - "phpstan/phpdoc-parser": "^2.3.0", + "phpstan/phpdoc-parser": "^2.3.1", "squizlabs/php_codesniffer": "^4.0.1" }, "require-dev": { - "phing/phing": "3.0.1|3.1.0", + "phing/phing": "3.0.1|3.1.1", "php-parallel-lint/php-parallel-lint": "1.4.0", - "phpstan/phpstan": "2.1.32", + "phpstan/phpstan": "2.1.37", "phpstan/phpstan-deprecation-rules": "2.0.3", - "phpstan/phpstan-phpunit": "2.0.8", + "phpstan/phpstan-phpunit": "2.0.12", "phpstan/phpstan-strict-rules": "2.0.7", - "phpunit/phpunit": "9.6.8|10.5.48|11.4.4|11.5.36|12.4.4" + "phpunit/phpunit": "9.6.31|10.5.60|11.4.4|11.5.49|12.5.7" }, "type": "phpcodesniffer-standard", "extra": { @@ -4847,7 +4847,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.25.1" + "source": "https://github.com/slevomat/coding-standard/tree/8.27.1" }, "funding": [ { @@ -4859,7 +4859,7 @@ "type": "tidelift" } ], - "time": "2025-11-25T18:01:43+00:00" + "time": "2026-01-25T15:57:07+00:00" }, { "name": "squizlabs/php_codesniffer", @@ -4942,16 +4942,16 @@ }, { "name": "symfony/config", - "version": "v6.4.28", + "version": "v6.4.32", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "15947c18ef3ddb0b2f4ec936b9e90e2520979f62" + "reference": "d445badf0ad2c2a492e38c0378c39997a56ef97b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/15947c18ef3ddb0b2f4ec936b9e90e2520979f62", - "reference": "15947c18ef3ddb0b2f4ec936b9e90e2520979f62", + "url": "https://api.github.com/repos/symfony/config/zipball/d445badf0ad2c2a492e38c0378c39997a56ef97b", + "reference": "d445badf0ad2c2a492e38c0378c39997a56ef97b", "shasum": "" }, "require": { @@ -4997,7 +4997,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v6.4.28" + "source": "https://github.com/symfony/config/tree/v6.4.32" }, "funding": [ { @@ -5017,20 +5017,20 @@ "type": "tidelift" } ], - "time": "2025-11-01T19:52:02+00:00" + "time": "2026-01-13T08:40:30+00:00" }, { "name": "symfony/dependency-injection", - "version": "v6.4.31", + "version": "v6.4.32", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "10058832a74a33648870aa2057e3fdc8796a6566" + "reference": "b17882e933c4c606620247b6708ab53aa3b88753" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/10058832a74a33648870aa2057e3fdc8796a6566", - "reference": "10058832a74a33648870aa2057e3fdc8796a6566", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/b17882e933c4c606620247b6708ab53aa3b88753", + "reference": "b17882e933c4c606620247b6708ab53aa3b88753", "shasum": "" }, "require": { @@ -5082,7 +5082,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v6.4.31" + "source": "https://github.com/symfony/dependency-injection/tree/v6.4.32" }, "funding": [ { @@ -5102,20 +5102,20 @@ "type": "tidelift" } ], - "time": "2025-12-23T13:34:50+00:00" + "time": "2026-01-23T10:54:33+00:00" }, { "name": "symfony/translation", - "version": "v6.4.31", + "version": "v6.4.32", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "81579408ecf7dc5aa2d8462a6d5c3a430a80e6f2" + "reference": "d6cc8e2fdd484f2f41d25938b0e8e3915de3cfbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/81579408ecf7dc5aa2d8462a6d5c3a430a80e6f2", - "reference": "81579408ecf7dc5aa2d8462a6d5c3a430a80e6f2", + "url": "https://api.github.com/repos/symfony/translation/zipball/d6cc8e2fdd484f2f41d25938b0e8e3915de3cfbc", + "reference": "d6cc8e2fdd484f2f41d25938b0e8e3915de3cfbc", "shasum": "" }, "require": { @@ -5181,7 +5181,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v6.4.31" + "source": "https://github.com/symfony/translation/tree/v6.4.32" }, "funding": [ { @@ -5201,7 +5201,7 @@ "type": "tidelift" } ], - "time": "2025-12-18T11:37:55+00:00" + "time": "2026-01-12T19:15:33+00:00" }, { "name": "symfony/translation-contracts", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 1299aa47..73debb0b 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -414,18 +414,6 @@ parameters: count: 1 path: test/unit/ComposerIntegration/VendorCleanupTest.php - - - message: '#^Parameter \#1 \$phpExt of method Composer\\Package\\Package\:\:setPhpExt\(\) expects array\{extension\-name\?\: string, priority\?\: int, support\-zts\?\: bool, support\-nts\?\: bool, build\-path\?\: string\|null, download\-url\-method\?\: string, os\-families\?\: non\-empty\-list\, os\-families\-exclude\?\: non\-empty\-list\, \.\.\.\}\|null, array\{download\-url\-method\: array\{''composer\-default''\}\} given\.$#' - identifier: argument.type - count: 1 - path: test/unit/DependencyResolver/PackageTest.php - - - - message: '#^Parameter \#1 \$phpExt of method Composer\\Package\\Package\:\:setPhpExt\(\) expects array\{extension\-name\?\: string, priority\?\: int, support\-zts\?\: bool, support\-nts\?\: bool, build\-path\?\: string\|null, download\-url\-method\?\: string, os\-families\?\: non\-empty\-list\, os\-families\-exclude\?\: non\-empty\-list\, \.\.\.\}\|null, array\{download\-url\-method\: array\{''pre\-packaged\-source'', ''composer\-default''\}\} given\.$#' - identifier: argument.type - count: 1 - path: test/unit/DependencyResolver/PackageTest.php - - message: '#^Parameter \#2 \$extractedSourcePath of static method Php\\Pie\\Downloading\\DownloadedPackage\:\:fromPackageAndExtractedPath\(\) expects string, string\|false given\.$#' identifier: argument.type @@ -433,7 +421,7 @@ parameters: path: test/unit/Downloading/DownloadedPackageTest.php - - message: '#^Parameter \#1 \$phpExt of method Composer\\Package\\Package\:\:setPhpExt\(\) expects array\{extension\-name\?\: string, priority\?\: int, support\-zts\?\: bool, support\-nts\?\: bool, build\-path\?\: string\|null, download\-url\-method\?\: string, os\-families\?\: non\-empty\-list\, os\-families\-exclude\?\: non\-empty\-list\, \.\.\.\}\|null, array\{extension\-name\: null\} given\.$#' + message: '#^Parameter \#1 \$phpExt of method Composer\\Package\\Package\:\:setPhpExt\(\) expects array\{extension\-name\?\: string, priority\?\: int, support\-zts\?\: bool, support\-nts\?\: bool, build\-path\?\: string\|null, download\-url\-method\?\: list\\|string, os\-families\?\: non\-empty\-list\, os\-families\-exclude\?\: non\-empty\-list\, \.\.\.\}\|null, array\{extension\-name\: null\} given\.$#' identifier: argument.type count: 1 path: test/unit/ExtensionNameTest.php