From 4e8d3cd26e14fb083e35ac7898514b8c2ec221a9 Mon Sep 17 00:00:00 2001 From: gpb Date: Wed, 18 Feb 2026 23:57:11 +0800 Subject: [PATCH 1/6] Fix Windows extension install from downloads.php.net - Generate asset name variants for "x64" arch (not just "x86_64") and versions without "v" prefix - Add Architecture::windowsName() for conventional Windows arch labels - Fall back to downloads.php.net/~windows/pecl/releases/ when GitHub release has no matching Windows binary - Support simple DLL naming (e.g. "php_apcu.dll") used in downloads.php.net zips --- .../GithubPackageReleaseAssets.php | 70 +++++++++-- src/Platform/Architecture.php | 16 +++ src/Platform/WindowsExtensionAssetName.php | 76 +++++++---- .../GithubPackageReleaseAssetsTest.php | 119 +++++++++++++++++- test/unit/Platform/ArchitectureTest.php | 16 +++ .../WindowsExtensionAssetNameTest.php | 86 +++++++++++++ 6 files changed, 352 insertions(+), 31 deletions(-) diff --git a/src/Downloading/GithubPackageReleaseAssets.php b/src/Downloading/GithubPackageReleaseAssets.php index ad1bd13a..76c99627 100644 --- a/src/Downloading/GithubPackageReleaseAssets.php +++ b/src/Downloading/GithubPackageReleaseAssets.php @@ -12,6 +12,7 @@ use function array_map; use function in_array; +use function ltrim; use function strtolower; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ @@ -34,15 +35,28 @@ public function findMatchingReleaseAssetUrl( DownloadUrlMethod $downloadUrlMethod, array $possibleReleaseAssetNames, ): string { - $releaseAsset = $this->selectMatchingReleaseAsset( - $targetPlatform, - $package, - $this->getReleaseAssetsForPackage($package, $httpDownloader, $downloadUrlMethod), - $downloadUrlMethod, - $possibleReleaseAssetNames, - ); + try { + $releaseAsset = $this->selectMatchingReleaseAsset( + $targetPlatform, + $package, + $this->getReleaseAssetsForPackage($package, $httpDownloader, $downloadUrlMethod), + $downloadUrlMethod, + $possibleReleaseAssetNames, + ); + + return $releaseAsset['browser_download_url']; + } catch (Exception\CouldNotFindReleaseAsset $githubException) { + // GitHub release had no matching asset — try downloads.php.net as a fallback + // for Windows binaries, since many PECL extensions publish prebuilt DLLs there. + if ($downloadUrlMethod === DownloadUrlMethod::WindowsBinaryDownload) { + $fallbackUrl = $this->tryPhpNetWindowsDownload($package, $httpDownloader, $possibleReleaseAssetNames); + if ($fallbackUrl !== null) { + return $fallbackUrl; + } + } - return $releaseAsset['browser_download_url']; + throw $githubException; + } } /** @link https://github.com/squizlabs/PHP_CodeSniffer/issues/3734 */ @@ -114,4 +128,44 @@ static function (array $asset): array { $decodedResponse['assets'], ); } + + /** + * Fallback: attempt to find a prebuilt Windows extension archive on + * downloads.php.net, which hosts PECL binaries that may not be attached + * to GitHub releases. + * + * URL pattern: https://downloads.php.net/~windows/pecl/releases/{ext}/{version}/{asset} + * + * @param non-empty-list $possibleReleaseAssetNames + * + * @return non-empty-string|null + */ + private function tryPhpNetWindowsDownload( + Package $package, + HttpDownloader $httpDownloader, + array $possibleReleaseAssetNames, + ): string|null { + $extName = $package->extensionName()->name(); + $versionWithoutV = ltrim($package->version(), 'vV'); + + foreach ($possibleReleaseAssetNames as $assetName) { + $url = 'https://downloads.php.net/~windows/pecl/releases/' + . $extName . '/' . $versionWithoutV . '/' . $assetName; + + try { + $response = $httpDownloader->get($url, [ + 'http' => ['method' => 'HEAD'], + ]); + + if ($response->getStatusCode() === 200) { + return $url; + } + } catch (TransportException) { + // Asset not found at this URL, try next variant + continue; + } + } + + return null; + } } diff --git a/src/Platform/Architecture.php b/src/Platform/Architecture.php index 75229135..5e3acf0a 100644 --- a/src/Platform/Architecture.php +++ b/src/Platform/Architecture.php @@ -24,4 +24,20 @@ public static function parseArchitecture(string $architecture): self default => self::x86, }; } + + /** + * Returns the conventional Windows architecture label, which may differ + * from the PHP enum case name (e.g. "x64" instead of "x86_64"). + * Used when matching asset filenames from sources like downloads.php.net. + * + * @return non-empty-string + */ + public function windowsName(): string + { + return match ($this) { + self::x86_64 => 'x64', + self::arm64 => 'arm64', + self::x86 => 'x86', + }; + } } diff --git a/src/Platform/WindowsExtensionAssetName.php b/src/Platform/WindowsExtensionAssetName.php index 5e836df3..ab4dd882 100644 --- a/src/Platform/WindowsExtensionAssetName.php +++ b/src/Platform/WindowsExtensionAssetName.php @@ -9,8 +9,11 @@ use Php\Pie\Downloading\Exception\CouldNotFindReleaseAsset; use RuntimeException; +use function array_unique; +use function array_values; use function file_exists; use function implode; +use function ltrim; use function sprintf; use function strtolower; @@ -31,29 +34,48 @@ private static function assetNames(TargetPlatform $targetPlatform, Package $pack /** * During development, we swapped compiler/ts around. It is fairly trivial to support both, so we can check * both formats pretty easily, just to avoid confusion for package maintainers... + * + * Additionally, some distributions (notably downloads.php.net) use the shorter Windows architecture + * label "x64" instead of "x86_64", and version strings without the "v" prefix (e.g. "5.1.28" instead + * of "v5.1.28"). We generate variants covering all combinations to match either convention. */ - return [ - strtolower(sprintf( - 'php_%s-%s-%s-%s-%s-%s.%s', - $package->extensionName()->name(), - $package->version(), - $targetPlatform->phpBinaryPath->majorMinorVersion(), - $targetPlatform->threadSafety->asShort(), - strtolower($targetPlatform->windowsCompiler->name), - $targetPlatform->architecture->name, - $fileExtension, - )), - strtolower(sprintf( - 'php_%s-%s-%s-%s-%s-%s.%s', - $package->extensionName()->name(), - $package->version(), - $targetPlatform->phpBinaryPath->majorMinorVersion(), - strtolower($targetPlatform->windowsCompiler->name), - $targetPlatform->threadSafety->asShort(), - $targetPlatform->architecture->name, - $fileExtension, - )), - ]; + $version = $package->version(); + $versionNoV = ltrim($version, 'vV'); + $versions = array_unique([$version, $versionNoV]); + $architectures = array_unique([ + $targetPlatform->architecture->name, + $targetPlatform->architecture->windowsName(), + ]); + + $names = []; + foreach ($versions as $ver) { + foreach ($architectures as $arch) { + // Format: {ts}-{compiler} (e.g. ts-vs17) + $names[] = strtolower(sprintf( + 'php_%s-%s-%s-%s-%s-%s.%s', + $package->extensionName()->name(), + $ver, + $targetPlatform->phpBinaryPath->majorMinorVersion(), + $targetPlatform->threadSafety->asShort(), + strtolower($targetPlatform->windowsCompiler->name), + $arch, + $fileExtension, + )); + // Format: {compiler}-{ts} (e.g. vs17-ts) — legacy/swapped ordering + $names[] = strtolower(sprintf( + 'php_%s-%s-%s-%s-%s-%s.%s', + $package->extensionName()->name(), + $ver, + $targetPlatform->phpBinaryPath->majorMinorVersion(), + strtolower($targetPlatform->windowsCompiler->name), + $targetPlatform->threadSafety->asShort(), + $arch, + $fileExtension, + )); + } + } + + return array_values(array_unique($names)); } /** @return non-empty-list */ @@ -79,6 +101,16 @@ public static function determineDllName(TargetPlatform $targetPlatform, Download } } + // Zips from downloads.php.net use a simple naming convention (e.g. "php_apcu.dll") + // without version/platform suffixes, so check for that as a fallback. + $simpleDllName = 'php_' . $package->package->extensionName()->name() . '.dll'; + $fullSimpleDllName = $package->extractedSourcePath . '/' . $simpleDllName; + if (file_exists($fullSimpleDllName)) { + return $fullSimpleDllName; + } + + $possibleDllNames[] = $simpleDllName; + throw new RuntimeException('Unable to find DLL for package, checked: ' . implode(', ', $possibleDllNames)); } } diff --git a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php index 4002a366..225c0637 100644 --- a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php +++ b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php @@ -25,6 +25,7 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; +use function str_contains; use function uniqid; #[CoversClass(GithubPackageReleaseAssets::class)] @@ -179,7 +180,7 @@ public function testFindWindowsDownloadUrlForPackageThrowsExceptionWhenAssetNotF $httpDownloader = $this->createMock(HttpDownloader::class); $httpDownloader - ->expects(self::once()) + ->expects(self::atLeastOnce()) ->method('get') ->willThrowException($e); @@ -206,4 +207,120 @@ public function testFindWindowsDownloadUrlForPackageThrowsExceptionWhenAssetNotF ), ); } + + public function testFallsBackToPhpNetWhenGithubReleaseHasNoMatchingAsset(): void + { + $phpBinaryPath = $this->createMock(PhpBinaryPath::class); + $phpBinaryPath->expects(self::any()) + ->method('majorMinorVersion') + ->willReturn('8.5'); + + $targetPlatform = new TargetPlatform( + OperatingSystem::Windows, + OperatingSystemFamily::Windows, + $phpBinaryPath, + Architecture::x86_64, + ThreadSafetyMode::ThreadSafe, + 1, + WindowsCompiler::VS17, + ); + + // GitHub release exists but has no matching Windows asset + $githubResponse = $this->createMock(Response::class); + $githubResponse + ->method('decodeJson') + ->willReturn(['assets' => []]); + + // downloads.php.net HEAD response succeeds + $phpNetResponse = $this->createMock(Response::class); + $phpNetResponse + ->method('getStatusCode') + ->willReturn(200); + + $httpDownloader = $this->createMock(HttpDownloader::class); + $httpDownloader + ->method('get') + ->willReturnCallback(static function (string $url) use ($githubResponse, $phpNetResponse): Response { + if (str_contains($url, 'github')) { + return $githubResponse; + } + + // The fallback should hit downloads.php.net + self::assertStringStartsWith('https://downloads.php.net/~windows/pecl/releases/apcu/5.1.28/', $url); + + return $phpNetResponse; + }); + + $package = new Package( + $this->createMock(CompletePackageInterface::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('apcu'), + 'apcu/apcu', + 'v5.1.28', + 'https://test-uri/' . uniqid('downloadUrl', true), + ); + + $releaseAssets = new GithubPackageReleaseAssets('https://test-github-api-base-url.thephp.foundation'); + + $url = $releaseAssets->findMatchingReleaseAssetUrl( + $targetPlatform, + $package, + $httpDownloader, + DownloadUrlMethod::WindowsBinaryDownload, + WindowsExtensionAssetName::zipNames($targetPlatform, $package), + ); + + self::assertStringStartsWith('https://downloads.php.net/~windows/pecl/releases/apcu/5.1.28/', $url); + } + + public function testPhpNetFallbackIsNotAttemptedForNonWindowsDownloadMethods(): void + { + $phpBinaryPath = $this->createMock(PhpBinaryPath::class); + $phpBinaryPath->expects(self::any()) + ->method('majorMinorVersion') + ->willReturn('8.5'); + + $targetPlatform = new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + $phpBinaryPath, + Architecture::x86_64, + ThreadSafetyMode::ThreadSafe, + 1, + null, + ); + + // GitHub release exists but has no matching asset + $githubResponse = $this->createMock(Response::class); + $githubResponse + ->method('decodeJson') + ->willReturn(['assets' => []]); + + // Only one HTTP call should be made (to GitHub) — no fallback to downloads.php.net + $httpDownloader = $this->createMock(HttpDownloader::class); + $httpDownloader + ->expects(self::once()) + ->method('get') + ->willReturn($githubResponse); + + $package = new Package( + $this->createMock(CompletePackageInterface::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foo'), + 'asgrim/example-pie-extension', + 'v1.2.3', + 'https://test-uri/' . uniqid('downloadUrl', true), + ); + + $releaseAssets = new GithubPackageReleaseAssets('https://test-github-api-base-url.thephp.foundation'); + + $this->expectException(CouldNotFindReleaseAsset::class); + $releaseAssets->findMatchingReleaseAssetUrl( + $targetPlatform, + $package, + $httpDownloader, + DownloadUrlMethod::PrePackagedSourceDownload, + ['foo-v1.2.3.tgz'], + ); + } } diff --git a/test/unit/Platform/ArchitectureTest.php b/test/unit/Platform/ArchitectureTest.php index 1b45aa82..62f83a00 100644 --- a/test/unit/Platform/ArchitectureTest.php +++ b/test/unit/Platform/ArchitectureTest.php @@ -32,4 +32,20 @@ public function testParseArchitecture(string $architectureString, Architecture $ { self::assertSame($expectedArchitecture, Architecture::parseArchitecture($architectureString)); } + + /** @return array */ + public static function windowsNameProvider(): array + { + return [ + 'x86_64 => x64' => [Architecture::x86_64, 'x64'], + 'arm64 => arm64' => [Architecture::arm64, 'arm64'], + 'x86 => x86' => [Architecture::x86, 'x86'], + ]; + } + + #[DataProvider('windowsNameProvider')] + public function testWindowsName(Architecture $architecture, string $expectedWindowsName): void + { + self::assertSame($expectedWindowsName, $architecture->windowsName()); + } } diff --git a/test/unit/Platform/WindowsExtensionAssetNameTest.php b/test/unit/Platform/WindowsExtensionAssetNameTest.php index 446b8a3d..3b12b1b4 100644 --- a/test/unit/Platform/WindowsExtensionAssetNameTest.php +++ b/test/unit/Platform/WindowsExtensionAssetNameTest.php @@ -6,6 +6,7 @@ use Composer\Package\CompletePackageInterface; use Php\Pie\DependencyResolver\Package; +use Php\Pie\Downloading\DownloadedPackage; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; use Php\Pie\Platform\Architecture; @@ -18,6 +19,12 @@ use Php\Pie\Platform\WindowsExtensionAssetName; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; +use RuntimeException; + +use function file_put_contents; +use function mkdir; +use function sys_get_temp_dir; +use function uniqid; #[CoversClass(WindowsExtensionAssetName::class)] final class WindowsExtensionAssetNameTest extends TestCase @@ -58,6 +65,8 @@ public function testZipNames(): void [ 'php_foo-1.2.3-' . $this->phpVersion . '-ts-vc14-x86_64.zip', 'php_foo-1.2.3-' . $this->phpVersion . '-vc14-ts-x86_64.zip', + 'php_foo-1.2.3-' . $this->phpVersion . '-ts-vc14-x64.zip', + 'php_foo-1.2.3-' . $this->phpVersion . '-vc14-ts-x64.zip', ], WindowsExtensionAssetName::zipNames($this->platform, $this->package), ); @@ -69,8 +78,85 @@ public function testDllNames(): void [ 'php_foo-1.2.3-' . $this->phpVersion . '-ts-vc14-x86_64.dll', 'php_foo-1.2.3-' . $this->phpVersion . '-vc14-ts-x86_64.dll', + 'php_foo-1.2.3-' . $this->phpVersion . '-ts-vc14-x64.dll', + 'php_foo-1.2.3-' . $this->phpVersion . '-vc14-ts-x64.dll', ], WindowsExtensionAssetName::dllNames($this->platform, $this->package), ); } + + public function testVersionWithVPrefixGeneratesVariantsWithAndWithoutPrefix(): void + { + $packageWithV = new Package( + $this->createMock(CompletePackageInterface::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foo'), + 'phpf/foo', + 'v1.2.3', + null, + ); + + $names = WindowsExtensionAssetName::zipNames($this->platform, $packageWithV); + + // Should contain both "v1.2.3" and "1.2.3" variants + self::assertContains('php_foo-v1.2.3-' . $this->phpVersion . '-ts-vc14-x86_64.zip', $names); + self::assertContains('php_foo-1.2.3-' . $this->phpVersion . '-ts-vc14-x86_64.zip', $names); + self::assertContains('php_foo-v1.2.3-' . $this->phpVersion . '-ts-vc14-x64.zip', $names); + self::assertContains('php_foo-1.2.3-' . $this->phpVersion . '-ts-vc14-x64.zip', $names); + } + + public function testX86ArchitectureDoesNotDuplicate(): void + { + $x86Platform = new TargetPlatform( + OperatingSystem::Windows, + OperatingSystemFamily::Windows, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86, + ThreadSafetyMode::ThreadSafe, + 1, + WindowsCompiler::VC14, + ); + + $names = WindowsExtensionAssetName::zipNames($x86Platform, $this->package); + + // x86 has the same enum name and windowsName(), so no arch duplicates + self::assertSame( + [ + 'php_foo-1.2.3-' . $this->phpVersion . '-ts-vc14-x86.zip', + 'php_foo-1.2.3-' . $this->phpVersion . '-vc14-ts-x86.zip', + ], + $names, + ); + } + + public function testDetermineDllNameFallsBackToSimpleName(): void + { + // Create a temp directory with only a simple-named DLL (as downloads.php.net provides) + $tempDir = sys_get_temp_dir() . '/' . uniqid('pie_test_', true); + mkdir($tempDir, 0777, true); + file_put_contents($tempDir . '/php_foo.dll', 'fake dll content'); + + $downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( + $this->package, + $tempDir, + ); + + $result = WindowsExtensionAssetName::determineDllName($this->platform, $downloadedPackage); + self::assertStringEndsWith('php_foo.dll', $result); + } + + public function testDetermineDllNameThrowsWhenNoDllFound(): void + { + $tempDir = sys_get_temp_dir() . '/' . uniqid('pie_test_empty_', true); + mkdir($tempDir, 0777, true); + + $downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( + $this->package, + $tempDir, + ); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('php_foo.dll'); + WindowsExtensionAssetName::determineDllName($this->platform, $downloadedPackage); + } } From b608c0db8b1ee375bb7e17ee36f218107e6bc592 Mon Sep 17 00:00:00 2001 From: gpb Date: Thu, 19 Feb 2026 03:18:57 +0800 Subject: [PATCH 2/6] fix some differences in $ARCH names across platforms --- src/Platform/Architecture.php | 17 +++++----- src/Platform/PrePackagedBinaryAssetName.php | 31 ++++++++++--------- src/Platform/WindowsExtensionAssetName.php | 9 ++---- .../Downloading/DownloadUrlMethodTest.php | 8 +++++ test/unit/Platform/ArchitectureTest.php | 17 +++++----- .../PrePackagedBinaryAssetNameTest.php | 10 ++++++ .../WindowsExtensionAssetNameTest.php | 2 +- 7 files changed, 57 insertions(+), 37 deletions(-) diff --git a/src/Platform/Architecture.php b/src/Platform/Architecture.php index 5e3acf0a..aefde4c9 100644 --- a/src/Platform/Architecture.php +++ b/src/Platform/Architecture.php @@ -26,18 +26,19 @@ public static function parseArchitecture(string $architecture): self } /** - * Returns the conventional Windows architecture label, which may differ - * from the PHP enum case name (e.g. "x64" instead of "x86_64"). - * Used when matching asset filenames from sources like downloads.php.net. + * Returns all known name variants for this architecture, with the + * canonical (enum case) name first. Used when matching asset filenames + * that may use platform-specific conventions (e.g. "x64" on Windows, + * "aarch64" on Linux). * - * @return non-empty-string + * @return non-empty-list */ - public function windowsName(): string + public function allNames(): array { return match ($this) { - self::x86_64 => 'x64', - self::arm64 => 'arm64', - self::x86 => 'x86', + self::x86_64 => ['x86_64', 'x64'], + self::arm64 => ['arm64', 'aarch64'], + self::x86 => ['x86'], }; } } diff --git a/src/Platform/PrePackagedBinaryAssetName.php b/src/Platform/PrePackagedBinaryAssetName.php index eda3f947..6181344f 100644 --- a/src/Platform/PrePackagedBinaryAssetName.php +++ b/src/Platform/PrePackagedBinaryAssetName.php @@ -21,51 +21,54 @@ private function __construct() /** @return non-empty-list */ public static function packageNames(TargetPlatform $targetPlatform, Package $package): array { - return array_values(array_unique([ - strtolower(sprintf( + $names = []; + foreach ($targetPlatform->architecture->allNames() as $arch) { + $names[] = strtolower(sprintf( 'php_%s-%s_php%s-%s-%s-%s%s%s.zip', $package->extensionName()->name(), $package->version(), $targetPlatform->phpBinaryPath->majorMinorVersion(), - $targetPlatform->architecture->name, + $arch, $targetPlatform->operatingSystemFamily->value, $targetPlatform->libcFlavour()->value, $targetPlatform->phpBinaryPath->debugMode() === DebugBuild::Debug ? '-debug' : '', $targetPlatform->threadSafety === ThreadSafetyMode::ThreadSafe ? '-zts' : '', - )), - strtolower(sprintf( + )); + $names[] = strtolower(sprintf( 'php_%s-%s_php%s-%s-%s-%s%s%s.tgz', $package->extensionName()->name(), $package->version(), $targetPlatform->phpBinaryPath->majorMinorVersion(), - $targetPlatform->architecture->name, + $arch, $targetPlatform->operatingSystemFamily->value, $targetPlatform->libcFlavour()->value, $targetPlatform->phpBinaryPath->debugMode() === DebugBuild::Debug ? '-debug' : '', $targetPlatform->threadSafety === ThreadSafetyMode::ThreadSafe ? '-zts' : '', - )), - strtolower(sprintf( + )); + $names[] = strtolower(sprintf( 'php_%s-%s_php%s-%s-%s-%s%s%s.zip', $package->extensionName()->name(), $package->version(), $targetPlatform->phpBinaryPath->majorMinorVersion(), - $targetPlatform->architecture->name, + $arch, $targetPlatform->operatingSystemFamily->value, $targetPlatform->libcFlavour()->value, $targetPlatform->phpBinaryPath->debugMode() === DebugBuild::Debug ? '-debug' : '', $targetPlatform->threadSafety === ThreadSafetyMode::ThreadSafe ? '-zts' : '-nts', - )), - strtolower(sprintf( + )); + $names[] = strtolower(sprintf( 'php_%s-%s_php%s-%s-%s-%s%s%s.tgz', $package->extensionName()->name(), $package->version(), $targetPlatform->phpBinaryPath->majorMinorVersion(), - $targetPlatform->architecture->name, + $arch, $targetPlatform->operatingSystemFamily->value, $targetPlatform->libcFlavour()->value, $targetPlatform->phpBinaryPath->debugMode() === DebugBuild::Debug ? '-debug' : '', $targetPlatform->threadSafety === ThreadSafetyMode::ThreadSafe ? '-zts' : '-nts', - )), - ])); + )); + } + + return array_values(array_unique($names)); } } diff --git a/src/Platform/WindowsExtensionAssetName.php b/src/Platform/WindowsExtensionAssetName.php index ab4dd882..e80b5bc1 100644 --- a/src/Platform/WindowsExtensionAssetName.php +++ b/src/Platform/WindowsExtensionAssetName.php @@ -35,17 +35,14 @@ private static function assetNames(TargetPlatform $targetPlatform, Package $pack * During development, we swapped compiler/ts around. It is fairly trivial to support both, so we can check * both formats pretty easily, just to avoid confusion for package maintainers... * - * Additionally, some distributions (notably downloads.php.net) use the shorter Windows architecture - * label "x64" instead of "x86_64", and version strings without the "v" prefix (e.g. "5.1.28" instead + * Additionally, some distributions (notably downloads.php.net) use alternative architecture labels + * (e.g. "x64" instead of "x86_64"), and version strings without the "v" prefix (e.g. "5.1.28" instead * of "v5.1.28"). We generate variants covering all combinations to match either convention. */ $version = $package->version(); $versionNoV = ltrim($version, 'vV'); $versions = array_unique([$version, $versionNoV]); - $architectures = array_unique([ - $targetPlatform->architecture->name, - $targetPlatform->architecture->windowsName(), - ]); + $architectures = $targetPlatform->architecture->allNames(); $names = []; foreach ($versions as $ver) { diff --git a/test/unit/Downloading/DownloadUrlMethodTest.php b/test/unit/Downloading/DownloadUrlMethodTest.php index b1113b9d..a38572e9 100644 --- a/test/unit/Downloading/DownloadUrlMethodTest.php +++ b/test/unit/Downloading/DownloadUrlMethodTest.php @@ -65,6 +65,8 @@ public function testWindowsPackages(): void [ 'php_foo-1.2.3-8.1-nts-vc15-x86_64.zip', 'php_foo-1.2.3-8.1-vc15-nts-x86_64.zip', + 'php_foo-1.2.3-8.1-nts-vc15-x64.zip', + 'php_foo-1.2.3-8.1-vc15-nts-x64.zip', ], $downloadUrlMethod->possibleAssetNames($package, $targetPlatform), ); @@ -146,6 +148,8 @@ public function testPrePackagedBinaryDownloads(): void [ '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', + 'php_bar-1.2.3_php8.3-x64-linux-glibc-debug-zts.zip', + 'php_bar-1.2.3_php8.3-x64-linux-glibc-debug-zts.tgz', ], $downloadUrlMethod->possibleAssetNames($package, $targetPlatform), ); @@ -222,6 +226,10 @@ public function testMultipleDownloadUrlMethods(): void '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', + 'php_bar-1.2.3_php8.3-x64-linux-glibc-debug.zip', + 'php_bar-1.2.3_php8.3-x64-linux-glibc-debug.tgz', + 'php_bar-1.2.3_php8.3-x64-linux-glibc-debug-nts.zip', + 'php_bar-1.2.3_php8.3-x64-linux-glibc-debug-nts.tgz', ], $firstMethod->possibleAssetNames($package, $targetPlatform), ); diff --git a/test/unit/Platform/ArchitectureTest.php b/test/unit/Platform/ArchitectureTest.php index 62f83a00..469eba50 100644 --- a/test/unit/Platform/ArchitectureTest.php +++ b/test/unit/Platform/ArchitectureTest.php @@ -33,19 +33,20 @@ public function testParseArchitecture(string $architectureString, Architecture $ self::assertSame($expectedArchitecture, Architecture::parseArchitecture($architectureString)); } - /** @return array */ - public static function windowsNameProvider(): array + /** @return array}> */ + public static function allNamesProvider(): array { return [ - 'x86_64 => x64' => [Architecture::x86_64, 'x64'], - 'arm64 => arm64' => [Architecture::arm64, 'arm64'], - 'x86 => x86' => [Architecture::x86, 'x86'], + 'x86_64' => [Architecture::x86_64, ['x86_64', 'x64']], + 'arm64' => [Architecture::arm64, ['arm64', 'aarch64']], + 'x86' => [Architecture::x86, ['x86']], ]; } - #[DataProvider('windowsNameProvider')] - public function testWindowsName(Architecture $architecture, string $expectedWindowsName): void + /** @param non-empty-list $expectedNames */ + #[DataProvider('allNamesProvider')] + public function testAllNames(Architecture $architecture, array $expectedNames): void { - self::assertSame($expectedWindowsName, $architecture->windowsName()); + self::assertSame($expectedNames, $architecture->allNames()); } } diff --git a/test/unit/Platform/PrePackagedBinaryAssetNameTest.php b/test/unit/Platform/PrePackagedBinaryAssetNameTest.php index 022312c6..4822380f 100644 --- a/test/unit/Platform/PrePackagedBinaryAssetNameTest.php +++ b/test/unit/Platform/PrePackagedBinaryAssetNameTest.php @@ -45,6 +45,10 @@ public function testPackageNamesNts(): void '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', + 'php_foobar-1.2.3_php8.2-x64-linux-' . $libc->value . '.zip', + 'php_foobar-1.2.3_php8.2-x64-linux-' . $libc->value . '.tgz', + 'php_foobar-1.2.3_php8.2-x64-linux-' . $libc->value . '-nts.zip', + 'php_foobar-1.2.3_php8.2-x64-linux-' . $libc->value . '-nts.tgz', ], PrePackagedBinaryAssetName::packageNames( $targetPlatform, @@ -81,6 +85,8 @@ public function testPackageNamesZts(): void [ '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', + 'php_foobar-1.2.3_php8.3-x64-linux-' . $libc->value . '-zts.zip', + 'php_foobar-1.2.3_php8.3-x64-linux-' . $libc->value . '-zts.tgz', ], PrePackagedBinaryAssetName::packageNames( $targetPlatform, @@ -119,6 +125,10 @@ public function testPackageNamesDebug(): void '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', + 'php_foobar-1.2.3_php8.4-aarch64-darwin-' . $libc->value . '-debug.zip', + 'php_foobar-1.2.3_php8.4-aarch64-darwin-' . $libc->value . '-debug.tgz', + 'php_foobar-1.2.3_php8.4-aarch64-darwin-' . $libc->value . '-debug-nts.zip', + 'php_foobar-1.2.3_php8.4-aarch64-darwin-' . $libc->value . '-debug-nts.tgz', ], PrePackagedBinaryAssetName::packageNames( $targetPlatform, diff --git a/test/unit/Platform/WindowsExtensionAssetNameTest.php b/test/unit/Platform/WindowsExtensionAssetNameTest.php index 3b12b1b4..92da3772 100644 --- a/test/unit/Platform/WindowsExtensionAssetNameTest.php +++ b/test/unit/Platform/WindowsExtensionAssetNameTest.php @@ -119,7 +119,7 @@ public function testX86ArchitectureDoesNotDuplicate(): void $names = WindowsExtensionAssetName::zipNames($x86Platform, $this->package); - // x86 has the same enum name and windowsName(), so no arch duplicates + // x86 has only one name in allNames(), so no arch duplicates self::assertSame( [ 'php_foo-1.2.3-' . $this->phpVersion . '-ts-vc14-x86.zip', From a629289aa9890fbe8135bf093406a3290c28d921 Mon Sep 17 00:00:00 2001 From: gpb Date: Fri, 20 Feb 2026 01:34:54 +0800 Subject: [PATCH 3/6] add debug flag --- .github/workflows/build-assets.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-assets.yml b/.github/workflows/build-assets.yml index 4a0f340f..5cf53935 100644 --- a/.github/workflows/build-assets.yml +++ b/.github/workflows/build-assets.yml @@ -129,7 +129,7 @@ jobs: name: pie-${{ github.sha }}.phar - name: Build for ${{ runner.os }} ${{ runner.arch }} on ${{ matrix.operating-system }} - run: ${{ env.SPC_BINARY }} craft resources/spc/craft.yml + run: ${{ env.SPC_BINARY }} craft resources/spc/craft.yml --debug env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Bundle pie.phar into executable PIE binary From 3ad54cd72432ac625fef262e2e78db3f0db955be Mon Sep 17 00:00:00 2001 From: gpb Date: Fri, 20 Feb 2026 01:58:22 +0800 Subject: [PATCH 4/6] fix zlib filename (hacky test) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zlib 1.3.2 changed its output names. The library files are now named z.lib, z.dll, and zs.lib instead of the old zlib.lib, zlib.dll, and zlibstatic.lib that SPC expects. SPC's zlib.php builder tries to: 1. Copy zlibstatic.lib — doesn't exist (it's zs.lib now) 2. Delete zlib.dll — doesn't exist (it's z.dll now) 3. Delete zlib.lib — doesn't exist (it's z.lib now) --- .github/workflows/build-assets.yml | 32 ++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/.github/workflows/build-assets.yml b/.github/workflows/build-assets.yml index 5cf53935..7c6506bd 100644 --- a/.github/workflows/build-assets.yml +++ b/.github/workflows/build-assets.yml @@ -129,9 +129,41 @@ jobs: name: pie-${{ github.sha }}.phar - name: Build for ${{ runner.os }} ${{ runner.arch }} on ${{ matrix.operating-system }} + if: runner.os != 'Windows' + run: ${{ env.SPC_BINARY }} craft resources/spc/craft.yml --debug + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Build for Windows (initial attempt, may fail due to zlib) + if: runner.os == 'Windows' + continue-on-error: true run: ${{ env.SPC_BINARY }} craft resources/spc/craft.yml --debug env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: "Workaround: fix zlib 1.3.2 library naming on Windows" + if: runner.os == 'Windows' + shell: pwsh + run: | + # zlib 1.3.2 changed cmake output file names: + # zlibstatic.lib -> zs.lib, zlib.lib -> z.lib, zlib.dll -> z.dll + # SPC expects the old names, so the static lib never gets placed correctly. + if (Test-Path "buildroot\lib\zs.lib") { + Write-Host "Renaming zlib artifacts to expected names..." + Copy-Item "buildroot\lib\zs.lib" "buildroot\lib\zlibstatic.lib" -Force + Remove-Item "buildroot\bin\z.dll" -Force -ErrorAction SilentlyContinue + Remove-Item "buildroot\lib\z.lib" -Force -ErrorAction SilentlyContinue + } else { + Write-Host "zs.lib not found, skipping workaround" + } + + - name: "Retry nmake micro after zlib fix (Windows)" + if: runner.os == 'Windows' + shell: pwsh + run: | + Push-Location source\php-src + & "$env:GITHUB_WORKSPACE\php-sdk-binary-tools\phpsdk-vs17-x64.bat" -t nmake_micro_wrapper.bat --task-args micro + Pop-Location - name: Bundle pie.phar into executable PIE binary run: ${{ env.SPC_BINARY }} micro:combine pie.phar --output=${{ env.PIE_BINARY_OUTPUT }} From 88d1f0ce5fe6a62425774d7aab7b9bddea359f0e Mon Sep 17 00:00:00 2001 From: gpb Date: Fri, 20 Feb 2026 02:16:04 +0800 Subject: [PATCH 5/6] more renaming zlib --- .github/workflows/build-assets.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-assets.yml b/.github/workflows/build-assets.yml index 7c6506bd..60f9e2d9 100644 --- a/.github/workflows/build-assets.yml +++ b/.github/workflows/build-assets.yml @@ -147,21 +147,27 @@ jobs: run: | # zlib 1.3.2 changed cmake output file names: # zlibstatic.lib -> zs.lib, zlib.lib -> z.lib, zlib.dll -> z.dll - # SPC expects the old names, so the static lib never gets placed correctly. + # PHP configure expects zlib_a.lib or zlib.lib, and SPC expects zlibstatic.lib. + # None of these exist, so zlib never gets linked. if (Test-Path "buildroot\lib\zs.lib") { Write-Host "Renaming zlib artifacts to expected names..." Copy-Item "buildroot\lib\zs.lib" "buildroot\lib\zlibstatic.lib" -Force + Copy-Item "buildroot\lib\zs.lib" "buildroot\lib\zlib_a.lib" -Force Remove-Item "buildroot\bin\z.dll" -Force -ErrorAction SilentlyContinue Remove-Item "buildroot\lib\z.lib" -Force -ErrorAction SilentlyContinue } else { Write-Host "zs.lib not found, skipping workaround" } - - name: "Retry nmake micro after zlib fix (Windows)" + - name: "Retry configure + nmake after zlib fix (Windows)" if: runner.os == 'Windows' shell: pwsh run: | Push-Location source\php-src + # Re-run configure so it picks up zlib_a.lib this time + & "$env:GITHUB_WORKSPACE\php-sdk-binary-tools\phpsdk-vs17-x64.bat" -t configure.bat --task-args "--disable-all --with-php-build=$env:GITHUB_WORKSPACE\buildroot --with-extra-includes=$env:GITHUB_WORKSPACE\buildroot\include --with-extra-libs=$env:GITHUB_WORKSPACE\buildroot\lib --disable-cli --enable-micro --disable-embed --disable-cgi --enable-opcache-jit=yes --enable-zlib --with-openssl --with-openssl-argon2 --with-curl --enable-filter --with-iconv --enable-phar --enable-zts=no" + # Clean and rebuild + & "$env:GITHUB_WORKSPACE\php-sdk-binary-tools\phpsdk-vs17-x64.bat" -t nmake_clean_wrapper.bat --task-args "clean" & "$env:GITHUB_WORKSPACE\php-sdk-binary-tools\phpsdk-vs17-x64.bat" -t nmake_micro_wrapper.bat --task-args micro Pop-Location - name: Bundle pie.phar into executable PIE binary From 2461ccd07db8715469b74b9e72250b952246bc1c Mon Sep 17 00:00:00 2001 From: gpb Date: Fri, 20 Feb 2026 02:45:27 +0800 Subject: [PATCH 6/6] copy micro.sfx to buildroot after manual nmake retry Co-Authored-By: Claude Opus 4.6 --- .github/workflows/build-assets.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build-assets.yml b/.github/workflows/build-assets.yml index 60f9e2d9..88211fa4 100644 --- a/.github/workflows/build-assets.yml +++ b/.github/workflows/build-assets.yml @@ -169,6 +169,8 @@ jobs: # Clean and rebuild & "$env:GITHUB_WORKSPACE\php-sdk-binary-tools\phpsdk-vs17-x64.bat" -t nmake_clean_wrapper.bat --task-args "clean" & "$env:GITHUB_WORKSPACE\php-sdk-binary-tools\phpsdk-vs17-x64.bat" -t nmake_micro_wrapper.bat --task-args micro + # Copy micro.sfx to where SPC expects it + Copy-Item "x64\Release\micro.sfx" "$env:GITHUB_WORKSPACE\buildroot\bin\micro.sfx" -Force Pop-Location - name: Bundle pie.phar into executable PIE binary run: ${{ env.SPC_BINARY }} micro:combine pie.phar --output=${{ env.PIE_BINARY_OUTPUT }}