Author: Shivam Mathur (shivammathur)
Date: 2026-10-13T14:47:26+05:30
Commit:
https://github.com/php/web-downloads/commit/f276a47cd35ca80bb2c1a5792675293229e7699b
Raw diff:
https://github.com/php/web-downloads/commit/f276a47cd35ca80bb2c1a5792675293229e7699b.diff
Add support to init arch series files
Changed paths:
M .github/workflows/series-init.yml
M API.md
M src/Console/Command/PhpCommand.php
M src/Console/Command/SeriesDeleteCommand.php
M src/Console/Command/SeriesInitCommand.php
M src/Console/Command/SeriesStabilityCommand.php
M src/Console/Command/SeriesUpdateCommand.php
M src/Http/Controllers/SeriesInitController.php
M tests/Console/Command/PhpCommandTest.php
M tests/Console/Command/SeriesDeleteCommandTest.php
M tests/Console/Command/SeriesInitCommandTest.php
M tests/Console/Command/SeriesStabilityCommandTest.php
M tests/Console/Command/SeriesUpdateCommandTest.php
M tests/Http/Controllers/SeriesInitControllerTest.php
Diff:
diff --git a/.github/workflows/series-init.yml
b/.github/workflows/series-init.yml
index 371216a..f7fbb5f 100644
--- a/.github/workflows/series-init.yml
+++ b/.github/workflows/series-init.yml
@@ -1,10 +1,18 @@
-name: Create series files for new PHP version
-run-name: Series init ${{ inputs.php_version }}
+name: Create PHP or architecture series files
+run-name: Series init ${{ inputs.php_version }} (${{ inputs.subject }})
on:
workflow_dispatch:
inputs:
+ subject:
+ description: 'Initialize a VS/PHP series or copy an architecture
series'
+ type: choice
+ default: vs
+ required: true
+ options:
+ - vs
+ - arch
php_version:
- description: 'PHP Version'
+ description: 'PHP series (e.g. 8.4; master is also supported for
architecture copies)'
required: true
source_vs:
description: 'Source VS Version'
@@ -12,6 +20,22 @@ on:
target_vs:
description: 'Target VS Version'
required: true
+ source_arch:
+ description: 'Source architecture (used when subject is arch)'
+ type: choice
+ default: x64
+ options:
+ - x86
+ - x64
+ - arm64
+ target_arch:
+ description: 'Target architecture (used when subject is arch)'
+ type: choice
+ default: arm64
+ options:
+ - x86
+ - x64
+ - arm64
jobs:
test:
runs-on: ubuntu-latest
@@ -23,4 +47,4 @@ jobs:
--request POST \
--location https://downloads.php.net/api/series-init \
--header 'Authorization: Bearer ${{ secrets.AUTH_TOKEN }}' \
- --data '{ "php_version": "${{ inputs.php_version }}", "source_vs":
"${{ inputs.source_vs }}", "target_vs": "${{ inputs.target_vs }}" }'
+ --data '{ "subject": "${{ inputs.subject }}", "php_version": "${{
inputs.php_version }}", "source_vs": "${{ inputs.source_vs }}", "target_vs":
"${{ inputs.target_vs }}", "source_arch": "${{ inputs.source_arch }}",
"target_arch": "${{ inputs.target_arch }}" }'
diff --git a/API.md b/API.md
index 8b72718..d9564fa 100644
--- a/API.md
+++ b/API.md
@@ -217,21 +217,30 @@ curl -i -X POST \
### POST /api/series-init
- Auth: Required
-- Purpose: Initialize a series configuration by writing a JSON file into
`BUILDS_DIRECTORY/series`.
+- Purpose: Queue initialization of a new PHP series or copy an existing PHP
series to another architecture. The API writes a JSON job into
`BUILDS_DIRECTORY/series`.
+- Processor: `php runner.php series:init --base-directory=<downloads-root>
--builds-directory=<builds-root>`.
- Request body (JSON):
- - `php_version` (string, required): Matches `^\d+.\d+$`.
+ - `subject` (string, optional, default `vs`): `vs` uses the existing
initialization path from master; `arch` copies an architecture within the
selected PHP series.
+ - `php_version` (string, required): PHP series matching `^\d+\.\d+$`, such
as `8.2`. With `subject=arch`, also accepts `master`.
- `source_vs` (string, required): Matches `^v[c|s]\d{2}$`.
- `target_vs` (string, required): Matches `^v[c|s]\d{2}$`.
-- Success: `200 OK`, empty body.
-- Errors: `400` with validation details if input is invalid.
+ - `source_arch` (string, required when `subject=arch`): One of `x86`,
`x64`, or `arm64`. Ignored when `subject=vs`.
+ - `target_arch` (string, required when `subject=arch`): One of `x86`,
`x64`, or `arm64`. Ignored when `subject=vs`.
+- Success: `200 OK`, empty body, meaning the initialization job was queued.
+- Errors: `400` with validation details if input is invalid. Missing source
files and existing destinations are checked later by the processor, as
described below.
-Example
+With `subject=vs` (or `subject` omitted), the processor follows the existing
path. For each architecture, it copies
`packages-master-<source_vs>-<arch>-<stability>.txt` to
`packages-<php_version>-<target_vs>-<arch>-<stability>.txt` under
`php-sdk/deps/series`, for both `stable` and `staging`. The x86/x64 source
files are required; ARM64 files are copied when present and skipped
individually when missing. Package entries are copied unchanged. An existing
destination causes the job to fail.
+
+With `subject=arch`, the processor copies within the selected `php_version`,
including `master`. It reads
`packages-<php_version>-<source_vs>-<source_arch>-<stability>.txt` and creates
`packages-<php_version>-<target_vs>-<target_arch>-<stability>.txt` for both
`stable` and `staging`. Set `source_vs` and `target_vs` to the same value to
change only the architecture. In each file, `-<source_arch>.zip` is replaced
with `-<target_arch>.zip`; library names, versions, and VS labels in package
entries are preserved. Existing target files are skipped, so repeated
initialization preserves any subsequent updates. Missing source files for
targets that need creation fail the job before any series files are written.
Target ZIPs need not exist yet.
+
+Example (`subject=vs`: initialize a new PHP series from master)
```bash
curl -i -X POST \
-H "Authorization: Bearer $AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
+ "subject": "vs",
"php_version": "8.4",
"source_vs": "vc15",
"target_vs": "vs16"
@@ -239,6 +248,25 @@ curl -i -X POST \
https://downloads.php.net/api/series-init
```
+Example (`subject=arch`: initialize ARM64 from the existing PHP 8.2 x64 series)
+
+```bash
+curl -i -X POST \
+ -H "Authorization: Bearer $AUTH_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "subject": "arch",
+ "php_version": "8.2",
+ "source_vs": "vs16",
+ "target_vs": "vs16",
+ "source_arch": "x64",
+ "target_arch": "arm64"
+ }' \
+ https://downloads.php.net/api/series-init
+```
+
+This creates `packages-8.2-vs16-arm64-stable.txt` and
`packages-8.2-vs16-arm64-staging.txt` from the corresponding x64 files. For
example, `libffi-3.8.0-1-vs16-x64.zip` becomes `libffi-3.8.0-1-vs16-arm64.zip`.
The source series files remain unchanged.
+
---
### POST /api/series-delete
diff --git a/src/Console/Command/PhpCommand.php
b/src/Console/Command/PhpCommand.php
index a091373..da2da67 100644
--- a/src/Console/Command/PhpCommand.php
+++ b/src/Console/Command/PhpCommand.php
@@ -215,10 +215,13 @@ private function promoteSeriesFiles(string $phpVersion):
void
if (!is_dir($baseDirectory)) {
mkdir($baseDirectory, 0755, true);
}
- foreach(['x86', 'x64'] as $arch) {
+ foreach(['x86', 'x64', 'arm64'] as $arch) {
$sourceFile = $baseDirectory . '/packages-' . $phpVersion . '-' .
$vsVersion . '-' . $arch . '-staging.txt';
$destinationFile = $baseDirectory . '/packages-' . $phpVersion .
'-' . $vsVersion . '-' . $arch . '-stable.txt';
if(!file_exists($sourceFile)) {
+ if ($arch === 'arm64') {
+ continue;
+ }
throw new Exception($sourceFile . ' does not exist');
}
copy($sourceFile, $destinationFile);
diff --git a/src/Console/Command/SeriesDeleteCommand.php
b/src/Console/Command/SeriesDeleteCommand.php
index 9a2c8dd..9375fa6 100644
--- a/src/Console/Command/SeriesDeleteCommand.php
+++ b/src/Console/Command/SeriesDeleteCommand.php
@@ -73,7 +73,7 @@ private function deleteSeriesFiles(
if (!is_dir($baseDirectory)) {
mkdir($baseDirectory, 0755, true);
}
- foreach(['x86', 'x64'] as $arch) {
+ foreach(['x86', 'x64', 'arm64'] as $arch) {
foreach(['stable', 'staging'] as $stability) {
$filePath = $baseDirectory . '/packages-' . $php_version . '-'
. $vs_version . '-' . $arch . '-' . $stability . '.txt';
if(file_exists($filePath)) {
diff --git a/src/Console/Command/SeriesInitCommand.php
b/src/Console/Command/SeriesInitCommand.php
index 00ea201..4a45e28 100644
--- a/src/Console/Command/SeriesInitCommand.php
+++ b/src/Console/Command/SeriesInitCommand.php
@@ -46,11 +46,24 @@ public function handle(): int
foreach ($filteredFiles as $filepath) {
$data = json_decode(file_get_contents($filepath), true, 512,
JSON_THROW_ON_ERROR);
- $this->initSeriesFiles(
- $data['php_version'],
- $data['source_vs'],
- $data['target_vs']
- );
+ $subject = $data['subject'] ?? 'vs';
+ if ($subject === 'vs') {
+ $this->initSeriesFiles(
+ $data['php_version'],
+ $data['source_vs'],
+ $data['target_vs']
+ );
+ } elseif ($subject === 'arch') {
+ $this->initArchitectureSeriesFiles(
+ $data['php_version'],
+ $data['source_vs'],
+ $data['target_vs'],
+ $data['source_arch'] ?? '',
+ $data['target_arch'] ?? ''
+ );
+ } else {
+ throw new Exception('Subject must be vs or arch');
+ }
unlink($filepath);
unlink($filepath . '.lock');
}
@@ -61,6 +74,46 @@ public function handle(): int
}
}
+ /**
+ * @throws Exception
+ */
+ private function initArchitectureSeriesFiles(
+ string $phpVersion,
+ string $sourceVs,
+ string $targetVs,
+ string $sourceArch,
+ string $targetArch
+ ): void {
+ $architectures = ['x86', 'x64', 'arm64'];
+ if (!in_array($sourceArch, $architectures, true) ||
!in_array($targetArch, $architectures, true)) {
+ throw new Exception('Source and target architectures must both be
x86, x64, or arm64');
+ }
+
+ $seriesDirectory = $this->baseDirectory . '/php-sdk/deps/series';
+ $files = [];
+ foreach (['stable', 'staging'] as $stability) {
+ $sourceFile =
"$seriesDirectory/packages-$phpVersion-$sourceVs-$sourceArch-$stability.txt";
+ $targetFile =
"$seriesDirectory/packages-$phpVersion-$targetVs-$targetArch-$stability.txt";
+ if (file_exists($targetFile)) {
+ continue;
+ }
+ if (!is_file($sourceFile)) {
+ throw new Exception("$sourceFile not found");
+ }
+ $contents = file_get_contents($sourceFile);
+ if ($contents === false) {
+ throw new Exception("Unable to read $sourceFile");
+ }
+ $files[$targetFile] = str_replace("-$sourceArch.zip",
"-$targetArch.zip", $contents);
+ }
+
+ foreach ($files as $targetFile => $contents) {
+ if (file_put_contents($targetFile, $contents) === false) {
+ throw new Exception("Unable to write $targetFile");
+ }
+ }
+ }
+
/**
* @throws Exception
*/
@@ -75,10 +128,13 @@ private function initSeriesFiles(
if (!is_dir($baseDirectory)) {
mkdir($baseDirectory, 0755, true);
}
- foreach(['x86', 'x64'] as $arch) {
+ foreach(['x86', 'x64', 'arm64'] as $arch) {
foreach(['stable', 'staging'] as $stability) {
$sourceSeries = 'packages-master-' . $source_vs . '-' . $arch
. '-' . $stability . '.txt';
if(!file_exists($baseDirectory . '/' . $sourceSeries)) {
+ if ($arch === 'arm64') {
+ continue;
+ }
throw new Exception("$baseDirectory/$sourceSeries not
found");
}
$destinationFileName = 'packages-' . $php_version . '-' .
$target_vs . '-' . $arch . '-' . $stability . '.txt';
diff --git a/src/Console/Command/SeriesStabilityCommand.php
b/src/Console/Command/SeriesStabilityCommand.php
index 10fe913..bbd4eeb 100644
--- a/src/Console/Command/SeriesStabilityCommand.php
+++ b/src/Console/Command/SeriesStabilityCommand.php
@@ -73,10 +73,13 @@ private function promoteSeriesFiles(
if (!is_dir($baseDirectory)) {
mkdir($baseDirectory, 0755, true);
}
- foreach(['x86', 'x64'] as $arch) {
+ foreach(['x86', 'x64', 'arm64'] as $arch) {
$sourceFile = $baseDirectory . '/packages-' . $php_version . '-' .
$vs_version . '-' . $arch . '-staging.txt';
$destinationFile = $baseDirectory . '/packages-' . $php_version .
'-' . $vs_version . '-' . $arch . '-stable.txt';
if(!file_exists($sourceFile)) {
+ if ($arch === 'arm64') {
+ continue;
+ }
throw new Exception($sourceFile . ' does not exist');
}
copy($sourceFile, $destinationFile);
diff --git a/src/Console/Command/SeriesUpdateCommand.php
b/src/Console/Command/SeriesUpdateCommand.php
index b5f4373..f52b7f9 100644
--- a/src/Console/Command/SeriesUpdateCommand.php
+++ b/src/Console/Command/SeriesUpdateCommand.php
@@ -94,19 +94,24 @@ private function updateSeriesFiles(
mkdir($seriesDirectory, 0755, true);
}
- $arches = ['x86', 'x64'];
+ $arches = ['x86', 'x64', 'arm64'];
foreach ($arches as $arch) {
$filePath = $seriesDirectory .
"/packages-$phpVersion-$vsVersion-$arch-$stability.txt";
+ $refValue = trim($ref);
+ $package = $refValue === '' ? null : sprintf('%s-%s-%s-%s.zip',
$library, $refValue, $vsVersion, $arch);
+
+ // ARM64 is populated incrementally; preserve existing entries
until the requested build is uploaded.
+ if ($arch === 'arm64' && $package !== null
+ && (!is_file($filePath) || !is_file($this->baseDirectory .
"/php-sdk/deps/$vsVersion/$arch/$package"))) {
+ continue;
+ }
$lines = [];
if (file_exists($filePath)) {
$lines = file($filePath, FILE_IGNORE_NEW_LINES);
}
- $refValue = trim($ref);
- $package = $refValue === '' ? null : sprintf('%s-%s-%s-%s.zip',
$library, $refValue, $vsVersion, $arch);
-
$replaced = false;
foreach ($lines as $index => $line) {
if (str_starts_with($line, $library . '-')) {
diff --git a/src/Http/Controllers/SeriesInitController.php
b/src/Http/Controllers/SeriesInitController.php
index 63ca1cd..2373b22 100644
--- a/src/Http/Controllers/SeriesInitController.php
+++ b/src/Http/Controllers/SeriesInitController.php
@@ -10,11 +10,20 @@ class SeriesInitController extends BaseController
{
protected function validate(array $data): bool
{
- $validator = new Validator([
- 'php_version' => 'required|string|regex:/^\d+\.\d+$/',
+ $data += ['subject' => 'vs'];
+ $rules = [
+ 'subject' => 'required|string|regex:/\A(?:vs|arch)\z/',
+ 'php_version' => $data['subject'] === 'arch'
+ ? 'required|string|regex:/\A(?:\d+\.\d+|master)\z/'
+ : 'required|string|regex:/^\d+\.\d+$/',
'source_vs' => 'required|string|regex:/^v[c|s]\d{2}$/',
'target_vs' => 'required|string|regex:/^v[c|s]\d{2}$/',
- ]);
+ ];
+ if ($data['subject'] === 'arch') {
+ $rules['source_arch'] =
'required|string|regex:/\A(?:x86|x64|arm64)\z/';
+ $rules['target_arch'] =
'required|string|regex:/\A(?:x86|x64|arm64)\z/';
+ }
+ $validator = new Validator($rules);
$validator->validate($data);
diff --git a/tests/Console/Command/PhpCommandTest.php
b/tests/Console/Command/PhpCommandTest.php
index b731efb..d0b490b 100644
--- a/tests/Console/Command/PhpCommandTest.php
+++ b/tests/Console/Command/PhpCommandTest.php
@@ -45,7 +45,7 @@ protected function tearDown(): void
public static function buildsProvider(): array
{
- return [
+ $builds = [
[[
'php-8.4.1-Win32-vs17-x64.zip',
'php-8.4.1-Win32-vs17-x64.zip.cdx.json',
@@ -83,6 +83,13 @@ 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('-x64',
'-arm64', $file),
+ array_filter($files, static fn (string $file): bool =>
!str_contains($file, '-x86'))
+ )];
+ }
+ return $builds;
}
private function stageBuilds(array $phpZips, $zipPath): void
@@ -130,6 +137,21 @@ public function
testCommandHandlesSuccessfulExecution(array $phpZips): void
$this->assertStringNotContainsString('.json',
file_get_contents($expectedDestination . '/sha256sum.txt'));
$this->assertStringNotContainsString('.json',
file_get_contents($expectedDestination . '/sha1sum.txt'));
$this->assertStringNotContainsString('.json',
file_get_contents($expectedDestination . '/releases.json'));
+ if (str_contains($phpZips[0], '-arm64')) {
+ $releases = json_decode(file_get_contents($expectedDestination .
'/releases.json'), true);
+ foreach (['ts', 'nts'] as $threadSafety) {
+ $build = $releases['8.4']["$threadSafety-vs17-arm64"];
+ foreach (['zip', 'debug_pack', 'devel_pack'] as $type) {
+ $path = $build[$type]['path'];
+ $this->assertSame(hash_file('sha256',
"$expectedDestination/$path"), $build[$type]['sha256']);
+ $this->assertFileEquals("$expectedDestination/$path",
"$expectedDestination/archives/$path");
+ }
+ if ($expectedDestination === $this->baseDirectory .
'/releases') {
+ $latest = str_replace([$releases['8.4']['version'],
'.zip'], ['8.4', '-latest.zip'], $build['zip']['path']);
+ $this->assertFileEquals("$expectedDestination/" .
$build['zip']['path'], "$expectedDestination/latest/$latest");
+ }
+ }
+ }
}
public function testCommandHandlerWithMissingTestPackZip(): void
@@ -176,8 +198,27 @@ public function testCleanupAfterCommand(): void
$this->assertEmpty(glob($tempDirectory));
}
- public function testStableReleasePromotesSeriesFiles(): void
+ public static function seriesPromotionProvider(): array
+ {
+ return [
+ 'no ARM64 files' => [null, null],
+ 'ARM64 staging only' =>
["libffi-3.8.0-1-vs17-arm64.zip\nzlib-1.3.2-vs17-x64.zip", null],
+ 'ARM64 stable only' => [null, 'existing-arm64-stable'],
+ 'both ARM64 files' => ['new-arm64-staging',
'existing-arm64-stable'],
+ ];
+ }
+
+ #[DataProvider('seriesPromotionProvider')]
+ public function testStableReleasePromotesSeriesFiles(?string
$arm64Staging, ?string $arm64Stable): void
{
+ $seriesBase = $this->baseDirectory . '/php-sdk/deps/series';
+ $dstArm64 = "$seriesBase/packages-8.4-vs17-arm64-stable.txt";
+ if ($arm64Staging !== null) {
+
file_put_contents("$seriesBase/packages-8.4-vs17-arm64-staging.txt",
$arm64Staging);
+ }
+ if ($arm64Stable !== null) {
+ file_put_contents($dstArm64, $arm64Stable);
+ }
$command = new PhpCommand(new GetListing(), new UpdateReleasesJson());
$command->options = ['base-directory' => $this->baseDirectory,
'builds-directory' => $this->buildsDirectory];
@@ -190,10 +231,16 @@ public function testStableReleasePromotesSeriesFiles():
void
$this->assertFileExists($this->baseDirectory .
'/php-sdk/deps/series/packages-8.4-vs17-x64-stable.txt');
$this->assertSame('x86-staging',
file_get_contents($this->baseDirectory .
'/php-sdk/deps/series/packages-8.4-vs17-x86-stable.txt'));
$this->assertSame('x64-staging',
file_get_contents($this->baseDirectory .
'/php-sdk/deps/series/packages-8.4-vs17-x64-stable.txt'));
+ if ($arm64Staging !== null || $arm64Stable !== null) {
+ $this->assertStringEqualsFile($dstArm64, $arm64Staging ??
$arm64Stable);
+ } else {
+ $this->assertFileDoesNotExist($dstArm64);
+ }
}
public function testQaReleaseDoesNotPromoteSeriesFiles(): void
{
+ file_put_contents($this->baseDirectory .
'/php-sdk/deps/series/packages-8.4-vs17-arm64-staging.txt', 'arm64-staging');
$command = new PhpCommand(new GetListing(), new UpdateReleasesJson());
$command->options = ['base-directory' => $this->baseDirectory,
'builds-directory' => $this->buildsDirectory];
@@ -204,5 +251,6 @@ public function testQaReleaseDoesNotPromoteSeriesFiles():
void
$this->assertSame(0, $result);
$this->assertFileDoesNotExist($this->baseDirectory .
'/php-sdk/deps/series/packages-8.4-vs17-x86-stable.txt');
$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');
}
}
diff --git a/tests/Console/Command/SeriesDeleteCommandTest.php
b/tests/Console/Command/SeriesDeleteCommandTest.php
index 65246b7..9ce469d 100644
--- a/tests/Console/Command/SeriesDeleteCommandTest.php
+++ b/tests/Console/Command/SeriesDeleteCommandTest.php
@@ -88,6 +88,8 @@ public function
testDeletesAllSeriesFilesAndCleansUpTask(string $phpVersion, str
"$seriesBase/packages-$phpVersion-$vsVersion-x86-staging.txt",
"$seriesBase/packages-$phpVersion-$vsVersion-x64-stable.txt",
"$seriesBase/packages-$phpVersion-$vsVersion-x64-staging.txt",
+ "$seriesBase/packages-$phpVersion-$vsVersion-arm64-stable.txt",
+ "$seriesBase/packages-$phpVersion-$vsVersion-arm64-staging.txt",
];
foreach ($paths as $p) {
file_put_contents($p, "dummy");
@@ -133,6 +135,8 @@ public function testIdempotentWhenSomeFilesMissing(string
$phpVersion, string $v
$missing = [
"$seriesBase/packages-$phpVersion-$vsVersion-x86-staging.txt",
"$seriesBase/packages-$phpVersion-$vsVersion-x64-stable.txt",
+ "$seriesBase/packages-$phpVersion-$vsVersion-arm64-stable.txt",
+ "$seriesBase/packages-$phpVersion-$vsVersion-arm64-staging.txt",
];
$seriesDir = $this->buildsDirectory . '/series';
diff --git a/tests/Console/Command/SeriesInitCommandTest.php
b/tests/Console/Command/SeriesInitCommandTest.php
index 1f7199a..70d1ff6 100644
--- a/tests/Console/Command/SeriesInitCommandTest.php
+++ b/tests/Console/Command/SeriesInitCommandTest.php
@@ -36,6 +36,9 @@ public static function caseProvider(): array
['8.3', 'vs17', 'vs17'],
['8.2', 'vs16', 'vs16'],
['8.7', 'vs18', 'vs18'],
+ ['8.7', 'vs18', 'vs18', ['stable', 'staging']],
+ ['8.7', 'vs18', 'vs18', ['stable']],
+ ['8.7', 'vs18', 'vs18', ['staging']],
];
}
@@ -79,7 +82,7 @@ public function testMissingBuildsDirectory(): void
}
#[DataProvider('caseProvider')]
- public function testProcessesJsonCopiesSeriesFilesAndCleansUp(string
$series, string $series_vs, string $target_vs): void
+ public function testProcessesJsonCopiesSeriesFilesAndCleansUp(string
$series, string $series_vs, string $target_vs, array $arm64Stabilities = []):
void
{
$seriesBase = $this->baseDirectory . '/php-sdk/deps/series';
mkdir($seriesBase, 0755, true);
@@ -90,6 +93,9 @@ public function
testProcessesJsonCopiesSeriesFilesAndCleansUp(string $series, st
"packages-master-$series_vs-x64-stable.txt",
"packages-master-$series_vs-x64-staging.txt",
];
+ foreach ($arm64Stabilities as $stability) {
+ $sourceNames[] = "packages-master-$series_vs-arm64-$stability.txt";
+ }
foreach ($sourceNames as $name) {
file_put_contents($seriesBase . '/' . $name, "content-of-$name");
}
@@ -115,6 +121,9 @@ public function
testProcessesJsonCopiesSeriesFilesAndCleansUp(string $series, st
"packages-$series-$target_vs-x64-stable.txt",
"packages-$series-$target_vs-x64-staging.txt",
];
+ foreach ($arm64Stabilities as $stability) {
+ $destinations[] =
"packages-$series-$target_vs-arm64-$stability.txt";
+ }
foreach ($destinations as $dest) {
$destPath = $seriesBase . '/' . $dest;
@@ -129,6 +138,9 @@ public function
testProcessesJsonCopiesSeriesFilesAndCleansUp(string $series, st
$this->assertFileDoesNotExist($jsonPath);
$this->assertFileDoesNotExist($jsonPath . '.lock');
+ foreach (array_diff(['stable', 'staging'], $arm64Stabilities) as
$stability) {
+
$this->assertFileDoesNotExist("$seriesBase/packages-$series-$target_vs-arm64-$stability.txt");
+ }
}
public function testSkipsLockedJsonFile(): void
@@ -173,6 +185,109 @@ public function testSkipsLockedJsonFile(): void
$this->assertFileDoesNotExist($seriesBase .
"/packages-$series-$target_vs-x64-staging.txt");
}
+ public static function architectureProvider(): array
+ {
+ return [
+ 'x64 to ARM64' => ['8.2', 'vs16', 'vs16', 'x64', 'arm64', []],
+ 'master' => ['master', 'vs18', 'vs18', 'x64', 'arm64', []],
+ 'different VS target' => ['8.4', 'vs16', 'vs17', 'x64', 'arm64',
[]],
+ 'x86 source' => ['8.2', 'vs16', 'vs16', 'x86', 'arm64', []],
+ 'preserve stable' => ['8.2', 'vs16', 'vs16', 'x64', 'arm64',
['stable']],
+ 'preserve staging' => ['8.2', 'vs16', 'vs16', 'x64', 'arm64',
['staging']],
+ 'preserve both' => ['8.2', 'vs16', 'vs16', 'x64', 'arm64',
['stable', 'staging']],
+ ];
+ }
+
+ #[DataProvider('architectureProvider')]
+ public function testInitializesArchitectureWithinSelectedSeries(
+ string $phpVersion,
+ string $sourceVs,
+ string $targetVs,
+ string $sourceArch,
+ string $targetArch,
+ array $existingStabilities
+ ): void {
+ $seriesDirectory = $this->baseDirectory . '/php-sdk/deps/series';
+ mkdir($seriesDirectory, 0755, true);
+ $sourceContents =
"libffi-3.8.0-1-$sourceVs-$sourceArch.zip\r\nx64-helper-1.0-$sourceVs-$sourceArch.zip\r\n";
+ $expectedContents =
"libffi-3.8.0-1-$sourceVs-$targetArch.zip\r\nx64-helper-1.0-$sourceVs-$targetArch.zip\r\n";
+ foreach (['stable', 'staging'] as $stability) {
+
file_put_contents("$seriesDirectory/packages-$phpVersion-$sourceVs-$sourceArch-$stability.txt",
$sourceContents);
+ if (in_array($stability, $existingStabilities, true)) {
+
file_put_contents("$seriesDirectory/packages-$phpVersion-$targetVs-$targetArch-$stability.txt",
"existing-$stability");
+ }
+ }
+ $task = [
+ 'subject' => 'arch',
+ 'php_version' => $phpVersion,
+ 'source_vs' => $sourceVs,
+ 'target_vs' => $targetVs,
+ 'source_arch' => $sourceArch,
+ 'target_arch' => $targetArch,
+ ];
+ $jsonPath = $this->createTask($task);
+ $command = new SeriesInitCommand();
+ $command->options = ['base-directory' => $this->baseDirectory,
'builds-directory' => $this->buildsDirectory];
+
+ $this->assertSame(0, $command->handle());
+
+ foreach (['stable', 'staging'] as $stability) {
+ $expected = in_array($stability, $existingStabilities, true) ?
"existing-$stability" : $expectedContents;
+
$this->assertStringEqualsFile("$seriesDirectory/packages-$phpVersion-$targetVs-$targetArch-$stability.txt",
$expected);
+
$this->assertStringEqualsFile("$seriesDirectory/packages-$phpVersion-$sourceVs-$sourceArch-$stability.txt",
$sourceContents);
+ }
+ $this->assertFileDoesNotExist($jsonPath);
+ $this->assertFileDoesNotExist($jsonPath . '.lock');
+
+ // Repeating initialization must preserve series that have since been
updated.
+ $targetStable =
"$seriesDirectory/packages-$phpVersion-$targetVs-$targetArch-stable.txt";
+ file_put_contents($targetStable, 'updated-after-init');
+ $jsonPath = $this->createTask($task);
+ $this->assertSame(0, $command->handle());
+ $this->assertStringEqualsFile($targetStable, 'updated-after-init');
+ $this->assertFileDoesNotExist($jsonPath);
+ $this->assertFileDoesNotExist($jsonPath . '.lock');
+ }
+
+ public function testMissingArchitectureSourceDoesNotCreatePartialSeries():
void
+ {
+ $seriesDirectory = $this->baseDirectory . '/php-sdk/deps/series';
+ mkdir($seriesDirectory, 0755, true);
+ file_put_contents("$seriesDirectory/packages-8.2-vs16-x64-stable.txt",
'libffi-3.8.0-1-vs16-x64.zip');
+ $jsonPath = $this->createTask([
+ 'subject' => 'arch',
+ 'php_version' => '8.2',
+ 'source_vs' => 'vs16',
+ 'target_vs' => 'vs16',
+ 'source_arch' => 'x64',
+ 'target_arch' => 'arm64',
+ ]);
+ $command = new SeriesInitCommand();
+ $command->options = ['base-directory' => $this->baseDirectory,
'builds-directory' => $this->buildsDirectory];
+
+ ob_start();
+ $result = $command->handle();
+ $output = ob_get_clean();
+
+ $this->assertSame(1, $result);
+ $this->assertStringContainsString('packages-8.2-vs16-x64-staging.txt
not found', $output);
+
$this->assertFileDoesNotExist("$seriesDirectory/packages-8.2-vs16-arm64-stable.txt");
+
$this->assertFileDoesNotExist("$seriesDirectory/packages-8.2-vs16-arm64-staging.txt");
+ $this->assertFileExists($jsonPath);
+ $this->assertFileExists($jsonPath . '.lock');
+ }
+
+ private function createTask(array $data): string
+ {
+ $seriesDirectory = $this->buildsDirectory . '/series';
+ if (!is_dir($seriesDirectory)) {
+ mkdir($seriesDirectory, 0755, true);
+ }
+ $file = $seriesDirectory . '/series-init-' . uniqid() . '.json';
+ file_put_contents($file, json_encode($data));
+ return $file;
+ }
+
public function testHandlesCorruptJson(): void
{
$seriesDir = $this->buildsDirectory . '/series';
diff --git a/tests/Console/Command/SeriesStabilityCommandTest.php
b/tests/Console/Command/SeriesStabilityCommandTest.php
index ad51f9a..3e4f2e7 100644
--- a/tests/Console/Command/SeriesStabilityCommandTest.php
+++ b/tests/Console/Command/SeriesStabilityCommandTest.php
@@ -35,6 +35,9 @@ public static function versionProvider(): array
return [
['8.3', 'vs17'],
['8.2', 'vs16'],
+ ['8.4', 'vs17',
"libffi-3.8.0-1-vs17-arm64.zip\nzlib-1.3.2-vs17-x64.zip"],
+ ['8.4', 'vs17', null, 'existing-arm64-stable'],
+ ['8.4', 'vs17', 'new-arm64-staging', 'existing-arm64-stable'],
];
}
@@ -78,7 +81,7 @@ public function testMissingBuildsDirectory(): void
}
#[DataProvider('versionProvider')]
- public function testPromotesStagingToStableAndCleansTask(string
$phpVersion, string $vsVersion): void
+ public function testPromotesStagingToStableAndCleansTask(string
$phpVersion, string $vsVersion, ?string $arm64Staging = null, ?string
$arm64Stable = null): void
{
$seriesBase = $this->baseDirectory . '/php-sdk/deps/series';
mkdir($seriesBase, 0755, true);
@@ -87,6 +90,14 @@ public function
testPromotesStagingToStableAndCleansTask(string $phpVersion, str
$srcX64 =
"$seriesBase/packages-$phpVersion-$vsVersion-x64-staging.txt";
file_put_contents($srcX86, "x86-content");
file_put_contents($srcX64, "x64-content");
+ $srcArm64 =
"$seriesBase/packages-$phpVersion-$vsVersion-arm64-staging.txt";
+ $dstArm64 =
"$seriesBase/packages-$phpVersion-$vsVersion-arm64-stable.txt";
+ if ($arm64Staging !== null) {
+ file_put_contents($srcArm64, $arm64Staging);
+ }
+ if ($arm64Stable !== null) {
+ file_put_contents($dstArm64, $arm64Stable);
+ }
$seriesDir = $this->buildsDirectory . '/series';
mkdir($seriesDir, 0755, true);
@@ -108,6 +119,11 @@ public function
testPromotesStagingToStableAndCleansTask(string $phpVersion, str
$this->assertFileExists($dstX64);
$this->assertSame("x86-content", file_get_contents($dstX86));
$this->assertSame("x64-content", file_get_contents($dstX64));
+ if ($arm64Staging !== null || $arm64Stable !== null) {
+ $this->assertStringEqualsFile($dstArm64, $arm64Staging ??
$arm64Stable);
+ } else {
+ $this->assertFileDoesNotExist($dstArm64);
+ }
$this->assertFileDoesNotExist($jsonPath);
$this->assertFileDoesNotExist($jsonPath . '.lock');
diff --git a/tests/Console/Command/SeriesUpdateCommandTest.php
b/tests/Console/Command/SeriesUpdateCommandTest.php
index fb7d532..d25208f 100644
--- a/tests/Console/Command/SeriesUpdateCommandTest.php
+++ b/tests/Console/Command/SeriesUpdateCommandTest.php
@@ -5,6 +5,7 @@
use App\Console\Command\SeriesUpdateCommand;
use App\Helpers\Helpers;
+use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
class SeriesUpdateCommandTest extends TestCase
@@ -244,6 +245,114 @@ public function testCreatesSeriesFileWhenMissing(): void
$this->assertSame([
'libpq-16.0.0-vs17-x86.zip',
], file($filePathX86, FILE_IGNORE_NEW_LINES));
+ $this->assertFileDoesNotExist($this->baseDirectory .
'/php-sdk/deps/series/packages-8.3-vs17-arm64-staging.txt');
+ }
+
+ public static function arm64UpdateProvider(): array
+ {
+ return [
+ 'replace x64 placeholder' => [true, 'libffi-3.7.0-vs16-x64.zip',
true],
+ 'replace old ARM64 build' => [true, 'libffi-3.7.0-vs16-arm64.zip',
true],
+ 'add available ARM64 library' => [true, null, true],
+ 'keep x64 placeholder until upload' => [true,
'libffi-3.7.0-vs16-x64.zip', false],
+ 'keep previous ARM64 build until upload' => [true,
'libffi-3.7.0-vs16-arm64.zip', false],
+ 'skip unavailable ARM64 library' => [true, null, false],
+ 'skip missing ARM64 series' => [false, null, true],
+ 'skip absent ARM64 support' => [false, null, false],
+ ];
+ }
+
+ #[DataProvider('arm64UpdateProvider')]
+ public function
testUpdatesArm64OnlyWhenSeriesAndRequestedArtifactExist(bool $hasSeries,
?string $existingLibrary, bool $hasArtifact): void
+ {
+ $seriesBase = $this->baseDirectory . '/php-sdk/deps/series';
+ $artifactBase = $this->baseDirectory . '/php-sdk/deps/vs16/arm64';
+ mkdir($seriesBase, 0755, true);
+ mkdir($artifactBase, 0755, true);
+ $seriesFile = "$seriesBase/packages-8.2-vs16-arm64-staging.txt";
+ $package = 'libffi-3.8.0-1-vs16-arm64.zip';
+ $lines = ['zlib-1.3.2-vs16-x64.zip'];
+ if ($existingLibrary !== null) {
+ array_unshift($lines, $existingLibrary);
+ }
+ if ($hasSeries) {
+ file_put_contents($seriesFile, implode("\n", $lines));
+ }
+ // An older artifact must not satisfy the check for the requested
version.
+ file_put_contents("$artifactBase/libffi-3.7.0-vs16-arm64.zip", 'old
artifact');
+ if ($hasArtifact) {
+ file_put_contents("$artifactBase/$package", 'new artifact');
+ }
+ $taskFile = $this->createTask([
+ 'php_version' => '8.2',
+ 'vs_version' => 'vs16',
+ 'stability' => 'staging',
+ 'library' => 'libffi',
+ 'ref' => '3.8.0-1',
+ ]);
+
+ $command = new SeriesUpdateCommand();
+ $command->options = ['base-directory' => $this->baseDirectory,
'builds-directory' => $this->buildsDirectory];
+ $this->assertSame(0, $command->handle());
+
+ if ($hasSeries) {
+ $expected = $hasArtifact ? [$package, 'zlib-1.3.2-vs16-x64.zip'] :
$lines;
+ $this->assertSame($expected, file($seriesFile,
FILE_IGNORE_NEW_LINES));
+ } else {
+ $this->assertFileDoesNotExist($seriesFile);
+ }
+ foreach (['x86', 'x64'] as $arch) {
+
$this->assertStringEqualsFile("$seriesBase/packages-8.2-vs16-$arch-staging.txt",
"libffi-3.8.0-1-vs16-$arch.zip");
+ }
+ $this->assertFileDoesNotExist($taskFile);
+ $this->assertFileDoesNotExist($taskFile . '.lock');
+ }
+
+ public static function arm64RemovalProvider(): array
+ {
+ return [
+ 'mixed series' => [["libffi-3.8.0-1-vs16-arm64.zip",
"zlib-1.3.2-vs16-x64.zip"], ['zlib-1.3.2-vs16-x64.zip']],
+ 'x64 placeholder' => [['libffi-3.7.0-vs16-x64.zip'], null],
+ 'missing series' => [null, null],
+ ];
+ }
+
+ #[DataProvider('arm64RemovalProvider')]
+ public function testRemovesArm64LibraryAndArtifacts(?array $lines, ?array
$remaining): void
+ {
+ $seriesBase = $this->baseDirectory . '/php-sdk/deps/series';
+ $artifactBase = $this->baseDirectory . '/php-sdk/deps/vs16/arm64';
+ mkdir($seriesBase, 0755, true);
+ mkdir($artifactBase, 0755, true);
+ $seriesFile = "$seriesBase/packages-8.2-vs16-arm64-staging.txt";
+ if ($lines !== null) {
+ file_put_contents($seriesFile, implode("\n", $lines));
+ }
+ $artifact = "$artifactBase/libffi-3.8.0-1-vs16-arm64.zip";
+ $unrelated = "$artifactBase/zlib-1.3.2-vs16-arm64.zip";
+ file_put_contents($artifact, 'remove');
+ file_put_contents($unrelated, 'keep');
+ $taskFile = $this->createTask([
+ 'php_version' => '8.2',
+ 'vs_version' => 'vs16',
+ 'stability' => 'staging',
+ 'library' => 'libffi',
+ 'ref' => '',
+ ]);
+
+ $command = new SeriesUpdateCommand();
+ $command->options = ['base-directory' => $this->baseDirectory,
'builds-directory' => $this->buildsDirectory];
+ $this->assertSame(0, $command->handle());
+
+ if ($remaining !== null) {
+ $this->assertSame($remaining, file($seriesFile,
FILE_IGNORE_NEW_LINES));
+ } else {
+ $this->assertFileDoesNotExist($seriesFile);
+ }
+ $this->assertFileDoesNotExist($artifact);
+ $this->assertStringEqualsFile($unrelated, 'keep');
+ $this->assertFileDoesNotExist($taskFile);
+ $this->assertFileDoesNotExist($taskFile . '.lock');
}
public function testSkipsLockedTask(): void
diff --git a/tests/Http/Controllers/SeriesInitControllerTest.php
b/tests/Http/Controllers/SeriesInitControllerTest.php
index 156d77c..ad0a538 100644
--- a/tests/Http/Controllers/SeriesInitControllerTest.php
+++ b/tests/Http/Controllers/SeriesInitControllerTest.php
@@ -3,44 +3,118 @@
namespace Http\Controllers;
+use App\Console\Command\SeriesInitCommand;
+use App\Helpers\Helpers;
use App\Http\Controllers\SeriesInitController;
-use JsonException;
-use Override;
+use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
-class MockSeriesInitController extends SeriesInitController {
- #[Override]
- protected function validate(array $data): bool {
- return isset($data['key']);
+class SeriesInitControllerTest extends TestCase
+{
+ private string $directory;
+ private string|false $originalBuildsDirectory;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->directory = sys_get_temp_dir() . '/series_init_controller_' .
uniqid();
+ mkdir($this->directory . '/builds/series', 0755, true);
+ $this->originalBuildsDirectory = getenv('BUILDS_DIRECTORY');
+ putenv('BUILDS_DIRECTORY=' . $this->directory . '/builds');
+ http_response_code(200);
+ }
+
+ protected function tearDown(): void
+ {
+ putenv($this->originalBuildsDirectory === false ? 'BUILDS_DIRECTORY' :
'BUILDS_DIRECTORY=' . $this->originalBuildsDirectory);
+ http_response_code(200);
+ Helpers::rmdirr($this->directory);
+ parent::tearDown();
}
- #[Override]
- protected function execute(array $data): void {
- echo "Executed";
+ public static function validPayloadProvider(): array
+ {
+ return [
+ 'legacy request' => [[]],
+ 'VS initialization' => [['subject' => 'vs']],
+ 'VS initialization ignores architecture inputs' => [['subject' =>
'vs', 'source_arch' => 'x64', 'target_arch' => 'arm64']],
+ 'missing subject defaults to VS initialization' => [['source_arch'
=> 'x64', 'target_arch' => 'arm64']],
+ 'ARM64 for existing PHP series' => [['subject' => 'arch',
'source_arch' => 'x64', 'target_arch' => 'arm64']],
+ 'ARM64 for master' => [['subject' => 'arch', 'php_version' =>
'master', 'source_arch' => 'x64', 'target_arch' => 'arm64']],
+ ];
}
- #[Override]
- public function handle(): void
+ #[DataProvider('validPayloadProvider')]
+ public function testRequestCreatesExpectedSeries(array $fields): void
{
- $data = json_decode(file_get_contents($this->inputPath), true);
+ $payload = $fields + ['php_version' => '8.2', 'source_vs' => 'vs16',
'target_vs' => 'vs16'];
+ $input = $this->directory . '/input.json';
+ file_put_contents($input, json_encode($payload));
+ (new SeriesInitController($input))->handle();
+ $this->assertSame(200, http_response_code());
+ $jobs = glob($this->directory . '/builds/series/series-init-*.json');
+ $this->assertCount(1, $jobs);
+ $this->assertSame($payload, json_decode(file_get_contents($jobs[0]),
true));
- if ($this->validate($data)) {
- $this->execute($data);
+ $seriesDirectory = $this->directory . '/base/php-sdk/deps/series';
+ mkdir($seriesDirectory, 0755, true);
+ $architectureCopy = ($payload['subject'] ?? 'vs') === 'arch';
+ $phpVersion = $payload['php_version'];
+ $sourceVersion = $architectureCopy ? $phpVersion : 'master';
+ $sourceArchitectures = $architectureCopy ? ['x64'] : ['x86', 'x64'];
+ foreach ($sourceArchitectures as $arch) {
+ foreach (['stable', 'staging'] as $stability) {
+
file_put_contents("$seriesDirectory/packages-$sourceVersion-vs16-$arch-$stability.txt",
"libffi-3.8.0-1-vs16-$arch.zip\nzlib-1.3.2-vs16-$arch.zip\n");
+ }
}
+
+ $command = new SeriesInitCommand();
+ $command->options = ['base-directory' => $this->directory . '/base',
'builds-directory' => $this->directory . '/builds'];
+ $this->assertSame(0, $command->handle());
+
+ foreach ($architectureCopy ? ['arm64'] : ['x86', 'x64'] as $arch) {
+ foreach (['stable', 'staging'] as $stability) {
+
$this->assertStringEqualsFile("$seriesDirectory/packages-$phpVersion-vs16-$arch-$stability.txt",
"libffi-3.8.0-1-vs16-$arch.zip\nzlib-1.3.2-vs16-$arch.zip\n");
+ }
+ }
+ $this->assertFileDoesNotExist($jobs[0]);
+ $this->assertFileDoesNotExist($jobs[0] . '.lock');
}
-}
-class SeriesInitControllerTest extends TestCase {
- /**
- * @throws JsonException
- */
- public function testHandleWithValidData() {
- $data = json_encode(["key" => "value"]);
- $tempFile = tempnam(sys_get_temp_dir(), 'phpunit');
- file_put_contents($tempFile, $data);
- $controller = new MockSeriesInitController($tempFile);
- $this->expectOutputString("Executed");
- $controller->handle();
- unlink($tempFile);
+ public static function invalidPayloadProvider(): array
+ {
+ return [
+ 'missing target architecture' => [['source_arch' => 'x64']],
+ 'missing source architecture' => [['target_arch' => 'arm64']],
+ 'all to specific architecture' => [['source_arch' => 'all',
'target_arch' => 'arm64']],
+ 'specific architecture to all' => [['source_arch' => 'x64',
'target_arch' => 'all']],
+ 'invalid architecture' => [['source_arch' => 'x64', 'target_arch'
=> 'aarch64']],
+ 'architecture path' => [['source_arch' => '../x64', 'target_arch'
=> 'arm64']],
+ 'architecture trailing newline' => [['source_arch' => 'x64',
'target_arch' => "arm64\n"]],
+ 'null architecture' => [['source_arch' => null, 'target_arch' =>
'arm64']],
+ 'empty architecture' => [['source_arch' => '', 'target_arch' =>
'arm64']],
+ 'array architecture' => [['source_arch' => ['x64'], 'target_arch'
=> 'arm64']],
+ 'invalid PHP series' => [['php_version' => '../8.2', 'source_arch'
=> 'x64', 'target_arch' => 'arm64']],
+ 'VS master request' => [['subject' => 'vs', 'php_version' =>
'master']],
+ 'invalid subject' => [['subject' => 'all']],
+ 'null subject' => [['subject' => null]],
+ 'empty subject' => [['subject' => '']],
+ 'array subject' => [['subject' => ['arch']]],
+ 'subject trailing newline' => [['subject' => "arch\n"]],
+ ];
+ }
+
+ #[DataProvider('invalidPayloadProvider')]
+ public function testRejectsInvalidArchitectureCopy(array $fields): void
+ {
+ $input = $this->directory . '/input.json';
+ file_put_contents($input, json_encode($fields + ['subject' => 'arch',
'php_version' => '8.2', 'source_vs' => 'vs16', 'target_vs' => 'vs16']));
+ ob_start();
+ (new SeriesInitController($input))->handle();
+ $output = ob_get_clean();
+
+ $this->assertSame(400, http_response_code());
+ $this->assertStringStartsWith('Invalid request:', $output);
+ $this->assertEmpty(glob($this->directory .
'/builds/series/series-init-*.json'));
}
}