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));
+ }
+ }
+ }
}