[web-downloads] support-build-number: Support numbered PHP builds

From: Date: Mon, 26 Oct 2026 11:36:13 +0000
Subject: [web-downloads] support-build-number: Support numbered PHP builds
Groups: php.webmaster 
Request: Send a blank email to php-webmaster+get-34147@lists.php.net to get a copy of this message
Author: Shivam Mathur (shivammathur) Date: 2026-10-26T04:14:39+05:30 Commit: https://github.com/php/web-downloads/commit/d0d268577bd3c28cb4ab6b4fbceaa9e1555f7ff7 Raw diff: https://github.com/php/web-downloads/commit/d0d268577bd3c28cb4ab6b4fbceaa9e1555f7ff7.diff Support numbered PHP builds Changed paths: M src/Actions/GetListing.php M src/Console/Command/PhpCommand.php M tests/Actions/GetListingTest.php M tests/Console/Command/PhpCommandTest.php Diff: diff --git a/src/Actions/GetListing.php b/src/Actions/GetListing.php index 5194809..5e6648b 100644 --- a/src/Actions/GetListing.php +++ b/src/Actions/GetListing.php @@ -22,8 +22,12 @@ public function handle(string $directory): array $parts = $this->parseFileName(basename($file)); $key = ($parts['nts'] ? 'nts-' : 'ts-') . $parts['vc'] . '-' . $parts['arch']; $version_short = $parts['version_short']; - if (!isset($releases[$version_short]['version'])) { - $releases[$version_short]['version'] = $parts['version']; + $currentVersion = $releases[$version_short]['version'] ?? null; + if ($currentVersion !== null && version_compare($parts['version'], $currentVersion) < 0) { + continue; + } + if ($currentVersion === null || version_compare($parts['version'], $currentVersion) > 0) { + $releases[$version_short] = ['version' => $parts['version']]; } $releases[$version_short][$key]['mtime'] = $mtime; $releases[$version_short][$key]['zip'] = [ @@ -104,7 +108,11 @@ public function parseFileName($fileName): array $fileName = str_replace(['-Win32', '.zip'], ['', ''], $fileName); $parts = explode('-', $fileName); - if (is_numeric($parts[2]) || $parts[2] == 'dev') { + if ($parts[2] === 'dev' && isset($parts[3]) && ctype_digit($parts[3])) { + $parts[2] .= '-' . $parts[3]; + array_splice($parts, 3, 1); + } + if (is_numeric($parts[2]) || str_starts_with($parts[2], 'dev')) { $version = $parts[1] . '-' . $parts[2]; $nts = $parts[3] == 'nts' ? 'nts' : false; if ($nts) { diff --git a/src/Console/Command/PhpCommand.php b/src/Console/Command/PhpCommand.php index da2da67..c6543c2 100644 --- a/src/Console/Command/PhpCommand.php +++ b/src/Console/Command/PhpCommand.php @@ -86,8 +86,10 @@ public function handle(): int $this->updateReleasesJson->handle($releases, $destinationDirectory); if ($destinationDirectory === $this->baseDirectory . '/releases') { $this->updateLatestBuilds($releases, $destinationDirectory); - $parts = explode('.', $version); - $this->promoteSeriesFiles($parts[0] . '.' . $parts[1]); + $series = implode('.', array_slice(explode('.', $version), 0, 2)); + if (($releases[$series]['version'] ?? null) === $version) { + $this->promoteSeriesFiles($series); + } } Helpers::rmdirr($tempDirectory); @@ -113,7 +115,7 @@ public function handle(): int private function getDestinationDirectory(string $tempDirectory): string { $version = $this->getBuildVersion($tempDirectory); - return $this->baseDirectory . (preg_match('/^\d+\.\d+\.\d+$/', $version) ? '/releases' : '/qa'); + return $this->baseDirectory . (preg_match('/^\d+\.\d+\.\d+(?:-\d+)?$/', $version) ? '/releases' : '/qa'); } /** @@ -143,7 +145,7 @@ private function moveBuild(string $tempDirectory, string $destinationDirectory): { $files = glob($tempDirectory . '/*'); if ($files) { - $version = $this->getFileVersion($files[0]); + $version = $this->getBuildVersion($tempDirectory); foreach ($files as $file) { $fileName = basename($file); $destination = $destinationDirectory . '/' . $fileName; @@ -160,6 +162,12 @@ private function copyBuildsToArchive(string $directory, string $version): void { $version_short = substr($version, 0, 3); $files = glob($directory . '/php*' . $version_short . '*.zip'); + foreach ($files as $file) { + $fileVersion = $this->getFileVersion($file); + if (version_compare($fileVersion, $version) > 0) { + $version = $fileVersion; + } + } if(!is_dir($directory . '/archives')) { mkdir($directory . '/archives', 0755, true); } @@ -190,8 +198,8 @@ private function getFileVersion(string $file): string $pattern = '/php-/'; } $file = preg_replace($pattern, '', $file); - $parts = explode('-', (string) $file); - return str_replace('.zip', '', $parts[0]); + preg_match('/^(\d+\.\d+\.\d+(?:(?:alpha|beta|RC)\d+|-dev)?(?:-\d+)?)/i', (string) $file, $matches); + return $matches[1] ?? ''; } /** diff --git a/tests/Actions/GetListingTest.php b/tests/Actions/GetListingTest.php index 29ed332..8a63056 100644 --- a/tests/Actions/GetListingTest.php +++ b/tests/Actions/GetListingTest.php @@ -80,6 +80,26 @@ public static function parseFileNameProvider(): array 'ts' => false, ], ], + 'numbered nts' => [ + 'php-8.5.12-1-nts-Win32-vs17-x64.zip', + ['version' => '8.5.12-1', 'version_short' => '8.5', 'nts' => 'nts', 'vc' => 'vs17', 'arch' => 'x64', 'ts' => false], + ], + 'numbered ts' => [ + 'php-8.5.12-10-Win32-vs17-x86.zip', + ['version' => '8.5.12-10', 'version_short' => '8.5', 'nts' => false, 'vc' => 'vs17', 'arch' => 'x86', 'ts' => false], + ], + 'numbered dev' => [ + 'php-8.6.0-dev-2-nts-Win32-vs18-arm64.zip', + ['version' => '8.6.0-dev-2', 'version_short' => '8.6', 'nts' => 'nts', 'vc' => 'vs18', 'arch' => 'arm64', 'ts' => false], + ], + 'numbered RC' => [ + 'php-8.6.0RC1-2-Win32-vs18-x64.zip', + ['version' => '8.6.0RC1-2', 'version_short' => '8.6', 'nts' => false, 'vc' => 'vs18', 'arch' => 'x64', 'ts' => false], + ], + 'latest alias' => [ + 'php-8.5-nts-Win32-vs17-x64-latest.zip', + ['version' => '8.5', 'version_short' => '8.5', 'nts' => 'nts', 'vc' => 'vs17', 'arch' => 'x64', 'ts' => false], + ], ]; } @@ -207,4 +227,29 @@ public function testHandleWithMatchingFiles(): void $this->assertEquals($expectedSha256, $installerInfo['sha256']); $this->assertFileExists($this->tempDir . '/sha256sum.txt'); } + + public function testListingSelectsOneRevisionAcrossEveryPackage(): void + { + foreach (['8.5.12', '8.5.12-1', '8.5.12-2', '8.5.12-10'] as $version) { + foreach ([ + "php-$version-nts-Win32-vs17-x64.zip", + "php-debug-pack-$version-nts-Win32-vs17-x64.zip", + "php-devel-pack-$version-nts-Win32-vs17-x64.zip", + "php-$version-src.zip", + "php-test-pack-$version.zip", + ] as $file) { + file_put_contents($this->tempDir . '/' . $file, $file); + } + } + // An older architecture must not leak into metadata for the selected revision. + file_put_contents($this->tempDir . '/php-8.5.12-nts-Win32-vs17-x86.zip', 'older architecture'); + $release = $this->getListing->handle($this->tempDir)['8.5']; + $this->assertSame('8.5.12-10', $release['version']); + $this->assertArrayNotHasKey('nts-vs17-x86', $release); + foreach (['zip', 'debug_pack', 'devel_pack'] as $type) { + $this->assertStringContainsString('8.5.12-10-', $release['nts-vs17-x64'][$type]['path']); + } + $this->assertSame('php-8.5.12-10-src.zip', $release['source']['path']); + $this->assertSame('php-test-pack-8.5.12-10.zip', $release['test_pack']['path']); + } } diff --git a/tests/Console/Command/PhpCommandTest.php b/tests/Console/Command/PhpCommandTest.php index d0b490b..218da51 100644 --- a/tests/Console/Command/PhpCommandTest.php +++ b/tests/Console/Command/PhpCommandTest.php @@ -83,6 +83,12 @@ public static function buildsProvider(): array 'php-test-pack-8.4.0-dev.zip', ]] ]; + foreach ($builds as [$files]) { + $builds[] = [array_map( + static fn (string $file): string => str_replace(['8.4.1', '8.4.0-dev'], ['8.4.1-1', '8.4.0-dev-2'], $file), + $files + )]; + } foreach ($builds as [$files]) { $builds[] = [array_map( static fn (string $file): string => str_replace('-x64', '-arm64', $file), @@ -103,7 +109,7 @@ private function stageBuilds(array $phpZips, $zipPath): void } else { $innerZip = new ZipArchive(); if ($innerZip->open($zipFilePath, ZipArchive::CREATE) === TRUE) { - $innerZip->addFromString("test_file.php", "<?php echo 'Hello, world!'; ?>"); + $innerZip->addFromString('build.txt', $zipFileName); $innerZip->close(); } } @@ -130,6 +136,9 @@ public function testCommandHandlesSuccessfulExecution(array $phpZips): void $expectedDestination = $this->baseDirectory . (str_contains($phpZips[0], '-dev-') ? '/qa' : '/releases'); $this->assertDirectoryExists($expectedDestination, "Destination directory should exist."); + foreach ($phpZips as $file) { + $this->assertFileExists($expectedDestination . '/' . $file); + } foreach (array_filter($phpZips, static fn (string $file): bool => str_ends_with($file, '.json')) as $sidecar) { $this->assertFileExists($expectedDestination . '/' . $sidecar); $this->assertFileExists($expectedDestination . '/archives/' . $sidecar); @@ -253,4 +262,56 @@ public function testQaReleaseDoesNotPromoteSeriesFiles(): void $this->assertFileDoesNotExist($this->baseDirectory . '/php-sdk/deps/series/packages-8.4-vs17-x64-stable.txt'); $this->assertFileDoesNotExist($this->baseDirectory . '/php-sdk/deps/series/packages-8.4-vs17-arm64-stable.txt'); } + + public function testRebuildsArchiveOlderNumbersAndNeverDowngradeLatest(): void + { + $command = new PhpCommand(new GetListing(), new UpdateReleasesJson()); + $command->options = ['base-directory' => $this->baseDirectory, 'builds-directory' => $this->buildsDirectory]; + $files = self::buildsProvider()[0][0]; + $releaseDirectory = $this->baseDirectory . '/releases'; + $latestVersion = '8.4.1'; + foreach (['8.4.1', '8.4.1-1', '8.4.1-2', '8.4.1-10', '8.4.1-3', '8.4.2', '8.4.1-11'] as $version) { + $builds = array_map(static fn (string $file): string => str_replace('8.4.1', $version, $file), $files); + $this->stageBuilds($builds, $this->buildsDirectory . '/php/rebuild.zip'); + $this->assertSame(0, $command->handle()); + if (version_compare($version, $latestVersion) > 0) { + $latestVersion = $version; + } + $releases = json_decode(file_get_contents($releaseDirectory . '/releases.json'), true, flags: JSON_THROW_ON_ERROR); + $this->assertSame($latestVersion, $releases['8.4']['version']); + foreach ($builds as $file) { + $this->assertFileExists($releaseDirectory . '/archives/' . $file); + if ($version !== $latestVersion) { + $this->assertFileDoesNotExist($releaseDirectory . '/' . $file); + } + } + foreach ($files as $original) { + $current = str_replace('8.4.1', $latestVersion, $original); + $this->assertFileExists($releaseDirectory . '/' . $current); + if (str_ends_with($original, '.zip')) { + $latest = str_replace(['8.4.1', '.zip'], ['8.4', '-latest.zip'], $original); + $this->assertFileEquals($releaseDirectory . '/' . $current, $releaseDirectory . '/latest/' . $latest); + } + } + array_walk_recursive($releases, function ($value, $key) use ($latestVersion, $releaseDirectory): void { + if ($key === 'path') { + $this->assertStringContainsString($latestVersion, $value); + $this->assertFileExists($releaseDirectory . '/' . $value); + } + }); + foreach (['nts', 'ts'] as $ts) { + foreach (['x86', 'x64'] as $arch) { + foreach (['zip', 'debug_pack', 'devel_pack'] as $type) { + $entry = $releases['8.4']["$ts-vs17-$arch"][$type]; + $this->assertSame(hash_file('sha256', $releaseDirectory . '/' . $entry['path']), $entry['sha256']); + } + } + } + } + foreach (['8.4.1', '8.4.1-1', '8.4.1-2', '8.4.1-10'] as $old) { + foreach ($files as $file) { + $this->assertFileDoesNotExist($releaseDirectory . '/' . str_replace('8.4.1', $old, $file)); + } + } + } }

« previous php.webmaster (#34147) next »