diff --git a/pkgs.json b/pkgs.json new file mode 100644 index 0000000..646f295 --- /dev/null +++ b/pkgs.json @@ -0,0 +1,75 @@ +{ + "php-cs-fixer": { + "repo": "FriendsOfPHP/PHP-CS-Fixer", + "bin": "php-cs-fixer.phar" + }, + "phpunit": { + "url": "https://phar.phpunit.de/phpunit-${{version}}.phar", + "latest": "9", + "bin": "phpunit.phar" + }, + "php": { + "repo": "dixyes/lwmbs", + "jobs": { + "Darwin.x86_64": "2969003447", + "Darwin.arm64": "2969003447", + "Linux.x86_64": "2961452571", + "Linux.aarch64": "2961452571" + }, + "job_artifact_match_rule": { + "Darwin.x86_64": "${{prefix}}_${{php-version}}_${{arch}}", + "Darwin.arm64": "${{prefix}}_${{php-version}}_${{arch}}", + "Linux.x86_64": "${{prefix}}_static_${{php-version}}_musl_${{arch}}", + "Linux.aarch64": "${{prefix}}_static_${{php-version}}_musl_${{arch}}" + }, + "latest": "8.1", + "versions": ["8.1", "8.0"] + }, + "micro": { + "repo": "dixyes/lwmbs", + "jobs": { + "Darwin.x86_64": "2969003447", + "Darwin.arm64": "2969003447", + "Linux.x86_64": "2961452571", + "Linux.aarch64": "2961452571" + }, + "job_artifact_match_rule": { + "Darwin.x86_64": "${{prefix}}_${{php-version}}_${{arch}}", + "Darwin.arm64": "${{prefix}}_${{php-version}}_${{arch}}", + "Linux.x86_64": "${{prefix}}_static_${{php-version}}_musl_${{arch}}", + "Linux.aarch64": "${{prefix}}_static_${{php-version}}_musl_${{arch}}" + }, + "latest": "8.1", + "versions": ["8.1", "8.0"] + }, + "box": { + "repo": "hyperf/box", + "bin": "box.phar", + "release_asset_match_rule": { + "Darwin.x86_64": "box_x86_64_macos", + "Darwin.arm64": "box_arm64_macos", + "Linux.x86_64": "box_x86_64_linux", + "Linux.aarch64": "box_aarch64_linux" + } + }, + "composer": { + "repo": "composer/composer", + "bin": "composer.phar", + "sources": { + "github.com": { + "type": "github", + "url": "github.com" + }, + "getcomposer.org": { + "type": "url", + "url": "https://getcomposer.org/download/${{version}}/${{bin}}" + }, + "default": { + "type": "url", + "url": "https://getcomposer.org/download/${{version}}/${{bin}}" + } + }, + "latest": "latest", + "latest_fetch_type": "github" + } +} \ No newline at end of file diff --git a/src/app/DownloadHandler/AbstractDownloadHandler.php b/src/app/DownloadHandler/AbstractDownloadHandler.php index b36e7e6..244154a 100644 --- a/src/app/DownloadHandler/AbstractDownloadHandler.php +++ b/src/app/DownloadHandler/AbstractDownloadHandler.php @@ -15,6 +15,8 @@ use App\Config; use App\Exception\NotSupportVersionsException; use App\GithubClient; +use App\PkgDefinition\Definition; +use App\PkgDefinitionManager; use GuzzleHttp\Client; use GuzzleHttp\TransferStats; use Hyperf\Context\Context; @@ -39,6 +41,9 @@ abstract class AbstractDownloadHandler #[Inject] protected Config $config; + #[Inject] + protected PkgDefinitionManager $pkgsDefinitionManager; + protected string $runtimePath; public function __construct() @@ -46,9 +51,9 @@ public function __construct() $this->runtimePath = $this->config->getConfig('path.runtime', getenv('HOME') . '/.box'); } - abstract public function handle(string $repo, string $version, array $options = []): ?SplFileInfo; + abstract public function handle(string $pkgName, string $version, array $options = []): ?SplFileInfo; - abstract public function versions(string $repo, array $options = []): array; + abstract public function versions(string $pkgName, array $options = []): array; protected function fetchDownloadUrlFromGithubRelease(string $assetName, string $fullRepo, string $version): ?string { @@ -161,4 +166,17 @@ protected function byteToKb(int $byte): int { return (int)ceil($byte / 1024); } + + protected function getDefinition(string $pkgName): ?Definition + { + return $this->pkgsDefinitionManager->getDefinition($pkgName); + } + + protected function replaces(string $subject, array $replaces): string + { + foreach ($replaces as $search => $replace) { + $subject = str_replace('${{' . $search . '}}', $replace, $subject); + } + return $subject; + } } diff --git a/src/app/DownloadHandler/BoxHandler.php b/src/app/DownloadHandler/BoxHandler.php index 00b08c6..2034e50 100644 --- a/src/app/DownloadHandler/BoxHandler.php +++ b/src/app/DownloadHandler/BoxHandler.php @@ -27,7 +27,7 @@ public function __construct() $this->binName = $this->getAssetName(); } - public function handle(string $repo, string $version, array $options = []): ?SplFileInfo + public function handle(string $pkgName, string $version, array $options = []): ?SplFileInfo { $url = $this->fetchDownloadUrlFromGithubRelease($this->getAssetName(), $this->fullRepo, $version); $savePath = Phar::running(false) ?: $this->runtimePath . '/'; @@ -35,7 +35,7 @@ public function handle(string $repo, string $version, array $options = []): ?Spl return $this->download($url, $savePath, 0755, $this->binName); } - public function versions(string $repo, array $options = []): array + public function versions(string $pkgName, array $options = []): array { return $this->fetchVersionsFromGithubRelease($this->fullRepo, $this->getAssetName()); } diff --git a/src/app/DownloadHandler/ComposerHandler.php b/src/app/DownloadHandler/ComposerHandler.php index af8aab5..78906b9 100644 --- a/src/app/DownloadHandler/ComposerHandler.php +++ b/src/app/DownloadHandler/ComposerHandler.php @@ -13,45 +13,44 @@ namespace App\DownloadHandler; use App\Exception\NotSupportVersionsException; +use App\PkgDefinition\Definition; use SplFileInfo; class ComposerHandler extends AbstractDownloadHandler { protected string $fullRepo = 'composer/composer'; - protected string $binName = 'composer.phar'; - - protected string $getComposerOrgBaseUrl = 'getcomposer.org'; - protected string $githubBaseUrl = 'github.com'; - public function handle(string $repo, string $version, array $options = []): ?SplFileInfo + public function handle(string $pkgName, string $version, array $options = []): ?SplFileInfo { + $definition = $this->getDefinition($pkgName); if (! isset($options['source'])) { - $options['source'] = $this->getComposerOrgBaseUrl; + $options['source'] = $definition->getSources()->getSource('default')?->getUrl(); } $url = match ($options['source']) { - $this->githubBaseUrl => $this->fetchDownloadUrlFromGithubRelease($this->binName, $this->fullRepo, $version), - default => $this->fetchDownloadUrlFromGetComposerOrg($version), + $this->githubBaseUrl => $this->fetchDownloadUrlFromGithubRelease($definition->getBin(), $definition->getRepo(), $version), + default => $this->fetchDownloadUrlFromGetComposerOrg($definition, $version), }; return $this->download($url, $this->runtimePath . '/', 0755); } - public function versions(string $repo, array $options = []): array + public function versions(string $pkgName, array $options = []): array { + $definition = $this->getDefinition($pkgName); if (! isset($options['source'])) { - $options['source'] = $this->getComposerOrgBaseUrl; + $options['source'] = $definition->getSources()->getSource('default')?->getUrl(); } return match ($options['source']) { - $this->githubBaseUrl => $this->fetchVersionsFromGithubRelease($this->fullRepo), - default => throw new NotSupportVersionsException($repo), + $this->githubBaseUrl => $this->fetchVersionsFromGithubRelease($definition->getRepo()), + default => throw new NotSupportVersionsException($pkgName), }; } - protected function fetchDownloadUrlFromGetComposerOrg(string $version): string + protected function fetchDownloadUrlFromGetComposerOrg(Definition $definition, string $version): string { if ($version === 'latest') { - $release = $this->githubClient->getRelease($this->fullRepo, $version); + $release = $this->githubClient->getRelease($definition->getRepo(), $version); if (! isset($release['tag_name'])) { throw new \RuntimeException('Cannot match the specified version from github releases.'); } @@ -59,6 +58,13 @@ protected function fetchDownloadUrlFromGetComposerOrg(string $version): string } else { $specifiedVersion = $version; } - return 'https://' . $this->getComposerOrgBaseUrl . '/download/' . $specifiedVersion . '/' . $this->binName; + $url = $definition->getSources()?->getSource('getcomposer.org')?->getUrl(); + if (! $url) { + throw new \RuntimeException('Cannot parse the download url by getcomposer.org.'); + } + return $this->replaces($url, [ + 'version' => $specifiedVersion, + 'bin' => $definition->getBin(), + ]); } } diff --git a/src/app/DownloadHandler/DefaultHandler.php b/src/app/DownloadHandler/DefaultHandler.php index 204d195..4089c22 100644 --- a/src/app/DownloadHandler/DefaultHandler.php +++ b/src/app/DownloadHandler/DefaultHandler.php @@ -17,46 +17,34 @@ class DefaultHandler extends AbstractDownloadHandler { - protected array $definitions = [ - 'php-cs-fixer' => [ - 'repo' => 'FriendsOfPHP/PHP-CS-Fixer', - 'bin' => 'php-cs-fixer.phar', - ], - 'phpunit' => [ - 'url' => 'https://phar.phpunit.de/phpunit-${{version}}.phar', - 'latest' => '9', - 'bin' => 'phpunit.phar', - ], - ]; - - public function handle(string $repo, string $version, array $options = []): ?SplFileInfo + public function handle(string $pkgName, string $version, array $options = []): ?SplFileInfo { - if (! isset($this->definitions[$repo])) { + $definition = $this->getDefinition($pkgName); + if (! $definition) { throw new \RuntimeException('The package not found'); } - $definition = $this->definitions[$repo]; - if (isset($definition['repo'])) { - $url = $this->fetchDownloadUrlFromGithubRelease($definition['bin'], $definition['repo'], $version); - } elseif (isset($definition['url'])) { - if ($version === 'latest' && isset($definition['latest'])) { - $version = $definition['latest']; + if ($definition->getRepo()) { + $url = $this->fetchDownloadUrlFromGithubRelease($definition->getBin(), $definition->getRepo(), $version); + } elseif ($definition->getUrl()) { + if ($version === 'latest' && $definition->getLatest()) { + $version = $definition->getLatest(); } - $url = str_replace('${{version}}', $version, $definition['url']); + $url = str_replace('${{version}}', $version, $definition->getUrl()); } else { throw new \RuntimeException('The definition of package is invalid'); } - return $this->download($url, $this->runtimePath . '/', 0755, $definition['bin']); + return $this->download($url, $this->runtimePath . '/', 0755, $definition->getBin()); } - public function versions(string $repo, array $options = []): array + public function versions(string $pkgName, array $options = []): array { - if (! isset($this->definitions[$repo])) { + $definition = $this->getDefinition($pkgName); + if (! $definition) { throw new \RuntimeException('The package not found'); } - $definition = $this->definitions[$repo]; - if (! isset($definition['repo'])) { - throw new NotSupportVersionsException($repo); + if (! $definition->getRepo()) { + throw new NotSupportVersionsException($pkgName); } - return $this->fetchVersionsFromGithubRelease($definition['repo'], $definition['bin']); + return $this->fetchVersionsFromGithubRelease($definition->getRepo(), $definition->getBin()); } } diff --git a/src/app/DownloadHandler/MicroHandler.php b/src/app/DownloadHandler/MicroHandler.php index 04f28ee..c69e429 100644 --- a/src/app/DownloadHandler/MicroHandler.php +++ b/src/app/DownloadHandler/MicroHandler.php @@ -22,11 +22,11 @@ class MicroHandler extends PhpHandler #[Inject] protected Client $httpClient; - public function handle(string $repo, string $version, array $options = []): ?SplFileInfo + public function handle(string $pkgName, string $version, array $options = []): ?SplFileInfo { $version = $this->prehandleVersion($version); try { - $response = $this->getArtifact($version, 'micro'); + $response = $this->getArtifact($this->getDefinition($pkgName), $version, 'micro'); if ($response->getStatusCode() !== 302 || ! $response->getHeaderLine('Location')) { throw new \RuntimeException('Download failed, cannot retrieve the download url from artifact.'); } diff --git a/src/app/DownloadHandler/PhpHandler.php b/src/app/DownloadHandler/PhpHandler.php index c62ccb3..8f7e8d8 100644 --- a/src/app/DownloadHandler/PhpHandler.php +++ b/src/app/DownloadHandler/PhpHandler.php @@ -12,7 +12,7 @@ namespace App\DownloadHandler; -use App\Exception\NotSupportVersionsException; +use App\PkgDefinition\Definition; use GuzzleHttp\Client; use GuzzleHttp\Exception\GuzzleException; use Hyperf\Di\Annotation\Inject; @@ -24,27 +24,11 @@ class PhpHandler extends AbstractDownloadHandler #[Inject] protected Client $httpClient; - protected string $repo = 'dixyes/lwmbs'; - - protected array $jobs - = [ - 'Darwin.x86_64' => '2969003447', - 'Darwin.arm64' => '2969003447', - 'Linux.x86_64' => '2961452571', - 'Linux.aarch64' => '2961452571', - ]; - - protected array $matchRules - = [ - 'Darwin' => '${{prefix}}_${{php-version}}_${{arch}}', - 'Linux' => '${{prefix}}_static_${{php-version}}_musl_${{arch}}', - ]; - - public function handle(string $repo, string $version, array $options = []): ?SplFileInfo + public function handle(string $pkgName, string $version, array $options = []): ?SplFileInfo { $version = $this->prehandleVersion($version); try { - $response = $this->getArtifact($version, 'cli'); + $response = $this->getArtifact($this->getDefinition($pkgName), $version, 'cli'); if ($response->getStatusCode() !== 302 || ! $response->getHeaderLine('Location')) { throw new \RuntimeException('Download failed, cannot retrieve the download url from artifact.'); } @@ -98,7 +82,7 @@ protected function prehandleVersion(string $version): string /** * @throws \GuzzleHttp\Exception\GuzzleException */ - protected function getArtifact(string $version, string $prefix): ResponseInterface + protected function getArtifact(Definition $definition, string $version, string $prefix): ResponseInterface { $githubToken = $this->githubClient->getGithubToken(); if (! $githubToken) { @@ -107,8 +91,8 @@ protected function getArtifact(string $version, string $prefix): ResponseInterfa $os = PHP_OS_FAMILY; $arch = php_uname('m'); $key = $os . '.' . $arch; - $response = $this->githubClient->getActionsArtifacts($this->repo, $this->jobs[$key]); - $searchKey = $this->buildSearchKey($os, $prefix, $version, $arch); + $response = $this->githubClient->getActionsArtifacts($definition->getRepo(), $definition->getJobs()?->getJob($key)?->getJobId()); + $searchKey = $this->buildSearchKey($definition, $key, $prefix, $version, $arch); $artifact = $this->matchArtifact($response['artifacts'] ?? [], $searchKey); if (! isset($artifact['archive_download_url'])) { throw new \RuntimeException('Does not match any artifact.'); @@ -122,33 +106,23 @@ protected function getArtifact(string $version, string $prefix): ResponseInterfa ]); } - protected function buildSearchKey(string $os, string $prefix, string $version, string $arch): string + protected function buildSearchKey(Definition $definition, string $key, string $prefix, string $version, string $arch): string { - return $this->replaces($this->matchRules[$os], [ + return $this->replaces($definition->getJobArtifactMatchRule()[$key], [ 'prefix' => $prefix, 'php-version' => $version, 'arch' => $arch, ]); } - protected function replaces(string $subject, array $replaces): string - { - foreach ($replaces as $search => $replace) { - $subject = str_replace('${{' . $search . '}}', $replace, $subject); - } - return $subject; - } - protected function isBinExists(string $string): bool { $result = shell_exec(sprintf("which %s", escapeshellarg($string))); return ! empty($result) && ! str_contains($result, 'not found'); } - public function versions(string $repo, array $options = []): array + public function versions(string $pkgName, array $options = []): array { - return [ - '8.1', '8.0' - ]; + return $this->getDefinition($pkgName)->getVersions(); } } diff --git a/src/app/PkgDefinition/Definition.php b/src/app/PkgDefinition/Definition.php new file mode 100644 index 0000000..c5e5eab --- /dev/null +++ b/src/app/PkgDefinition/Definition.php @@ -0,0 +1,143 @@ +pkgName = $pkgName; + isset($data['repo']) && is_string($data['repo']) && $this->setRepo($data['repo']); + isset($data['bin']) && is_string($data['bin']) && $this->setBin($data['bin']); + isset($data['latest']) && is_string($data['latest']) && $this->setLatest($data['latest']); + isset($data['latest_fetch_type']) && is_string($data['latest_fetch_type']) && $this->setLatestFetchType($data['latest_fetch_type']); + isset($data['url']) && is_string($data['url']) && $this->setUrl($data['url']); + isset($data['jobs']) && is_array($data['jobs']) && $this->setJobs(new Jobs($data['jobs'])); + isset($data['job_artifact_match_rule']) && is_array($data['job_artifact_match_rule']) && $this->setJobArtifactMatchRule($data['job_artifact_match_rule']); + isset($data['release_asset_match_rule']) && is_array($data['release_asset_match_rule']) && $this->setReleaseAssetMatchRule($data['release_asset_match_rule']); + isset($data['versions']) && is_array($data['versions']) && $this->setVersions($data['versions']); + isset($data['sources']) && is_array($data['sources']) && $this->setSources(new Sources($data['sources'])); + } + + public function getRepo(): ?string + { + return $this->repo; + } + + public function setRepo(?string $repo): Definition + { + $this->repo = $repo; + return $this; + } + + public function getBin(): ?string + { + return $this->bin; + } + + public function setBin(?string $bin): Definition + { + $this->bin = $bin; + return $this; + } + + public function getLatest(): ?string + { + return $this->latest; + } + + public function setLatest(?string $latest): Definition + { + $this->latest = $latest; + return $this; + } + + public function getLatestFetchType(): ?string + { + return $this->latestFetchType; + } + + public function setLatestFetchType(?string $latestFetchType): Definition + { + $this->latestFetchType = $latestFetchType; + return $this; + } + + public function getUrl(): ?string + { + return $this->url; + } + + public function setUrl(?string $url): Definition + { + $this->url = $url; + return $this; + } + + public function getJobs(): ?Jobs + { + return $this->jobs; + } + + public function setJobs(?Jobs $jobs): Definition + { + $this->jobs = $jobs; + return $this; + } + + public function getJobArtifactMatchRule(): array + { + return $this->jobArtifactMatchRule; + } + + public function setJobArtifactMatchRule(array $jobArtifactMatchRule): Definition + { + $this->jobArtifactMatchRule = $jobArtifactMatchRule; + return $this; + } + + public function getReleaseAssetMatchRule(): array + { + return $this->releaseAssetMatchRule; + } + + public function setReleaseAssetMatchRule(array $releaseAssetMatchRule): Definition + { + $this->releaseAssetMatchRule = $releaseAssetMatchRule; + return $this; + } + + public function getVersions(): array + { + return $this->versions; + } + + public function setVersions(array $versions): Definition + { + $this->versions = $versions; + return $this; + } + + public function getSources(): Sources + { + return $this->sources; + } + + public function setSources(Sources $sources): Definition + { + $this->sources = $sources; + return $this; + } +} diff --git a/src/app/PkgDefinition/Job.php b/src/app/PkgDefinition/Job.php new file mode 100644 index 0000000..c7ef59c --- /dev/null +++ b/src/app/PkgDefinition/Job.php @@ -0,0 +1,50 @@ +os = explode('.', $osAndArch)[0] ?? 'Unknown'; + $this->arch = explode('.', $osAndArch)[1] ?? 'x86_64'; + $this->jobId = $jobId; + } + + public function getOs(): string + { + return $this->os; + } + + public function setOs(string $os): Job + { + $this->os = $os; + return $this; + } + + public function getArch(): string + { + return $this->arch; + } + + public function setArch(string $arch): Job + { + $this->arch = $arch; + return $this; + } + + public function getJobId(): string + { + return $this->jobId; + } + + public function setJobId(string $jobId): Job + { + $this->jobId = $jobId; + return $this; + } +} diff --git a/src/app/PkgDefinition/Jobs.php b/src/app/PkgDefinition/Jobs.php new file mode 100644 index 0000000..c182243 --- /dev/null +++ b/src/app/PkgDefinition/Jobs.php @@ -0,0 +1,35 @@ + $jobId) { + $this->jobs[$arch] = new Job($arch, $jobId); + } + } + + public function getJob(string $arch): ?Job + { + $job = $this->jobs[$arch] ?? null; + return $job instanceof Job ? $job : null; + } + + public function getJobs(): array + { + return $this->jobs; + } + + public function setJobs(array $jobs): static + { + $this->jobs = $jobs; + return $this; + } +} diff --git a/src/app/PkgDefinition/Source.php b/src/app/PkgDefinition/Source.php new file mode 100644 index 0000000..34eced7 --- /dev/null +++ b/src/app/PkgDefinition/Source.php @@ -0,0 +1,50 @@ +setName($name); + isset($data['type']) && is_string($data['type']) && $this->setType($data['type']); + isset($data['url']) && is_string($data['url']) && $this->setUrl($data['url']); + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): Source + { + $this->name = $name; + return $this; + } + + public function getType(): string + { + return $this->type; + } + + public function setType(string $type): Source + { + $this->type = $type; + return $this; + } + + public function getUrl(): string + { + return $this->url; + } + + public function setUrl(string $url): Source + { + $this->url = $url; + return $this; + } +} diff --git a/src/app/PkgDefinition/Sources.php b/src/app/PkgDefinition/Sources.php new file mode 100644 index 0000000..dbfd1a4 --- /dev/null +++ b/src/app/PkgDefinition/Sources.php @@ -0,0 +1,38 @@ + $data) { + $this->sources[$name] = new Source($name, $data); + } + } + + public function getSource(string $name): ?Source + { + $source = $this->sources[$name] ?? null; + return $source instanceof Source ? $source : null; + } + + public function getSources(): array + { + return $this->sources; + } + + public function setSources(array $sources): Sources + { + // Validate the value is an array of Source objects + foreach ($sources as $source) { + if (!($source instanceof Source)) { + throw new \InvalidArgumentException('Invalid value for sources'); + } + } + $this->sources = $sources; + return $this; + } +} diff --git a/src/app/PkgDefinitionManager.php b/src/app/PkgDefinitionManager.php new file mode 100644 index 0000000..6e1faf4 --- /dev/null +++ b/src/app/PkgDefinitionManager.php @@ -0,0 +1,66 @@ +fetchPkgs(); + } + + public function getDefinition(string $pkg): ?Definition + { + foreach ($this->pkgs as $name => $item) { + if ($name === $pkg) { + return new Definition($name, $item); + } + } + return null; + } + + public function getPkgs(): array + { + return $this->pkgs; + } + + public function fetchPkgs(): bool + { + try { + // Is url start with file:// ? + if (str_starts_with($this->url, 'file://')) { + $path = substr($this->url, 7); + if (! file_exists($path)) { + $this->logger->error(sprintf('File %s not exists.', $path)); + return false; + } + $this->pkgs = json_decode(file_get_contents($path), true); + } else { + $response = (new Client())->get($this->url); + if ($response->getStatusCode() === 200) { + $this->pkgs = json_decode($response->getBody()->getContents(), true); + } + } + } catch (\Throwable $exception) { + $this->logger->error($exception->getMessage()); + return false; + } + return true; + } +}