From 278060040851612f65972f0e7b0ee02a75bb4f5d Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 23 Dec 2025 15:09:10 +0000 Subject: [PATCH 01/14] 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 680ef5293da48763db15d6b9f74775debe87740e Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 31 Dec 2025 14:21:10 +0000 Subject: [PATCH 02/14] 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 | 21 ++++- src/Downloading/DownloadUrlMethod.php | 40 ++++++--- .../Exception/CouldNotFindReleaseAsset.php | 46 +++++----- src/Installing/UnixInstall.php | 2 +- 7 files changed, 151 insertions(+), 70 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 13111a35..a005163f 100644 --- a/src/DependencyResolver/Package.php +++ b/src/DependencyResolver/Package.php @@ -16,8 +16,11 @@ use function array_key_exists; use function array_map; use function array_slice; +use function array_values; +use function count; use function explode; use function implode; +use function is_array; use function parse_url; use function str_contains; use function str_starts_with; @@ -40,7 +43,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, @@ -89,7 +93,15 @@ public static function fromComposerCompletePackage(CompletePackageInterface $com $package->priority = $phpExtOptions['priority'] ?? 80; if ($phpExtOptions !== null && array_key_exists('download-url-method', $phpExtOptions)) { - $package->downloadUrlMethod = DownloadUrlMethod::tryFrom($phpExtOptions['download-url-method']); + /** @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, + ); + } } return $package; @@ -219,8 +231,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 16e4a10c6b565e97ddee75f922ebea3ba107d452 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Fri, 2 Jan 2026 14:21:40 +0000 Subject: [PATCH 03/14] 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 03c0192b852dba70637f69b68475b2b6898bc6b7 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Fri, 2 Jan 2026 14:43:02 +0000 Subject: [PATCH 04/14] 436: updating tests for multiple download URL methods support --- phpstan-baseline.neon | 10 +- src/DependencyResolver/Package.php | 7 +- 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(+), 21 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 2cda1c75..02419ada 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 a005163f..154c01ec 100644 --- a/src/DependencyResolver/Package.php +++ b/src/DependencyResolver/Package.php @@ -16,7 +16,6 @@ use function array_key_exists; use function array_map; use function array_slice; -use function array_values; use function count; use function explode; use function implode; @@ -40,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 8754c0b830018532c53d365f6dbf6bdb14faad03 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 7 Jan 2026 15:00:09 +0000 Subject: [PATCH 05/14] 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 ea73debacf018eb60499fe44f0b5effbcc3ce5d3 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 7 Jan 2026 15:04:13 +0000 Subject: [PATCH 06/14] 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 23418474fa078a6f7e68f2dc7158d7be83ee4737 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 7 Jan 2026 16:40:34 +0000 Subject: [PATCH 07/14] 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 3addaddbcbbe081c97c067f095eed91926b2b321 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 7 Jan 2026 16:56:36 +0000 Subject: [PATCH 08/14] 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 ac5d242a7b3b5e7deed31fc6666044ae8cdc5155 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 8 Jan 2026 15:38:42 +0000 Subject: [PATCH 09/14] 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 1e4b29b4e64426926aef5b253204abb8a5b46808 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Fri, 9 Jan 2026 15:49:09 +0000 Subject: [PATCH 10/14] 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 ea8b3080..b69f33af 100644 --- a/test/unit/DependencyResolver/PackageTest.php +++ b/test/unit/DependencyResolver/PackageTest.php @@ -146,4 +146,19 @@ public function testFromComposerCompletePackageWithBuildPath(): void self::assertSame('vendor/foo:1.2.3', $package->prettyNameAndVersion()); self::assertSame('some/subdirectory/path/', $package->buildPath()); } + + 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 e8fc2aff02edb5e69e972f960b1648335b5bc638 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 13 Jan 2026 16:37:32 +0000 Subject: [PATCH 11/14] 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 | 21 ++++- .../Downloading/DownloadUrlMethodTest.php | 32 +++++-- test/unit/Platform/LibcFlavourTest.php | 26 +++++- .../Platform/TargetPhp/PhpBinaryPathTest.php | 17 +++- test/unit/Platform/TargetPlatformTest.php | 5 +- 18 files changed, 444 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 b69f33af..14576c3b 100644 --- a/test/unit/DependencyResolver/PackageTest.php +++ b/test/unit/DependencyResolver/PackageTest.php @@ -8,6 +8,7 @@ use Composer\Package\CompletePackageInterface; use InvalidArgumentException; use Php\Pie\DependencyResolver\Package; +use Php\Pie\Downloading\DownloadUrlMethod; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; use Php\Pie\Platform\OperatingSystemFamily; @@ -149,16 +150,30 @@ public function testFromComposerCompletePackageWithBuildPath(): 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 e2670e2f..ce2db33e 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 99db426d3508e4208c0e12926ad77174e8902ca6 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Fri, 16 Jan 2026 16:37:14 +0000 Subject: [PATCH 12/14] 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 6d9b0cdfc86fddeba87daff37f36d9123653b9d9 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Fri, 16 Jan 2026 16:52:43 +0000 Subject: [PATCH 13/14] 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 6dafb2ebb9b9586d779531b2dfd15300d7e33657 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 26 Jan 2026 11:10:48 +0000 Subject: [PATCH 14/14] 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"] } ]