Author: Shivam Mathur (shivammathur)
Date: 2026-10-02T21:49:18+05:30
Commit: https://github.com/php/web-downloads/commit/92114628ae76c128da0d938e934424f490c1f385
Raw diff: https://github.com/php/web-downloads/commit/92114628ae76c128da0d938e934424f490c1f385.diff
Process Winlibs deletions from the existing build queue
Changed paths:
M API.md
M src/Actions/DeleteWinlibsBuild.php
M src/Console/Command/WinlibsCommand.php
M src/Http/Controllers/WinlibsDeleteController.php
M tests/Console/Command/WinlibsCommandDeleteTest.php
M tests/Http/Controllers/WinlibsDeleteControllerTest.php
Diff:
diff --git a/API.md b/API.md
index 8ad1453..779e438 100644
--- a/API.md
+++ b/API.md
@@ -190,13 +190,14 @@ curl -i -X POST \
### POST /api/winlibs-delete
- Auth: Required
-- Purpose: Queue deletion of one published Winlibs ZIP and its package references. The existing
winlibs:add runner processes deletion jobs after uploads. winlibs:add
--delete processes only deletion jobs when run manually.
+- Purpose: Queue deletion of one published Winlibs ZIP and its package references. The API writes a
winlibs/delete-*/data.json job into the existing Winlibs queue. The existing
winlibs:add cron processes uploads first and deletion jobs afterward. winlibs:add
--delete processes only deletion jobs when run manually.
- Request body (JSON):
- type (string, required): php or pecl.
- filename (string, required): Exact ZIP basename, such as
libcurl-8.22.0-1-vs18-x64.zip. Paths and non-ZIP names are rejected.
- For php, the processor removes exact matching lines from every
php-sdk/deps/series/packages-*.txt file and deletes every copy of that filename under
php-sdk/deps/<VS>/<arch>/. This covers builds copied to multiple VS
targets. An empty series file is removed.
- For pecl, the processor removes the exact line from
pecl/deps/packages.txt and deletes pecl/deps/<filename>. It leaves
every unrelated index entry unchanged.
- Success: 200 OK, empty body, meaning the deletion was queued. Repeating a deletion
is safe.
+- The next valid request also moves jobs previously queued under winlibs-delete/ into
the Winlibs queue, so older pending requests can be recovered.
- Errors:
- 400 with validation details if the payload is invalid.
- 500 if BUILDS_DIRECTORY is not configured or the job cannot be
queued.
diff --git a/src/Actions/DeleteWinlibsBuild.php b/src/Actions/DeleteWinlibsBuild.php
index 81b62e3..1cb0e6a 100644
--- a/src/Actions/DeleteWinlibsBuild.php
+++ b/src/Actions/DeleteWinlibsBuild.php
@@ -8,62 +8,21 @@
class DeleteWinlibsBuild
{
public function __construct(
- private readonly string $baseDirectory,
- private readonly string $buildsDirectory
+ private readonly string $baseDirectory
) {
}
- public function handle(): void
+ public function handle(string $type, string $filename): void
{
- $queueDirectory = rtrim($this->buildsDirectory, '/') .
'/winlibs-delete';
- if (!is_dir($queueDirectory)) {
- return;
- }
-
- $tasks = glob($queueDirectory . '/winlibs-delete-*.json');
- if ($tasks === false) {
- throw new Exception("Unable to list Winlibs deletion queue:
$queueDirectory");
+ if (!in_array($type, ['php', 'pecl'], true)
+ || preg_match('/\A[A-Za-z0-9][A-Za-z0-9._-]*\.zip\z/', $filename) !== 1) {
+ throw new Exception('Invalid Winlibs deletion job');
}
- foreach ($tasks as $taskFile) {
- $lockPath = $taskFile . '.lock';
- $lock = fopen($lockPath, 'c');
- if ($lock === false) {
- throw new Exception("Unable to open lock file: $lockPath");
- }
-
- try {
- if (!flock($lock, LOCK_EX | LOCK_NB)) {
- continue;
- }
- if (!is_file($taskFile)) {
- continue;
- }
- $data = json_decode((string) file_get_contents($taskFile), true, 512,
JSON_THROW_ON_ERROR);
- $type = $data['type'] ?? null;
- $filename = $data['filename'] ?? null;
- if (!in_array($type, ['php', 'pecl'], true)
- || !is_string($filename)
- || preg_match('/\A[A-Za-z0-9][A-Za-z0-9._-]*\.zip\z/', $filename) !==
1) {
- throw new Exception("Invalid Winlibs deletion task: $taskFile");
- }
-
- if ($type === 'php') {
- $this->deletePhpBuild(rtrim($this->baseDirectory, '/'),
$filename);
- } else {
- $this->deletePeclBuild(rtrim($this->baseDirectory, '/'),
$filename);
- }
-
- if (!unlink($taskFile)) {
- throw new Exception("Unable to remove task: $taskFile");
- }
- } finally {
- flock($lock, LOCK_UN);
- fclose($lock);
- if (!is_file($taskFile)) {
- @unlink($lockPath);
- }
- }
+ if ($type === 'php') {
+ $this->deletePhpBuild(rtrim($this->baseDirectory, '/'), $filename);
+ } else {
+ $this->deletePeclBuild(rtrim($this->baseDirectory, '/'), $filename);
}
}
diff --git a/src/Console/Command/WinlibsCommand.php b/src/Console/Command/WinlibsCommand.php
index bfe6b17..0d9b2c9 100644
--- a/src/Console/Command/WinlibsCommand.php
+++ b/src/Console/Command/WinlibsCommand.php
@@ -8,6 +8,7 @@
use App\Helpers\Helpers;
use Exception;
use JsonException;
+use Throwable;
class WinlibsCommand extends Command
{
@@ -29,60 +30,103 @@ public function handle(): int
throw new Exception('Build directory is required');
}
- $deletions = new DeleteWinlibsBuild($this->baseDirectory, $buildsDirectory);
- if (($this->options['delete'] ?? false) === true) {
- $deletions->handle();
- return Command::SUCCESS;
- }
-
$buildDirectories = glob($buildsDirectory . '/winlibs/*', GLOB_ONLYDIR);
+ if ($buildDirectories === false) {
+ throw new Exception('Unable to list Winlibs jobs');
+ }
- // We lock the Directories we are working on
- // so that we don't process them again if the command is run again
- $filteredDirectories = [];
+ $uploadDirectories = [];
+ $deleteDirectories = [];
foreach ($buildDirectories as $directoryPath) {
- $lockFile = $directoryPath . '.lock';
- if (!file_exists($lockFile)) {
- touch($lockFile);
- $filteredDirectories[] = $directoryPath;
+ if (str_starts_with(basename($directoryPath), 'delete-')) {
+ $deleteDirectories[] = $directoryPath;
+ } else {
+ $uploadDirectories[] = $directoryPath;
}
}
+ $deleteOnly = ($this->options['delete'] ?? false) === true;
+ $jobs = $deleteOnly ? $deleteDirectories : array_merge($uploadDirectories,
$deleteDirectories);
+ $deletions = new DeleteWinlibsBuild($this->baseDirectory);
+ $errors = [];
- foreach ($filteredDirectories as $directoryPath) {
- $data = json_decode(file_get_contents($directoryPath . '/data.json'),
true, 512, JSON_THROW_ON_ERROR);
- $files = glob($directoryPath . '/*.zip');
- $files = $this->parseFiles($files);
- if (empty($files)) {
- throw new Exception('No valid files found in ' .
basename($directoryPath));
- }
- if($data['type'] === 'php') {
- $this->copyPhpFiles($files, $data['library'],
$data['vs_version_targets']);
- $updateSeries = $data['update_series'] ?? 'true';
- if ($updateSeries === 'true') {
- $this->updatePhpSeriesFiles(
- $files,
- $data['library'],
- $data['php_versions'],
- $data['vs_version_targets'],
- $data['stability']
- );
+ foreach ($jobs as $directoryPath) {
+ $lockFile = $directoryPath . '.lock';
+ $isDeleteDirectory = str_starts_with(basename($directoryPath),
'delete-');
+ $deleteLock = null;
+ if ($isDeleteDirectory) {
+ $deleteLock = fopen($lockFile, 'c');
+ if ($deleteLock === false) {
+ throw new Exception('Unable to lock Winlibs job: ' .
basename($directoryPath));
+ }
+ if (!flock($deleteLock, LOCK_EX | LOCK_NB)) {
+ fclose($deleteLock);
+ continue;
}
} else {
- $this->copyPeclFiles($files, $data['library']);
- $this->updatePackagesFile();
+ if (file_exists($lockFile)) {
+ continue;
+ }
+ if (!touch($lockFile)) {
+ throw new Exception('Unable to lock Winlibs job: ' .
basename($directoryPath));
+ }
}
- Helpers::rmdirr($directoryPath);
+ try {
+ $data = json_decode((string) file_get_contents($directoryPath .
'/data.json'), true, 512, JSON_THROW_ON_ERROR);
+ $isDeleteJob = ($data['delete'] ?? false) === true;
+ if ($isDeleteJob) {
+ if (!str_starts_with(basename($directoryPath), 'delete-')
+ || !is_string($data['type'] ?? null)
+ || !is_string($data['filename'] ?? null)) {
+ throw new Exception('Invalid Winlibs deletion job: ' .
basename($directoryPath));
+ }
+ $deletions->handle($data['type'],
$data['filename']);
+ } else {
+ if (str_starts_with(basename($directoryPath), 'delete-')) {
+ throw new Exception('Invalid Winlibs deletion job: ' .
basename($directoryPath));
+ }
+ $files = glob($directoryPath . '/*.zip');
+ $files = $this->parseFiles($files);
+ if (empty($files)) {
+ throw new Exception('No valid files found in ' .
basename($directoryPath));
+ }
+ if ($data['type'] === 'php') {
+ $this->copyPhpFiles($files, $data['library'],
$data['vs_version_targets']);
+ $updateSeries = $data['update_series'] ?? 'true';
+ if ($updateSeries === 'true') {
+ $this->updatePhpSeriesFiles(
+ $files,
+ $data['library'],
+ $data['php_versions'],
+ $data['vs_version_targets'],
+ $data['stability']
+ );
+ }
+ } else {
+ $this->copyPeclFiles($files, $data['library']);
+ $this->updatePackagesFile();
+ }
+ }
- unlink($directoryPath . '.lock');
+ if (!Helpers::rmdirr($directoryPath)) {
+ throw new Exception('Unable to remove Winlibs job: ' .
basename($directoryPath));
+ }
+ unlink($lockFile);
+ } catch (Throwable $error) {
+ $errors[] = $error->getMessage();
+ } finally {
+ if ($deleteLock !== null) {
+ flock($deleteLock, LOCK_UN);
+ fclose($deleteLock);
+ }
+ }
}
- // Process deletions after uploads so a queued upload cannot restore
- // a build that was also queued for deletion.
- $deletions->handle();
-
+ if ($errors !== []) {
+ throw new Exception(implode("\n", $errors));
+ }
return Command::SUCCESS;
- } catch (Exception $e) {
+ } catch (Throwable $e) {
echo $e->getMessage();
return Command::FAILURE;
}
diff --git a/src/Http/Controllers/WinlibsDeleteController.php
b/src/Http/Controllers/WinlibsDeleteController.php
index c72d84a..e41533e 100644
--- a/src/Http/Controllers/WinlibsDeleteController.php
+++ b/src/Http/Controllers/WinlibsDeleteController.php
@@ -3,8 +3,10 @@
namespace App\Http\Controllers;
+use App\Helpers\Helpers;
use App\Http\BaseController;
use App\Validator;
+use Throwable;
class WinlibsDeleteController extends BaseController
{
@@ -34,28 +36,79 @@ protected function execute(array $data): void
return;
}
- $queueDirectory = $buildsDirectory . '/winlibs-delete';
- if (!is_dir($queueDirectory) && !mkdir($queueDirectory, 0755, true) &&
!is_dir($queueDirectory)) {
+ $queueDirectory = $buildsDirectory . '/winlibs';
+ if (!is_dir($queueDirectory) || !is_writable($queueDirectory)) {
http_response_code(500);
- echo 'Unable to create Winlibs deletion queue.';
+ echo 'Winlibs build queue is not writable.';
return;
}
- $taskFile = @tempnam($queueDirectory, 'winlibs-delete-');
- $payload = json_encode([
- 'type' => $data['type'],
- 'filename' => $data['filename'],
- ], JSON_THROW_ON_ERROR);
- if ($taskFile === false || realpath(dirname($taskFile)) !== realpath($queueDirectory)
- || file_put_contents($taskFile, $payload, LOCK_EX) === false
- || !chmod($taskFile, 0644)
- || !rename($taskFile, $taskFile . '.json')) {
- if ($taskFile !== false) {
- unlink($taskFile);
- }
+ try {
+ $this->migrateLegacyJobs($buildsDirectory, $queueDirectory);
+ $this->queueJob($queueDirectory, $data['type'],
$data['filename']);
+ } catch (Throwable $error) {
http_response_code(500);
- echo 'Unable to queue Winlibs deletion.';
+ echo 'Unable to queue Winlibs deletion: ' . $error->getMessage();
+ }
+ }
+
+ private function migrateLegacyJobs(string $buildsDirectory, string $queueDirectory): void
+ {
+ $legacyDirectory = $buildsDirectory . '/winlibs-delete';
+ if (!is_dir($legacyDirectory)) {
return;
}
+
+ $legacyJobs = glob($legacyDirectory . '/winlibs-delete-*.json');
+ if ($legacyJobs === false) {
+ throw new \RuntimeException('Unable to list previous deletion jobs.');
+ }
+ foreach ($legacyJobs as $job) {
+ $payload = json_decode((string) file_get_contents($job), true, 512,
JSON_THROW_ON_ERROR);
+ $type = is_string($payload['type'] ?? null) ?
rtrim($payload['type'], "\r\n") : null;
+ $filename = is_string($payload['filename'] ?? null) ?
rtrim($payload['filename'], "\r\n") : null;
+ if (!in_array($type, ['php', 'pecl'], true) ||
!is_string($filename)
+ || preg_match('/\A[A-Za-z0-9][A-Za-z0-9._-]*\.zip\z/', $filename) !== 1)
{
+ throw new \RuntimeException('Invalid previous deletion job: ' .
basename($job));
+ }
+
+ $this->queueJob($queueDirectory, $type, $filename);
+ if (!unlink($job)) {
+ throw new \RuntimeException('Unable to remove previous deletion job: ' .
basename($job));
+ }
+ }
+ }
+
+ private function queueJob(string $queueDirectory, string $type, string $filename): void
+ {
+ $id = bin2hex(random_bytes(12));
+ $stagingDirectory = $queueDirectory . '/.delete-' . $id;
+ $jobDirectory = $queueDirectory . '/delete-' . $id;
+ $previousUmask = umask(0);
+ try {
+ if (!@mkdir($stagingDirectory, 0777)) {
+ throw new \RuntimeException('Unable to create deletion job directory.');
+ }
+ } finally {
+ umask($previousUmask);
+ }
+
+ try {
+ $payload = json_encode([
+ 'delete' => true,
+ 'type' => $type,
+ 'filename' => $filename,
+ ], JSON_THROW_ON_ERROR);
+ $dataFile = $stagingDirectory . '/data.json';
+ if (file_put_contents($dataFile, $payload, LOCK_EX) === false
+ || !chmod($dataFile, 0644)
+ || !rename($stagingDirectory, $jobDirectory)) {
+ throw new \RuntimeException('Unable to publish deletion job.');
+ }
+ } finally {
+ if (is_dir($stagingDirectory)) {
+ Helpers::rmdirr($stagingDirectory);
+ }
+ }
}
}
diff --git a/tests/Console/Command/WinlibsCommandDeleteTest.php
b/tests/Console/Command/WinlibsCommandDeleteTest.php
index 0fcdb6a..26fbf07 100644
--- a/tests/Console/Command/WinlibsCommandDeleteTest.php
+++ b/tests/Console/Command/WinlibsCommandDeleteTest.php
@@ -145,7 +145,7 @@ public function testInvalidTaskFailsAndRemainsQueued(string $type, string $filen
$result = $this->runCommand();
$output = (string) ob_get_clean();
$this->assertSame(1, $result);
- $this->assertStringContainsString('Invalid Winlibs deletion task', $output);
+ $this->assertStringContainsString('Invalid Winlibs deletion job', $output);
$this->assertFileExists($task);
}
@@ -218,7 +218,7 @@ public function testLockedJobIsProcessedOnNextRun(): void
$target = 'libfoo-1.0-vs18-x64.zip';
file_put_contents($directory . '/' . $target, 'zip');
$task = $this->queue('pecl', $target);
- $lock = fopen($task . '.lock', 'c');
+ $lock = fopen(dirname($task) . '.lock', 'c');
flock($lock, LOCK_EX);
try {
$this->assertSame(0, $this->runCommand());
@@ -294,6 +294,32 @@ public function testDefaultRunnerDeletesBuildUploadedInSameRun(): void
$this->assertSame([], $this->queuedTasks());
}
+ public function testFailedUploadDoesNotBlockDeletionJobs(): void
+ {
+ $directory = $this->baseDirectory . '/pecl/deps';
+ mkdir($directory, 0755, true);
+ $target = 'libfoo-1.0-vs18-x64.zip';
+ file_put_contents($directory . '/' . $target, 'zip');
+ file_put_contents($directory . '/packages.txt', $target);
+ $this->queue('pecl', $target);
+
+ $failedUpload = $this->buildsDirectory . '/winlibs/invalid';
+ mkdir($failedUpload, 0755, true);
+ file_put_contents($failedUpload . '/data.json',
'{"type":"pecl","library":"libfoo"}');
+
+ $command = new WinlibsCommand();
+ $command->options = ['base-directory' => $this->baseDirectory,
'builds-directory' => $this->buildsDirectory];
+ ob_start();
+ $result = $command->handle();
+ $output = (string) ob_get_clean();
+
+ $this->assertSame(1, $result);
+ $this->assertStringContainsString('No valid files found in invalid', $output);
+ $this->assertFileDoesNotExist($directory . '/' . $target);
+ $this->assertSame('', file_get_contents($directory .
'/packages.txt'));
+ $this->assertSame([], $this->queuedTasks());
+ }
+
public function testRequiresBothDirectories(): void
{
$command = new WinlibsCommand();
@@ -310,18 +336,24 @@ public function testRequiresBothDirectories(): void
private function queue(string $type, string $filename): string
{
- $queue = $this->buildsDirectory . '/winlibs-delete';
+ $queue = $this->buildsDirectory . '/winlibs';
if (!is_dir($queue)) {
mkdir($queue, 0755, true);
}
- $path = $queue . '/winlibs-delete-' . uniqid() . '.json';
- file_put_contents($path, json_encode(['type' => $type, 'filename'
=> $filename], JSON_THROW_ON_ERROR));
+ $jobDirectory = $queue . '/delete-' . uniqid();
+ mkdir($jobDirectory, 0755, true);
+ $path = $jobDirectory . '/data.json';
+ file_put_contents($path, json_encode([
+ 'delete' => true,
+ 'type' => $type,
+ 'filename' => $filename,
+ ], JSON_THROW_ON_ERROR));
return $path;
}
private function queuedTasks(): array
{
- return glob($this->buildsDirectory . '/winlibs-delete/*.json') ?: [];
+ return glob($this->buildsDirectory . '/winlibs/delete-*/data.json') ?: [];
}
private function runCommand(): int
diff --git a/tests/Http/Controllers/WinlibsDeleteControllerTest.php
b/tests/Http/Controllers/WinlibsDeleteControllerTest.php
index 011960a..fca7876 100644
--- a/tests/Http/Controllers/WinlibsDeleteControllerTest.php
+++ b/tests/Http/Controllers/WinlibsDeleteControllerTest.php
@@ -19,6 +19,7 @@ protected function setUp(): void
parent::setUp();
$this->buildsDirectory = sys_get_temp_dir() . '/winlibs_delete_controller_' .
uniqid();
mkdir($this->buildsDirectory, 0755, true);
+ mkdir($this->buildsDirectory . '/winlibs', 0777);
$this->originalBuildsDirectory = getenv('BUILDS_DIRECTORY');
putenv('BUILDS_DIRECTORY=' . $this->buildsDirectory);
http_response_code(200);
@@ -39,13 +40,16 @@ public function testQueuesExactBuildForBothTypes(): void
$this->request(['type' => 'php', 'filename' =>
'libcurl-8.22.0-1-vs18-x64.zip']);
$this->request(['type' => 'pecl', 'filename' =>
'OpenBLAS-0.3.34-vs18-x86.zip']);
- $tasks = glob($this->buildsDirectory .
'/winlibs-delete/winlibs-delete-*.json');
+ $tasks = $this->queuedTasks();
$this->assertCount(2, $tasks);
$payloads = array_map(static fn (string $path): array => json_decode(
(string) file_get_contents($path), true, 512, JSON_THROW_ON_ERROR
), $tasks);
- $this->assertContains(['type' => 'php', 'filename'
=> 'libcurl-8.22.0-1-vs18-x64.zip'], $payloads);
- $this->assertContains(['type' => 'pecl', 'filename'
=> 'OpenBLAS-0.3.34-vs18-x86.zip'], $payloads);
+ $this->assertContains(['delete' => true, 'type' =>
'php', 'filename' => 'libcurl-8.22.0-1-vs18-x64.zip'], $payloads);
+ $this->assertContains(['delete' => true, 'type' =>
'pecl', 'filename' => 'OpenBLAS-0.3.34-vs18-x86.zip'], $payloads);
+ foreach ($tasks as $task) {
+ $this->assertSame(0777, fileperms(dirname($task)) & 0777);
+ }
$this->assertSame(200, http_response_code());
}
@@ -55,7 +59,7 @@ public function testRejectsInvalidPayload(array $payload): void
$output = $this->request($payload);
$this->assertSame(400, http_response_code());
$this->assertStringContainsString('Invalid request:', $output);
- $this->assertSame([], glob($this->buildsDirectory .
'/winlibs-delete/*.json') ?: []);
+ $this->assertSame([], $this->queuedTasks());
}
public static function invalidPayloads(): array
@@ -83,6 +87,15 @@ public function testRejectsMissingBuildsDirectoryConfiguration(): void
$this->assertStringContainsString('BUILDS_DIRECTORY is not set', $output);
}
+ public function testRequiresExistingWinlibsQueue(): void
+ {
+ rmdir($this->buildsDirectory . '/winlibs');
+ $output = $this->request(['type' => 'php', 'filename'
=> 'zlib-1.3.2-vs18-x64.zip']);
+ $this->assertSame(500, http_response_code());
+ $this->assertStringContainsString('Winlibs build queue is not writable',
$output);
+ $this->assertSame([], $this->queuedTasks());
+ }
+
public function testQueuedRequestIsAppliedByExistingWinlibsRunner(): void
{
$baseDirectory = sys_get_temp_dir() . '/winlibs_delete_api_base_' . uniqid();
@@ -105,12 +118,56 @@ public function testQueuedRequestIsAppliedByExistingWinlibsRunner(): void
$this->assertSame(0, $command->handle());
$this->assertSame($older, file_get_contents($index));
$this->assertFileDoesNotExist($depsDirectory . '/vs18/x64/' . $target);
- $this->assertSame([], glob($this->buildsDirectory .
'/winlibs-delete/*.json') ?: []);
+ $this->assertSame([], $this->queuedTasks());
} finally {
Helpers::rmdirr($baseDirectory);
}
}
+ public function testMovesPreviousJobsIntoTheWinlibsQueue(): void
+ {
+ $legacyDirectory = $this->buildsDirectory . '/winlibs-delete';
+ mkdir($legacyDirectory, 0755, true);
+ $previous = [
+ ['type' => 'php', 'filename' =>
'libcurl-8.22.0-1-vs18-x64.zip'],
+ ['type' => 'pecl', 'filename' =>
'OpenBLAS-0.3.34-vs18-x86.zip'],
+ ['type' => "php\n", 'filename' =>
'zlib-1.3.1-vs18-x64.zip'],
+ ['type' => 'pecl', 'filename' =>
"libfoo-1.0-vs17-x86.zip\n"],
+ ];
+ foreach ($previous as $index => $payload) {
+ file_put_contents($legacyDirectory . '/winlibs-delete-' . $index .
'.json', json_encode($payload));
+ }
+
+ $this->request(['type' => 'php', 'filename' =>
'zlib-1.3.2-vs18-x64.zip']);
+
+ $this->assertSame(200, http_response_code());
+ $this->assertSame([], glob($legacyDirectory . '/*.json') ?: []);
+ $this->assertCount(5, $this->queuedTasks());
+ foreach ($this->queuedTasks() as $task) {
+ $payload = json_decode((string) file_get_contents($task), true, 512,
JSON_THROW_ON_ERROR);
+ $this->assertSame(1, preg_match('/\A[A-Za-z0-9][A-Za-z0-9._-]*\.zip\z/',
$payload['filename']));
+ }
+
+ $baseDirectory = sys_get_temp_dir() . '/winlibs_delete_migration_base_' .
uniqid();
+ mkdir($baseDirectory, 0755, true);
+ try {
+ $command = new WinlibsCommand();
+ $command->options = [
+ 'base-directory' => $baseDirectory,
+ 'builds-directory' => $this->buildsDirectory,
+ ];
+ $this->assertSame(0, $command->handle());
+ $this->assertSame([], $this->queuedTasks());
+ } finally {
+ Helpers::rmdirr($baseDirectory);
+ }
+ }
+
+ private function queuedTasks(): array
+ {
+ return glob($this->buildsDirectory . '/winlibs/delete-*/data.json') ?: [];
+ }
+
private function request(array $payload): string
{
$input = tempnam(sys_get_temp_dir(), 'winlibs-delete-input-');