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