diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6d20c7e..ef5918a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,6 +44,13 @@ jobs: if: matrix.rust == 'stable' && matrix.os == 'ubuntu-latest' run: cargo build --workspace --release --locked --all-targets + - name: Install PHP + if: matrix.rust == 'stable' && matrix.os == 'ubuntu-latest' + uses: shivammathur/setup-php@master + with: + php-version: "8.3" + tools: 'composer:v2' + - name: Install composer dependencies if: matrix.rust == 'stable' && matrix.os == 'ubuntu-latest' run: composer install diff --git a/composer.json b/composer.json index 218a7d7..7f809df 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,8 @@ "name": "carthage-software/mago", "description": "Mago is a toolchain for PHP that aims to provide a set of tools to help developers write better code.", "type": "composer-plugin", - "license": "MIT", + "license": "MIT OR Apache-2.0", + "keywords": ["dev"], "bin": [ "composer/bin/mago" ], diff --git a/composer/InstallMagoAssetsCommand.php b/composer/InstallMagoAssetsCommand.php deleted file mode 100644 index 22f37c5..0000000 --- a/composer/InstallMagoAssetsCommand.php +++ /dev/null @@ -1,154 +0,0 @@ -setName('mago:install-assets'); - $this->setDescription('Installs the mago binaries for currently configured versions and platforms.'); - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $composer = $this->requireComposer(); - $loop = $composer->getLoop(); - $downloader = $loop->getHttpDownloader(); - $io = $this->getIO(); - $config = $this->getMagoConfig($composer); - - $release = $this->detectMagoReleaseId($downloader); - ['tag' => $release, 'downloads' => $downloads] = $this->buildAssetsMapForRelease( - $downloader, - $release, - $config, - ); - - $filesystem = new Filesystem($loop->getProcessExecutor()); - $target_dir = __DIR__ . '/bin/' . $release; - $filesystem->emptyDirectory($target_dir, ensureDirectoryExists: true); - - $io->write("Downloading mago {$release} binaries:"); - $io->write(''); - - $promises = []; - foreach ($downloads as $name => $url) { - $io->write(" - {$name}"); - - $target_file = $target_dir . '/' . $name; - - $promises[] = $downloader->addCopy($url, $target_file)->then(static function (Response $response) use ( - $filesystem, - $target_dir, - $target_file, - ): Response { - $phar = new \PharData($target_file); - $phar->extractTo($target_dir); - - $filesystem->remove($target_file); - - return $response; - }); - } - - file_put_contents(__DIR__ . '/bin/version', $release); - - $io->write(''); - $progress_bar = ($io instanceof ConsoleIO) ? $io->getProgressBar() : null; - $loop->wait($promises, $progress_bar); - $io->write(''); - $io->write(''); - $io->write('Done!'); - - return self::SUCCESS; - } - - private function detectMagoReleaseId(HttpDownloader $httpDownloader): string - { - $version = InstalledVersions::getVersion(MagoPlugin::PACKAGE_NAME); - - $response = $httpDownloader->get($this->buildGithubApiUri('/releases?per_page=99999999999999999')); - $json = $response->decodeJson(); - - foreach ($json as $release) { - if ($release['tag_name'] === $version) { - return $release['id']; - } - } - - return 'latest'; - } - - /** - * @param array{platforms: list} $config - * @return array{tag: string, downloads: array} - */ - private function buildAssetsMapForRelease(HttpDownloader $httpDownloader, string $releaseId, array $config): array - { - $response = $httpDownloader->get($this->buildGithubApiUri('/releases/' . $releaseId)); - $json = $response->decodeJson(); - $platforms = $config['platforms']; - - return [ - 'tag' => $json['tag_name'], - 'downloads' => array_reduce( - $json['assets'] ?? [], - /** - * @param array $downloadMap - * @param array{browser_download_url: string, name: string} $asset - * @return array - */ - static function (array $downloadMap, array $asset) use ($platforms): array { - if (!str_ends_with($asset['name'], '.tar.gz') && !str_ends_with($asset['name'], '.zip')) { - return $downloadMap; - } - - if ($platforms && - preg_match( - '/(' . implode('|', array_map(preg_quote(...), $platforms)) . ')/', - $asset['name'], - ) === - 0) { - return $downloadMap; - } - - $downloadMap[$asset['name']] = $asset['browser_download_url']; - - return $downloadMap; - }, - [], - ), - ]; - } - - private function buildGithubApiUri(string $path): string - { - return 'https://api.github.com/repos/carthage-software/mago' . $path; - } - - /** - * @param Composer $composer - * @return array{platforms: list} - */ - private function getMagoConfig(Composer $composer): array - { - $extra = $composer->getPackage()->getExtra(); - - return [ - 'platforms' => $extra['mago']['platforms'] ?? [], - ]; - } -} diff --git a/composer/InstallMagoBinaryCommand.php b/composer/InstallMagoBinaryCommand.php new file mode 100644 index 0000000..eaf8790 --- /dev/null +++ b/composer/InstallMagoBinaryCommand.php @@ -0,0 +1,268 @@ +setName('mago:install-binary'); + $this->setDescription('Installs the mago binary for current platform.'); + $this->addOption('force', 'f', InputOption::VALUE_NONE, 'Force re-installation of the mago binary.'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $composer = $this->requireComposer(); + $loop = $composer->getLoop(); + $downloader = $loop->getHttpDownloader(); + $io = $this->getIO(); + $process_executor = $loop->getProcessExecutor(); + assert( + $process_executor instanceof ProcessExecutor, + 'Expecting a process executor, but none was found on the composer loop.', + ); + + try { + $release = $this->detectMagoReleaseId($downloader); + ['tag' => $release, 'downloads' => $downloads] = $this->buildAssetsMapForRelease($downloader, $release); + [ + 'platform' => $platform, + 'executable' => $executable, + 'storage_dir' => $storage_dir, + ] = $this->detectPlatformInfo($process_executor, $release); + } catch (\Throwable $e) { + $io->error($e->getMessage()); + return self::FAILURE; + } + + $download = $downloads[$storage_dir] ?? null; + if (!$download) { + $io->error("There is no mago {$release} download available for the current platform '{$platform}'."); + return self::FAILURE; + } + + $filesystem = new Filesystem($process_executor); + $release_dir = __DIR__ . '/bin/' . $release; + $target_dir = $release_dir . '/' . $storage_dir; + $executable_path = $target_dir . '/' . $executable; + $executable_platform_file = __DIR__ . '/bin/.platform'; + $executable_platform_content = $release . '/' . $storage_dir . '/' . $executable; + + $filesystem->ensureDirectoryExists($release_dir); + if ($input->getOption('force')) { + $filesystem->emptyDirectory($target_dir); + } + + if (file_exists($executable_path)) { + file_put_contents($executable_platform_file, $executable_platform_content); + $io->write("Mago {$release} binaries for platform '{$platform}' already exist."); + return self::SUCCESS; + } + + $io->write("Downloading mago {$release} binary:"); + $io->write(''); + + $io->write(" - {$download['file']}"); + + $downloaded_file = $release_dir . '/' . $download['file']; + $promise = $downloader->addCopy( + $download['url'], + $downloaded_file, + )->then(static function (Response $response) use ( + $filesystem, + $release_dir, + $downloaded_file, + $executable_platform_file, + $executable_platform_content, + ): Response { + $phar = new \PharData($downloaded_file); + $phar->extractTo($release_dir); + + $filesystem->remove($downloaded_file); + + file_put_contents($executable_platform_file, $executable_platform_content); + + return $response; + }); + + $io->write(''); + $progress_bar = ($io instanceof ConsoleIO) ? $io->getProgressBar() : null; + $loop->wait([$promise], $progress_bar); + $io->write(''); + $io->write(''); + $io->write('Done!'); + + return self::SUCCESS; + } + + private function detectMagoReleaseId(HttpDownloader $httpDownloader): string + { + $version = InstalledVersions::getVersion(MagoPlugin::PACKAGE_NAME); + + $response = $httpDownloader->get($this->buildGithubApiUri('/releases?per_page=99999999999999999')); + $json = $response->decodeJson(); + + foreach ($json as $release) { + if ($release['tag_name'] === $version) { + return $release['id']; + } + } + + return 'latest'; + } + + /** + * @return array{tag: string, downloads: array} + */ + private function buildAssetsMapForRelease(HttpDownloader $httpDownloader, string $releaseId): array + { + $response = $httpDownloader->get($this->buildGithubApiUri('/releases/' . $releaseId)); + $json = $response->decodeJson(); + + return [ + 'tag' => $json['tag_name'], + 'downloads' => array_reduce( + $json['assets'] ?? [], + /** + * @param array $downloadMap + * @param array{browser_download_url: string, name: string} $asset + * @return array + */ + static function (array $downloadMap, array $asset): array { + if (!str_ends_with($asset['name'], '.tar.gz') && !str_ends_with($asset['name'], '.zip')) { + return $downloadMap; + } + + $platform = preg_replace('{^(.*)(\.tar\.gz|\.zip)$}', '$1', $asset['name']); + + $downloadMap[$platform] = [ + 'file' => $asset['name'], + 'url' => $asset['browser_download_url'], + ]; + + return $downloadMap; + }, + [], + ), + ]; + } + + private function buildGithubApiUri(string $path): string + { + return 'https://api.github.com/repos/carthage-software/mago' . $path; + } + + /** + * @return array{platform: string, executable: string, storage_dir: string} + */ + private function detectPlatformInfo(ProcessExecutor $process_executor, string $version): array + { + $arch_name = php_uname('m'); + $arch = match ($arch_name) { + 'x86_64', 'amd64' => 'x86_64', + 'arm64', 'aarch64' => 'aarch64', + 'armv7l' => 'armv7', + 'i386', 'i486', 'i586', 'i686' => 'i686', + 'ppc' => 'powerpc', + 'ppc64' => 'powerpc64', + 'ppc64le' => 'powerpc64le', + 's390x' => 's390x', + default + => throw new \RuntimeException( + "Unsupported machine architecture: {$arch_name}. Please open an issue on GitHub.", + ), + }; + + $os = strtolower(php_uname('s')); + $vendor = 'unknown'; + $os_suffix = ''; + $executable_extension = ''; + + switch ($os) { + case 'windows': + $vendor = 'pc'; + $os_suffix = 'msvc'; + $executable_extension = '.exe'; + break; + case 'darwin': + $vendor = 'apple'; + break; + case 'linux': + if ($process_executor->execute('command -v ldd') === 0) { + $process_executor->execute('ldd --version 2>&1', $ldd_version); + if (strpos($ldd_version, 'musl') !== false) { + switch ($arch) { + case 'x86_64': + case 'aarch64': + case 'i686': + $os_suffix = 'musl'; + break; + case 'arm': + case 'armv7': + if (strpos(file_get_contents('/proc/cpuinfo'), 'hard') !== false) { + $os_suffix = 'musleabihf'; + } else { + $os_suffix = 'musleabi'; + } + break; + default: + throw new \RuntimeException("Unsupported architecture for musl: {$arch_name}"); + } + } else { + switch ($arch) { + case 'x86_64': + case 'aarch64': + case 'i686': + case 'powerpc': + case 'powerpc64': + case 'powerpc64le': + case 's390x': + $os_suffix = 'gnu'; + break; + case 'arm': + case 'armv7': + if (strpos(file_get_contents('/proc/cpuinfo'), 'hard') !== false) { + $os_suffix = 'gnueabihf'; + } else { + $os_suffix = 'gnueabi'; + } + break; + default: + throw new \RuntimeException("Unsupported architecture for glibc: {$arch_name}"); + } + } + } else { + $os_suffix = 'musl'; + } + break; + case 'freebsd': + break; + default: + throw new \RuntimeException("Unsupported operating system: {$os}. Please open an issue on GitHub."); + } + + $target_triple = $os_suffix ? "{$arch}-{$vendor}-{$os}-{$os_suffix}" : "{$arch}-{$vendor}-{$os}"; + $storage_dir = "mago-{$version}-{$target_triple}"; + $executable = "mago{$executable_extension}"; + + return [ + 'platform' => $target_triple, + 'storage_dir' => $storage_dir, + 'executable' => $executable, + ]; + } +} diff --git a/composer/MagoCommandProvider.php b/composer/MagoCommandProvider.php index f3f7ea4..f74e370 100644 --- a/composer/MagoCommandProvider.php +++ b/composer/MagoCommandProvider.php @@ -14,6 +14,6 @@ final class MagoCommandProvider implements CommandProvider */ public function getCommands(): array { - return [new InstallMagoAssetsCommand()]; + return [new InstallMagoBinaryCommand()]; } } diff --git a/composer/MagoPlugin.php b/composer/MagoPlugin.php index 252812e..9d50f89 100644 --- a/composer/MagoPlugin.php +++ b/composer/MagoPlugin.php @@ -82,7 +82,7 @@ public function onPackageEvent(PackageEvent $event): void (new PhpExecutableFinder())->find(), ...array_map(static fn(string $argument): string => ProcessExecutor::escape($argument), [ getenv('COMPOSER_BINARY') ?: 'composer', - 'mago:install-assets', + 'mago:install-binary', ]), ])); } diff --git a/composer/bin/.gitignore b/composer/bin/.gitignore index 79ded0b..301aa01 100644 --- a/composer/bin/.gitignore +++ b/composer/bin/.gitignore @@ -1,2 +1,2 @@ -version +.platform */* \ No newline at end of file diff --git a/composer/bin/mago b/composer/bin/mago index f958ac8..35d3825 100755 --- a/composer/bin/mago +++ b/composer/bin/mago @@ -5,98 +5,14 @@ declare(strict_types=1); (function() { // Guess executable path - $versionFile = __DIR__ . '/version'; - if (!file_exists($versionFile)) { - throw new \RuntimeException('Unable to detect curent mago version. Please run "composer mago:install-assets"'); + $platformFile = __DIR__ . '/.platform'; + if (!file_exists($platformFile)) { + throw new \RuntimeException('Unable to detect curent mago version. Please run "composer mago:install-binary"'); } - $version = file_get_contents(__DIR__ . '/version'); - $arch_name = php_uname('m'); - $arch = match($arch_name) { - 'x86_64', 'amd64' => 'x86_64', - 'arm64', 'aarch64' => 'aarch64', - 'armv7l' => 'armv7', - 'i386', 'i486', 'i586', 'i686' => 'i686', - 'ppc' => 'powerpc', - 'ppc64' => 'powerpc64', - 'ppc64le' => 'powerpc64le', - 's390x' => 's390x', - default => throw new \RuntimeException("Unsupported machine architecture: {$arch_name}. Please open an issue on GitHub."), - }; - - $os = strtolower(php_uname('s')); - $vendor = 'unknown'; - $os_suffix = ''; - $executable_extension = ''; - - switch ($os) { - case 'windows': - $vendor = 'pc'; - $os_suffix = 'msvc'; - $executable_extension = '.exe'; - break; - case 'darwin': - $vendor = 'apple'; - break; - case 'linux': - if (shell_exec('command -v ldd')) { - $ldd_version = shell_exec('ldd --version 2>&1'); - if (strpos($ldd_version, 'musl') !== false) { - switch ($arch) { - case 'x86_64': - case 'aarch64': - case 'i686': - $os_suffix = 'musl'; - break; - case 'arm': - case 'armv7': - if (strpos(file_get_contents('/proc/cpuinfo'), 'hard') !== false) { - $os_suffix = 'musleabihf'; - } else { - $os_suffix = 'musleabi'; - } - break; - default: - throw new \RuntimeException("Unsupported architecture for musl: {$arch_name}"); - } - } else { - switch ($arch) { - case 'x86_64': - case 'aarch64': - case 'i686': - case 'powerpc': - case 'powerpc64': - case 'powerpc64le': - case 's390x': - $os_suffix = 'gnu'; - break; - case 'arm': - case 'armv7': - if (strpos(file_get_contents('/proc/cpuinfo'), 'hard') !== false) { - $os_suffix = 'gnueabihf'; - } else { - $os_suffix = 'gnueabi'; - } - break; - default: - throw new \RuntimeException("Unsupported architecture for glibc: {$arch_name}"); - } - } - } else { - $os_suffix = 'musl'; - } - break; - case 'freebsd': - break; - default: - throw new \RuntimeException("Unsupported operating system: {$os}. Please open an issue on GitHub."); - } - - $target_triple = $os_suffix ? "{$arch}-{$vendor}-{$os}-{$os_suffix}" : "{$arch}-{$vendor}-{$os}"; - $executable = __DIR__ . "/{$version}/mago-{$version}-{$target_triple}/mago{$executable_extension}"; - + $executable = __DIR__ . '/'.file_get_contents($platformFile); if (!file_exists($executable)) { - throw new \RuntimeException("Unable to find mago executable at {$executable}. Please run 'composer mago:install-assets'"); + throw new \RuntimeException("Unable to find mago executable at {$executable}. Please run 'composer mago:install-binary'"); } $args = $_SERVER['argv'];