diff --git a/docs/usage.md b/docs/usage.md
index b4ef8973..0af7c039 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -110,10 +110,7 @@ The PIE is built using [Static PHP](https://static-php.dev/), which builds a
self-contained PHP executable with the extensions that PIE needs to run, and
bundles the PHAR as a single distributable executable. Please keep in mind that
this is **experimental**, and we do not recommend this for production use for
-the time being. Please also note there are some limitations:
-
- - [php/pie#460](https://github.com/php/pie/discussions/460) - all the binary
- versions have the `pie self-update` feature disabled for now.
+the time being.
If you find the binary releases useful, please leave feedback or upvote on the
relevant discussions, so we can gauge interest in improving this functionality.
@@ -128,6 +125,7 @@ stable releases can be downloaded from these links:
| Linux | amd64 / x86_64 | https://github.com/php/pie/releases/latest/download/pie-Linux-X64 |
| OS X | ARM 64 / aarch64 | https://github.com/php/pie/releases/latest/download/pie-macOS-ARM64 |
| Linux | ARM 64 / aarch64 | https://github.com/php/pie/releases/latest/download/pie-Linux-ARM64 |
+| Windows | amd64 / x86_64 | https://github.com/php/pie/releases/latest/download/pie-Windows-X64.exe |
The "nightly" versions of these can be found here:
@@ -136,6 +134,7 @@ The "nightly" versions of these can be found here:
| Linux | amd64 / x86_64 | https://php.github.io/pie/pie-Linux-X64 |
| OS X | ARM 64 / aarch64 | https://php.github.io/pie/pie-macOS-ARM64 |
| Linux | ARM 64 / aarch64 | https://php.github.io/pie/pie-Linux-ARM64 |
+| Windows | amd64 / x86_64 | https://php.github.io/pie/pie-Windows-X64.exe |
We *highly* recommend you verify the file came from the PHP GitHub repository
before running it, for example:
diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php
index 92720bfa..dc6944ff 100644
--- a/src/Command/CommandHelper.php
+++ b/src/Command/CommandHelper.php
@@ -122,6 +122,28 @@ public static function configurePhpConfigOptions(Command $command): void
);
}
+ public static function writeRunningPhpVersion(IOInterface $io): void
+ {
+ if (PiePlatform::isRunningStaticPhp()) {
+ $io->write(sprintf('You are running a PIE Static PHP %s build', PHP_VERSION));
+
+ return;
+ }
+
+ $io->write(sprintf('You are running PHP %s', PHP_VERSION));
+ }
+
+ public static function warnPhpConfigOptionsHaveNoEffect(InputInterface $input, IOInterface $io): void
+ {
+ foreach ([self::OPTION_WITH_PHP_CONFIG, self::OPTION_WITH_PHP_PATH, self::OPTION_WITH_PHPIZE_PATH] as $option) {
+ if (! $input->hasOption($option) || $input->getOption($option) === null) {
+ continue;
+ }
+
+ $io->writeError(sprintf('The --%s option is now deprecated and has no effect for this command.', $option));
+ }
+ }
+
public static function configureBuildToolsCheckOptions(Command $command): void
{
$command->addOption(
@@ -281,11 +303,7 @@ public static function determineTargetPlatformFromInputs(InputInterface $input,
$targetPlatform = TargetPlatform::fromPhpBinaryPath($phpBinaryPath, $makeParallelJobs, $phpizePath);
- if (PiePlatform::isRunningStaticPhp()) {
- $io->write(sprintf('You are running a PIE Static PHP %s build', PHP_VERSION));
- } else {
- $io->write(sprintf('You are running PHP %s', PHP_VERSION));
- }
+ self::writeRunningPhpVersion($io);
$io->write(sprintf(
'Target PHP installation: %s %s%s, on %s %s (from %s)',
diff --git a/src/Command/SelfUpdateCommand.php b/src/Command/SelfUpdateCommand.php
index d4498b39..060e6a3e 100644
--- a/src/Command/SelfUpdateCommand.php
+++ b/src/Command/SelfUpdateCommand.php
@@ -4,25 +4,23 @@
namespace Php\Pie\Command;
+use Composer\Factory;
use Composer\IO\IOInterface;
-use Composer\IO\NullIO;
-use Php\Pie\ComposerIntegration\PieComposerFactory;
-use Php\Pie\ComposerIntegration\PieComposerRequest;
use Php\Pie\ComposerIntegration\QuieterConsoleIO;
use Php\Pie\File\FullPathToSelf;
+use Php\Pie\File\ReplaceRunningExecutable;
use Php\Pie\File\SudoFilePut;
use Php\Pie\Platform;
use Php\Pie\SelfManage\Update\Channel;
use Php\Pie\SelfManage\Update\FetchPieReleaseFromGitHub;
use Php\Pie\SelfManage\Update\IsBrewInstallation;
+use Php\Pie\SelfManage\Update\PieAssetName;
use Php\Pie\SelfManage\Update\ReleaseIsNewer;
-use Php\Pie\SelfManage\Update\ReleaseMetadata;
use Php\Pie\SelfManage\Verify\RecoverFromFailedVerification;
use Php\Pie\SelfManage\Verify\VerifyPieReleaseUsingAttestation;
use Php\Pie\Settings;
use Php\Pie\Util\Emoji;
use Php\Pie\Util\PieVersion;
-use Psr\Container\ContainerInterface;
use Safe\Exceptions\FilesystemException;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
@@ -53,7 +51,6 @@ public function __construct(
private readonly string $githubApiBaseUrl,
private readonly IOInterface $io,
private readonly QuieterConsoleIO $quieterConsoleIo,
- private readonly ContainerInterface $container,
private readonly FullPathToSelf $fullPathToSelf,
) {
parent::__construct();
@@ -86,7 +83,9 @@ public function configure(): void
public function execute(InputInterface $input, OutputInterface $output): int
{
- if (! PieVersion::isPharBuild() || Platform::isRunningStaticPhp()) {
+ CommandHelper::warnPhpConfigOptionsHaveNoEffect($input, $this->io);
+
+ if (! PieVersion::isPharBuild()) {
$this->io->writeError('Aborting! You are not running a PHAR, cannot self-update.');
return Command::FAILURE;
@@ -116,36 +115,28 @@ public function execute(InputInterface $input, OutputInterface $output): int
}
$this->io->write(sprintf('Updating using the %s> channel.', $updateChannel->value));
-
- $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $this->io);
+ CommandHelper::writeRunningPhpVersion($this->io);
CommandHelper::applyNoCacheOptionIfSet($input, $this->io);
- $composer = PieComposerFactory::createPieComposer(
- $this->container,
- PieComposerRequest::noOperation(
- new NullIO(),
- $targetPlatform,
- ),
- );
+ $composerConfig = Factory::createConfig($this->quieterConsoleIo);
+ $this->quieterConsoleIo->loadConfiguration($composerConfig);
$fetchLatestPieRelease = FetchPieReleaseFromGitHub::factory(
$this->quieterConsoleIo,
- $composer->getConfig(),
+ $composerConfig,
$this->githubApiBaseUrl,
+ PieAssetName::forRunningPie(),
);
$verifyPiePhar = VerifyPieReleaseUsingAttestation::factory(
$fetchLatestPieRelease,
$this->quieterConsoleIo,
- $composer->getConfig(),
+ $composerConfig,
$this->githubApiBaseUrl,
);
if ($updateChannel === Channel::Nightly) {
- $latestRelease = new ReleaseMetadata(
- 'nightly',
- 'https://php.github.io/pie/pie-nightly.phar',
- );
+ $latestRelease = $fetchLatestPieRelease->nightlyReleaseMetadata();
$this->io->write('Downloading the latest nightly release.');
} else {
@@ -201,7 +192,7 @@ public function execute(InputInterface $input, OutputInterface $output): int
try {
$pharContents = file_get_contents($pharFilename->filePath);
} catch (FilesystemException) {
- $this->io->writeError(sprintf('%s Failed to read the downloaded PHAR file %s', Emoji::CROSS, $pharFilename->filePath));
+ $this->io->writeError(sprintf('%s Failed to read the downloaded PIE release file %s', Emoji::CROSS, $pharFilename->filePath));
unlink($pharFilename->filePath);
return Command::FAILURE;
@@ -210,7 +201,7 @@ public function execute(InputInterface $input, OutputInterface $output): int
try {
$pharFilename->verifyContent($pharContents);
} catch (Throwable) {
- $this->io->writeError(sprintf('%s PHAR contents changed after verification; aborting self-update', Emoji::CROSS));
+ $this->io->writeError(sprintf('%s Downloaded PIE release contents changed after verification; aborting self-update', Emoji::CROSS));
unlink($pharFilename->filePath);
return Command::FAILURE;
@@ -220,7 +211,12 @@ public function execute(InputInterface $input, OutputInterface $output): int
sprintf('Writing new version to %s', $fullPathToSelf),
verbosity: IOInterface::VERBOSE,
);
- SudoFilePut::contents($fullPathToSelf, $pharContents);
+ if (Platform::isRunningStaticPhp()) {
+ ReplaceRunningExecutable::contents($fullPathToSelf, $pharContents);
+ } else {
+ SudoFilePut::contents($fullPathToSelf, $pharContents);
+ }
+
unlink($pharFilename->filePath);
$this->io->write(sprintf(
diff --git a/src/Command/SelfVerifyCommand.php b/src/Command/SelfVerifyCommand.php
index ced96742..f5285149 100644
--- a/src/Command/SelfVerifyCommand.php
+++ b/src/Command/SelfVerifyCommand.php
@@ -4,19 +4,17 @@
namespace Php\Pie\Command;
+use Composer\Factory;
use Composer\IO\IOInterface;
-use Composer\IO\NullIO;
-use Php\Pie\ComposerIntegration\PieComposerFactory;
-use Php\Pie\ComposerIntegration\PieComposerRequest;
use Php\Pie\ComposerIntegration\QuieterConsoleIO;
use Php\Pie\File\BinaryFile;
use Php\Pie\File\FullPathToSelf;
use Php\Pie\SelfManage\Update\FetchPieReleaseFromGitHub;
+use Php\Pie\SelfManage\Update\PieAssetName;
use Php\Pie\SelfManage\Update\ReleaseMetadata;
use Php\Pie\SelfManage\Verify\VerifyPieReleaseUsingAttestation;
use Php\Pie\Util\Emoji;
use Php\Pie\Util\PieVersion;
-use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
@@ -41,7 +39,6 @@ public function __construct(
private readonly FullPathToSelf $fullPathToSelf,
private readonly IOInterface $io,
private readonly QuieterConsoleIO $quieterConsoleIo,
- private readonly ContainerInterface $container,
) {
parent::__construct();
}
@@ -60,6 +57,8 @@ public function configure(): void
public function execute(InputInterface $input, OutputInterface $output): int
{
+ CommandHelper::warnPhpConfigOptionsHaveNoEffect($input, $this->io);
+
if (! PieVersion::isPharBuild()) {
$this->io->writeError('Aborting! You are not running a PHAR, cannot self-verify.');
@@ -73,22 +72,18 @@ public function execute(InputInterface $input, OutputInterface $output): int
$this->io->write(sprintf('No version specified, verifying against the version this PHAR claims to be (%s).', $expectedVersion));
}
- $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $this->io);
+ CommandHelper::writeRunningPhpVersion($this->io);
CommandHelper::applyNoCacheOptionIfSet($input, $this->io);
- $composer = PieComposerFactory::createPieComposer(
- $this->container,
- PieComposerRequest::noOperation(
- new NullIO(),
- $targetPlatform,
- ),
- );
+ $composerConfig = Factory::createConfig($this->quieterConsoleIo);
+ $this->quieterConsoleIo->loadConfiguration($composerConfig);
$fetchLatestPieRelease = FetchPieReleaseFromGitHub::factory(
$this->quieterConsoleIo,
- $composer->getConfig(),
+ $composerConfig,
$this->githubApiBaseUrl,
+ PieAssetName::forRunningPie(),
);
$latestRelease = new ReleaseMetadata($expectedVersion, 'blah');
@@ -96,7 +91,7 @@ public function execute(InputInterface $input, OutputInterface $output): int
$verifyPiePhar = VerifyPieReleaseUsingAttestation::factory(
$fetchLatestPieRelease,
$this->quieterConsoleIo,
- $composer->getConfig(),
+ $composerConfig,
$this->githubApiBaseUrl,
);
diff --git a/src/File/ReplaceRunningExecutable.php b/src/File/ReplaceRunningExecutable.php
new file mode 100644
index 00000000..3faa6fd2
--- /dev/null
+++ b/src/File/ReplaceRunningExecutable.php
@@ -0,0 +1,142 @@
+ $asset['name'] === $this->assetName->value,
);
- if (! count($assetsNamedPiePhar)) {
+ if (! count($assetsWithExpectedName)) {
return null;
}
- $firstAssetNamedPiePhar = reset($assetsNamedPiePhar);
+ $firstAssetWithExpectedName = reset($assetsWithExpectedName);
return new ReleaseMetadata(
$releaseResponse['tag_name'],
- $firstAssetNamedPiePhar['browser_download_url'],
+ $firstAssetWithExpectedName['browser_download_url'],
);
},
array_filter(
@@ -155,6 +154,14 @@ static function (ReleaseMetadata|null $releaseMetadata) use ($updateChannel): bo
return $first;
}
+ public function nightlyReleaseMetadata(): ReleaseMetadata
+ {
+ return new ReleaseMetadata(
+ 'nightly',
+ self::NIGHTLY_BASE_URL . $this->assetName->nightly()->value,
+ );
+ }
+
public function downloadContent(ReleaseMetadata $releaseMetadata): BinaryFile
{
$pharContent = $this->httpDownloader->get(
diff --git a/src/SelfManage/Update/PieAssetName.php b/src/SelfManage/Update/PieAssetName.php
new file mode 100644
index 00000000..760589b6
--- /dev/null
+++ b/src/SelfManage/Update/PieAssetName.php
@@ -0,0 +1,59 @@
+ self::PharNightly,
+ default => $this,
+ };
+ }
+
+ private static function forExecutable(string $osFamily, Architecture $architecture): self
+ {
+ return match ([$osFamily, $architecture]) {
+ ['Linux', Architecture::x86_64] => self::LinuxX64,
+ ['Linux', Architecture::arm64] => self::LinuxArm64,
+ ['Darwin', Architecture::arm64] => self::MacOsArm64,
+ ['Windows', Architecture::x86_64] => self::WindowsX64,
+ default => throw new RuntimeException(sprintf(
+ 'There is no PIE executable available for %s %s',
+ $osFamily,
+ $architecture->name,
+ )),
+ };
+ }
+}
diff --git a/src/SelfManage/Verify/RecoverFromFailedVerification.php b/src/SelfManage/Verify/RecoverFromFailedVerification.php
index 6daef44e..b444c598 100644
--- a/src/SelfManage/Verify/RecoverFromFailedVerification.php
+++ b/src/SelfManage/Verify/RecoverFromFailedVerification.php
@@ -22,12 +22,12 @@ final class RecoverFromFailedVerification
public function __invoke(IOInterface $io, ReleaseMetadata $releaseMetadata, Throwable $verificationFailure): bool
{
$io->writeError(sprintf(
- '%s Failed to verify the pie.phar release %s: %s',
+ '%s Failed to verify the PIE release %s: %s',
Emoji::CROSS,
$releaseMetadata->tag,
$verificationFailure->getMessage(),
));
- $io->writeError('This means I could not verify that the PHAR we tried to update to was authentic.');
+ $io->writeError('This means I could not verify that the PIE release we tried to update to was authentic.');
$io->writeError(sprintf(
'You can manually download release %s yourself from: %s',
$releaseMetadata->tag,
diff --git a/test/integration/File/ReplaceRunningExecutableNonWindowsTest.php b/test/integration/File/ReplaceRunningExecutableNonWindowsTest.php
new file mode 100644
index 00000000..91eae102
--- /dev/null
+++ b/test/integration/File/ReplaceRunningExecutableNonWindowsTest.php
@@ -0,0 +1,135 @@
+path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_test_replace_running_executable_', true);
+ mkdir($this->path);
+ $this->executable = $this->path . DIRECTORY_SEPARATOR . 'pie';
+ copy('/bin/sleep', $this->executable);
+ chmod($this->executable, 0755);
+
+ $this->runningExecutable = new Process([$this->executable, '30']);
+ $this->runningExecutable->start();
+ $this->waitUntilRunning();
+ }
+
+ public function tearDown(): void
+ {
+ $this->runningExecutable->stop(0);
+ chmod($this->path, 0755);
+
+ if (is_string($this->originalComposerNoInteraction)) {
+ ComposerPlatform::putEnv('COMPOSER_NO_INTERACTION', $this->originalComposerNoInteraction);
+ }
+
+ (new Filesystem())->remove($this->path);
+ }
+
+ public function testRunningExecutableCanBeReplaced(): void
+ {
+ ReplaceRunningExecutable::contents($this->executable, 'the new executable');
+
+ self::assertSame('the new executable', file_get_contents($this->executable));
+ self::assertTrue(is_executable($this->executable));
+ }
+
+ public function testRunningExecutableInUnwritableDirectoryCanBeReplaced(): void
+ {
+ $this->skipUnlessSudoIsNeededAndAvailable();
+ chmod($this->path, 0555);
+
+ ReplaceRunningExecutable::contents($this->executable, 'the new executable');
+
+ self::assertSame('the new executable', file_get_contents($this->executable));
+ self::assertTrue(is_executable($this->executable));
+ }
+
+ public function testRootOwnedRunningExecutableInUnwritableDirectoryCanBeReplaced(): void
+ {
+ $this->skipUnlessSudoIsNeededAndAvailable();
+ chmod($this->executable, 0555);
+ (new Process([Sudo::find(), 'chown', '0:0', $this->executable]))->mustRun();
+ chmod($this->path, 0555);
+
+ ReplaceRunningExecutable::contents($this->executable, 'the new executable');
+
+ self::assertSame('the new executable', file_get_contents($this->executable));
+ self::assertTrue(is_executable($this->executable));
+ self::assertSame(0, fileowner($this->executable));
+ self::assertSame(0, filegroup($this->executable));
+ }
+
+ private function skipUnlessSudoIsNeededAndAvailable(): void
+ {
+ if (TargetPlatform::isRunningAsRoot()) {
+ self::markTestSkipped('Running as root, so file permissions do not apply');
+ }
+
+ $this->originalComposerNoInteraction = ComposerPlatform::getEnv('COMPOSER_NO_INTERACTION');
+ ComposerPlatform::clearEnv('COMPOSER_NO_INTERACTION');
+
+ try {
+ Sudo::find();
+ } catch (SudoNotFoundOnSystem | SudoRequiresInteractiveTerminal) {
+ self::markTestSkipped('Cannot test without sudo in an interactive terminal');
+ }
+ }
+
+ private function waitUntilRunning(): void
+ {
+ $procExe = '/proc/' . $this->runningExecutable->getPid() . '/exe';
+ $timeout = microtime(true) + 5;
+
+ while (microtime(true) < $timeout) {
+ if (file_exists($procExe) && readlink($procExe) === $this->executable) {
+ return;
+ }
+
+ usleep(10_000);
+ }
+
+ self::fail('Executable did not start in time');
+ }
+}
diff --git a/test/integration/File/ReplaceRunningExecutableWindowsTest.php b/test/integration/File/ReplaceRunningExecutableWindowsTest.php
new file mode 100644
index 00000000..ff245809
--- /dev/null
+++ b/test/integration/File/ReplaceRunningExecutableWindowsTest.php
@@ -0,0 +1,77 @@
+path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_test_replace_running_executable_', true);
+ mkdir($this->path);
+ $this->executable = $this->path . DIRECTORY_SEPARATOR . 'pie.exe';
+ copy('C:\\Windows\\System32\\PING.EXE', $this->executable);
+
+ $this->runningExecutable = new Process([$this->executable, '-n', '30', '127.0.0.1']);
+ $this->runningExecutable->start();
+ $this->waitUntilRunning();
+ }
+
+ public function tearDown(): void
+ {
+ $this->runningExecutable->stop(0);
+ (new Filesystem())->remove($this->path);
+ }
+
+ public function testRunningExecutableCanBeReplaced(): void
+ {
+ ReplaceRunningExecutable::contents($this->executable, 'the new executable');
+
+ self::assertSame('the new executable', file_get_contents($this->executable));
+ }
+
+ private function waitUntilRunning(): void
+ {
+ $timeout = microtime(true) + 5;
+
+ while (microtime(true) < $timeout) {
+ try {
+ fclose(fopen($this->executable, 'r+'));
+ } catch (FilesystemException) {
+ // Windows locks the file of a running executable against writes
+ return;
+ }
+
+ usleep(10_000);
+ }
+
+ self::fail('Executable did not start in time');
+ }
+}
diff --git a/test/unit/SelfManage/Update/FetchPieReleaseFromGitHubTest.php b/test/unit/SelfManage/Update/FetchPieReleaseFromGitHubTest.php
index a4b8e2a2..3c26f819 100644
--- a/test/unit/SelfManage/Update/FetchPieReleaseFromGitHubTest.php
+++ b/test/unit/SelfManage/Update/FetchPieReleaseFromGitHubTest.php
@@ -6,7 +6,9 @@
use Composer\Util\Http\Response;
use Composer\Util\HttpDownloader;
+use Php\Pie\SelfManage\Update\Channel;
use Php\Pie\SelfManage\Update\FetchPieReleaseFromGitHub;
+use Php\Pie\SelfManage\Update\PieAssetName;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
@@ -26,7 +28,7 @@ final class FetchPieReleaseFromGitHubTest extends TestCase
protected function setUp(): void
{
$this->httpDownloader = $this->createMock(HttpDownloader::class);
- $this->fetcher = new FetchPieReleaseFromGitHub('https://api.github.com', $this->httpDownloader);
+ $this->fetcher = new FetchPieReleaseFromGitHub('https://api.github.com', $this->httpDownloader, PieAssetName::Phar);
}
#[DataProvider('validBranchProvider')]
@@ -88,4 +90,54 @@ public static function invalidBranchProvider(): array
'almost-main' => ['main-fix'],
];
}
+
+ public function testLatestReleaseMetadataSelectsTheExecutableAsset(): void
+ {
+ $response = $this->createMock(Response::class);
+ $response->method('decodeJson')->willReturn([
+ [
+ 'tag_name' => '1.5.1',
+ 'assets' => [
+ ['name' => 'pie.phar', 'browser_download_url' => 'https://github.com/php/pie/releases/download/1.5.1/pie.phar'],
+ ['name' => 'pie-Linux-ARM64', 'browser_download_url' => 'https://github.com/php/pie/releases/download/1.5.1/pie-Linux-ARM64'],
+ ['name' => 'pie-Linux-X64', 'browser_download_url' => 'https://github.com/php/pie/releases/download/1.5.1/pie-Linux-X64'],
+ ['name' => 'pie-macOS-ARM64', 'browser_download_url' => 'https://github.com/php/pie/releases/download/1.5.1/pie-macOS-ARM64'],
+ ['name' => 'pie-Windows-X64.exe', 'browser_download_url' => 'https://github.com/php/pie/releases/download/1.5.1/pie-Windows-X64.exe'],
+ ],
+ ],
+ ]);
+
+ $this->httpDownloader->expects(self::once())
+ ->method('get')
+ ->with('https://api.github.com/repos/php/pie/releases')
+ ->willReturn($response);
+
+ $release = (new FetchPieReleaseFromGitHub('https://api.github.com', $this->httpDownloader, PieAssetName::LinuxX64))
+ ->latestReleaseMetadata(Channel::Stable);
+
+ self::assertSame('1.5.1', $release->tag);
+ self::assertSame('https://github.com/php/pie/releases/download/1.5.1/pie-Linux-X64', $release->downloadUrl);
+ }
+
+ /** @return array */
+ public static function nightlyDownloadUrlProvider(): array
+ {
+ return [
+ 'PHAR' => [PieAssetName::Phar, 'https://php.github.io/pie/pie-nightly.phar'],
+ 'Linux x86_64' => [PieAssetName::LinuxX64, 'https://php.github.io/pie/pie-Linux-X64'],
+ 'Linux arm64' => [PieAssetName::LinuxArm64, 'https://php.github.io/pie/pie-Linux-ARM64'],
+ 'macOS arm64' => [PieAssetName::MacOsArm64, 'https://php.github.io/pie/pie-macOS-ARM64'],
+ 'Windows x86_64' => [PieAssetName::WindowsX64, 'https://php.github.io/pie/pie-Windows-X64.exe'],
+ ];
+ }
+
+ #[DataProvider('nightlyDownloadUrlProvider')]
+ public function testNightlyReleaseMetadata(PieAssetName $assetName, string $expectedDownloadUrl): void
+ {
+ $release = (new FetchPieReleaseFromGitHub('https://api.github.com', $this->httpDownloader, $assetName))
+ ->nightlyReleaseMetadata();
+
+ self::assertSame('nightly', $release->tag);
+ self::assertSame($expectedDownloadUrl, $release->downloadUrl);
+ }
}
diff --git a/test/unit/SelfManage/Update/PieAssetNameTest.php b/test/unit/SelfManage/Update/PieAssetNameTest.php
new file mode 100644
index 00000000..327506ff
--- /dev/null
+++ b/test/unit/SelfManage/Update/PieAssetNameTest.php
@@ -0,0 +1,83 @@
+ */
+ public static function supportedExecutablePlatformsProvider(): array
+ {
+ return [
+ 'Linux x86_64' => ['Linux', Architecture::x86_64, PieAssetName::LinuxX64],
+ 'Linux arm64' => ['Linux', Architecture::arm64, PieAssetName::LinuxArm64],
+ 'macOS arm64' => ['Darwin', Architecture::arm64, PieAssetName::MacOsArm64],
+ 'Windows x86_64' => ['Windows', Architecture::x86_64, PieAssetName::WindowsX64],
+ ];
+ }
+
+ #[DataProvider('supportedExecutablePlatformsProvider')]
+ public function testForExecutable(string $osFamily, Architecture $architecture, PieAssetName $expectedAssetName): void
+ {
+ self::assertSame($expectedAssetName, self::invokeForExecutable($osFamily, $architecture));
+ }
+
+ /** @return array */
+ public static function unsupportedExecutablePlatformsProvider(): array
+ {
+ return [
+ 'macOS x86_64' => ['Darwin', Architecture::x86_64],
+ 'Windows arm64' => ['Windows', Architecture::arm64],
+ 'Linux x86' => ['Linux', Architecture::x86],
+ 'BSD x86_64' => ['BSD', Architecture::x86_64],
+ ];
+ }
+
+ #[DataProvider('unsupportedExecutablePlatformsProvider')]
+ public function testForExecutableOnUnsupportedPlatform(string $osFamily, Architecture $architecture): void
+ {
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('There is no PIE executable available for ' . $osFamily . ' ' . $architecture->name);
+
+ self::invokeForExecutable($osFamily, $architecture);
+ }
+
+ /** @return array */
+ public static function nightlyProvider(): array
+ {
+ return [
+ 'PHAR' => [PieAssetName::Phar, PieAssetName::PharNightly],
+ 'PHAR nightly' => [PieAssetName::PharNightly, PieAssetName::PharNightly],
+ 'Linux x86_64' => [PieAssetName::LinuxX64, PieAssetName::LinuxX64],
+ 'Linux arm64' => [PieAssetName::LinuxArm64, PieAssetName::LinuxArm64],
+ 'macOS arm64' => [PieAssetName::MacOsArm64, PieAssetName::MacOsArm64],
+ 'Windows x86_64' => [PieAssetName::WindowsX64, PieAssetName::WindowsX64],
+ ];
+ }
+
+ #[DataProvider('nightlyProvider')]
+ public function testNightly(PieAssetName $assetName, PieAssetName $expectedNightlyAssetName): void
+ {
+ self::assertSame($expectedNightlyAssetName, $assetName->nightly());
+ }
+
+ private static function invokeForExecutable(string $osFamily, Architecture $architecture): mixed
+ {
+ return (new ReflectionMethod(PieAssetName::class, 'forExecutable'))->invoke(null, $osFamily, $architecture);
+ }
+}