From ed5a516004355e699f72470c39d824367baf76ec Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 13:44:23 +0800 Subject: [PATCH] Implement check-update functionality for artifacts and enhance download result handling --- src/StaticPHP/Artifact/ArtifactCache.php | 10 ++- src/StaticPHP/Artifact/ArtifactDownloader.php | 39 +++++++++++ .../Artifact/Downloader/DownloadResult.php | 26 +++++--- .../Artifact/Downloader/Type/BitBucketTag.php | 2 +- .../Downloader/Type/CheckUpdateInterface.php | 20 ++++++ .../Downloader/Type/CheckUpdateResult.php | 14 ++++ .../Artifact/Downloader/Type/FileList.php | 38 +++++++---- .../Artifact/Downloader/Type/Git.php | 64 +++++++++++++++++-- .../Downloader/Type/GitHubRelease.php | 19 +++++- .../Downloader/Type/GitHubTarball.php | 20 +++++- .../Downloader/Type/HostedPackageBin.php | 4 +- .../Artifact/Downloader/Type/LocalDir.php | 2 +- .../Artifact/Downloader/Type/PIE.php | 54 ++++++++++------ .../Artifact/Downloader/Type/PhpRelease.php | 52 +++++++++++---- .../Artifact/Downloader/Type/Url.php | 2 +- src/StaticPHP/Command/CheckUpdateCommand.php | 64 +++++++++++++++++++ src/StaticPHP/ConsoleApplication.php | 2 + src/StaticPHP/Exception/ExceptionHandler.php | 13 +--- 18 files changed, 368 insertions(+), 77 deletions(-) create mode 100644 src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php create mode 100644 src/StaticPHP/Artifact/Downloader/Type/CheckUpdateResult.php create mode 100644 src/StaticPHP/Command/CheckUpdateCommand.php diff --git a/src/StaticPHP/Artifact/ArtifactCache.php b/src/StaticPHP/Artifact/ArtifactCache.php index 3302a37b..dcd75ef7 100644 --- a/src/StaticPHP/Artifact/ArtifactCache.php +++ b/src/StaticPHP/Artifact/ArtifactCache.php @@ -18,7 +18,8 @@ class ArtifactCache * filename?: string, * dirname?: string, * extract: null|'&custom'|string, - * hash: null|string + * hash: null|string, + * downloader: null|string * }, * binary: array{ * windows-x86_64?: null|array{ @@ -28,7 +29,8 @@ class ArtifactCache * dirname?: string, * extract: null|'&custom'|string, * hash: null|string, - * version?: null|string + * version?: null|string, + * downloader: null|string * } * } * }> @@ -108,6 +110,7 @@ class ArtifactCache 'hash' => sha1_file(DOWNLOAD_PATH . '/' . $download_result->filename), 'version' => $download_result->version, 'config' => $download_result->config, + 'downloader' => $download_result->downloader, ]; } elseif ($download_result->cache_type === 'file') { $obj = [ @@ -118,6 +121,7 @@ class ArtifactCache 'hash' => sha1_file(DOWNLOAD_PATH . '/' . $download_result->filename), 'version' => $download_result->version, 'config' => $download_result->config, + 'downloader' => $download_result->downloader, ]; } elseif ($download_result->cache_type === 'git') { $obj = [ @@ -128,6 +132,7 @@ class ArtifactCache 'hash' => trim(exec('cd ' . escapeshellarg(DOWNLOAD_PATH . '/' . $download_result->dirname) . ' && ' . SPC_GIT_EXEC . ' rev-parse HEAD')), 'version' => $download_result->version, 'config' => $download_result->config, + 'downloader' => $download_result->downloader, ]; } elseif ($download_result->cache_type === 'local') { $obj = [ @@ -138,6 +143,7 @@ class ArtifactCache 'hash' => null, 'version' => $download_result->version, 'config' => $download_result->config, + 'downloader' => $download_result->downloader, ]; } if ($obj === null) { diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index fd3caeaf..b2773c80 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -6,6 +6,8 @@ namespace StaticPHP\Artifact; use Psr\Log\LogLevel; use StaticPHP\Artifact\Downloader\DownloadResult; +use StaticPHP\Artifact\Downloader\Type\CheckUpdateInterface; +use StaticPHP\Artifact\Downloader\Type\CheckUpdateResult; use StaticPHP\Artifact\Downloader\Type\DownloadTypeInterface; use StaticPHP\Artifact\Downloader\Type\Git; use StaticPHP\Artifact\Downloader\Type\LocalDir; @@ -323,6 +325,43 @@ class ArtifactDownloader } } + public function checkUpdate(string $artifact_name, bool $prefer_source = false, bool $bare = false): CheckUpdateResult + { + $artifact = ArtifactLoader::getArtifactInstance($artifact_name); + if ($artifact === null) { + throw new WrongUsageException("Artifact '{$artifact_name}' not found, please check the name."); + } + if ($bare) { + $config = $artifact->getDownloadConfig('source'); + if (!is_array($config)) { + throw new WrongUsageException("Artifact '{$artifact_name}' has no source config for bare update check."); + } + $cls = $this->downloaders[$config['type']] ?? null; + if (!is_a($cls, CheckUpdateInterface::class, true)) { + throw new WrongUsageException("Artifact '{$artifact_name}' downloader does not support update checking."); + } + /** @var CheckUpdateInterface $downloader */ + $downloader = new $cls(); + return $downloader->checkUpdate($artifact_name, $config, null, $this); + } + $cache = ApplicationContext::get(ArtifactCache::class); + if ($prefer_source) { + $info = $cache->getSourceInfo($artifact_name) ?? $cache->getBinaryInfo($artifact_name, SystemTarget::getCurrentPlatformString()); + } else { + $info = $cache->getBinaryInfo($artifact_name, SystemTarget::getCurrentPlatformString()) ?? $cache->getSourceInfo($artifact_name); + } + if ($info === null) { + throw new WrongUsageException("Artifact '{$artifact_name}' is not downloaded yet, cannot check update."); + } + if (is_a($info['downloader'] ?? null, CheckUpdateInterface::class, true)) { + $cls = $info['downloader']; + /** @var CheckUpdateInterface $downloader */ + $downloader = new $cls(); + return $downloader->checkUpdate($artifact_name, $info['config'], $info['version'], $this); + } + throw new WrongUsageException("Artifact '{$artifact_name}' downloader does not support update checking, exit."); + } + public function getRetry(): int { return $this->retry; diff --git a/src/StaticPHP/Artifact/Downloader/DownloadResult.php b/src/StaticPHP/Artifact/Downloader/DownloadResult.php index 6fa40bed..2efe6945 100644 --- a/src/StaticPHP/Artifact/Downloader/DownloadResult.php +++ b/src/StaticPHP/Artifact/Downloader/DownloadResult.php @@ -17,6 +17,7 @@ class DownloadResult * @param bool $verified Whether the download has been verified (hash check) * @param null|string $version Version of the downloaded artifact (e.g., "1.2.3", "v2.0.0") * @param array $metadata Additional metadata (e.g., commit hash, release notes, etc.) + * @param null|string $downloader Class name of the downloader that performed this download */ private function __construct( public readonly string $cache_type, @@ -27,6 +28,7 @@ class DownloadResult public bool $verified = false, public readonly ?string $version = null, public readonly array $metadata = [], + public readonly ?string $downloader = null, ) { switch ($this->cache_type) { case 'archive': @@ -59,11 +61,12 @@ class DownloadResult mixed $extract = null, bool $verified = false, ?string $version = null, - array $metadata = [] + array $metadata = [], + ?string $downloader = null, ): DownloadResult { // judge if it is archive or just a pure file $cache_type = self::isArchiveFile($filename) ? 'archive' : 'file'; - return new self($cache_type, config: $config, filename: $filename, extract: $extract, verified: $verified, version: $version, metadata: $metadata); + return new self($cache_type, config: $config, filename: $filename, extract: $extract, verified: $verified, version: $version, metadata: $metadata, downloader: $downloader); } public static function file( @@ -71,10 +74,11 @@ class DownloadResult array $config, bool $verified = false, ?string $version = null, - array $metadata = [] + array $metadata = [], + ?string $downloader = null, ): DownloadResult { $cache_type = self::isArchiveFile($filename) ? 'archive' : 'file'; - return new self($cache_type, config: $config, filename: $filename, verified: $verified, version: $version, metadata: $metadata); + return new self($cache_type, config: $config, filename: $filename, verified: $verified, version: $version, metadata: $metadata, downloader: $downloader); } /** @@ -85,9 +89,9 @@ class DownloadResult * @param null|string $version Version string (tag, branch, or commit) * @param array $metadata Additional metadata (e.g., commit hash) */ - public static function git(string $dirname, array $config, mixed $extract = null, ?string $version = null, array $metadata = []): DownloadResult + public static function git(string $dirname, array $config, mixed $extract = null, ?string $version = null, array $metadata = [], ?string $downloader = null): DownloadResult { - return new self('git', config: $config, dirname: $dirname, extract: $extract, version: $version, metadata: $metadata); + return new self('git', config: $config, dirname: $dirname, extract: $extract, version: $version, metadata: $metadata, downloader: $downloader); } /** @@ -98,9 +102,9 @@ class DownloadResult * @param null|string $version Version string if known * @param array $metadata Additional metadata */ - public static function local(string $dirname, array $config, mixed $extract = null, ?string $version = null, array $metadata = []): DownloadResult + public static function local(string $dirname, array $config, mixed $extract = null, ?string $version = null, array $metadata = [], ?string $downloader = null): DownloadResult { - return new self('local', config: $config, dirname: $dirname, extract: $extract, version: $version, metadata: $metadata); + return new self('local', config: $config, dirname: $dirname, extract: $extract, version: $version, metadata: $metadata, downloader: $downloader); } /** @@ -136,7 +140,8 @@ class DownloadResult $this->extract, $this->verified, $version, - $this->metadata + $this->metadata, + $this->downloader, ); } @@ -154,7 +159,8 @@ class DownloadResult $this->extract, $this->verified, $this->version, - array_merge($this->metadata, [$key => $value]) + array_merge($this->metadata, [$key => $value]), + $this->downloader, ); } diff --git a/src/StaticPHP/Artifact/Downloader/Type/BitBucketTag.php b/src/StaticPHP/Artifact/Downloader/Type/BitBucketTag.php index 30942fe1..2ecc48df 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/BitBucketTag.php +++ b/src/StaticPHP/Artifact/Downloader/Type/BitBucketTag.php @@ -36,6 +36,6 @@ class BitBucketTag implements DownloadTypeInterface $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; logger()->debug("Downloading {$name} version {$ver} from BitBucket: {$download_url}"); default_shell()->executeCurlDownload($download_url, $path, retries: $downloader->getRetry()); - return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null); + return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null, downloader: static::class); } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php b/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php new file mode 100644 index 00000000..18445648 --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php @@ -0,0 +1,20 @@ +fetchFileList($name, $config, $downloader); + if (isset($config['download-url'])) { + $url = str_replace(['{file}', '{version}'], [$filename, $version], $config['download-url']); + } else { + $url = $config['url'] . $filename; + } + $filename = end($versions); + $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; + logger()->debug("Downloading {$name} from URL: {$url}"); + default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); + return DownloadResult::archive($filename, $config, $config['extract'] ?? null, version: $version, downloader: static::class); + } + + public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + [, $version] = $this->fetchFileList($name, $config, $downloader); + return new CheckUpdateResult( + old: $old_version, + new: $version, + needUpdate: $old_version === null || version_compare($version, $old_version, '>'), + ); + } + + protected function fetchFileList(string $name, array $config, ArtifactDownloader $downloader): array { logger()->debug("Fetching file list from {$config['url']}"); $page = default_shell()->executeCurl($config['url'], retries: $downloader->getRetry()); @@ -33,15 +58,6 @@ class FileList implements DownloadTypeInterface uksort($versions, 'version_compare'); $filename = end($versions); $version = array_key_last($versions); - if (isset($config['download-url'])) { - $url = str_replace(['{file}', '{version}'], [$filename, $version], $config['download-url']); - } else { - $url = $config['url'] . $filename; - } - $filename = end($versions); - $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; - logger()->debug("Downloading {$name} from URL: {$url}"); - default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); - return DownloadResult::archive($filename, $config, $config['extract'] ?? null); + return [$filename, $version, $versions]; } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/Git.php b/src/StaticPHP/Artifact/Downloader/Type/Git.php index 83c236eb..f518b396 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/Git.php +++ b/src/StaticPHP/Artifact/Downloader/Type/Git.php @@ -10,7 +10,7 @@ use StaticPHP\Exception\DownloaderException; use StaticPHP\Util\FileSystem; /** git */ -class Git implements DownloadTypeInterface +class Git implements DownloadTypeInterface, CheckUpdateInterface { public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult { @@ -21,8 +21,10 @@ class Git implements DownloadTypeInterface // direct branch clone if (isset($config['rev'])) { default_shell()->executeGitClone($config['url'], $config['rev'], $path, $shallow, $config['submodules'] ?? null); - $version = "dev-{$config['rev']}"; - return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version); + $hash_result = shell(false)->execWithResult(SPC_GIT_EXEC . ' -C ' . escapeshellarg($path) . ' rev-parse HEAD'); + $hash = ($hash_result[0] === 0 && !empty($hash_result[1])) ? trim($hash_result[1][0]) : ''; + $version = $hash !== '' ? "dev-{$config['rev']}+{$hash}" : "dev-{$config['rev']}"; + return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version, downloader: static::class); } if (!isset($config['regex'])) { throw new DownloaderException('Either "rev" or "regex" must be specified for git download type.'); @@ -64,8 +66,62 @@ class Git implements DownloadTypeInterface $branch = $matched_version_branch[$version]; logger()->info("Matched version {$version} from branch {$branch} for {$name}"); default_shell()->executeGitClone($config['url'], $branch, $path, $shallow, $config['submodules'] ?? null); - return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version); + return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version, downloader: static::class); } throw new DownloaderException("No matching branch found for regex {$config['regex']} (checked {$matched_count} branches)."); } + + public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + if (isset($config['rev'])) { + $shell = PHP_OS_FAMILY === 'Windows' ? cmd(false) : shell(false); + $result = $shell->execWithResult(SPC_GIT_EXEC . ' ls-remote ' . escapeshellarg($config['url']) . ' ' . escapeshellarg('refs/heads/' . $config['rev'])); + if ($result[0] !== 0 || empty($result[1])) { + throw new DownloaderException("Failed to ls-remote from {$config['url']}"); + } + $new_hash = substr($result[1][0], 0, 40); + $new_version = "dev-{$config['rev']}+{$new_hash}"; + // Extract stored hash from "dev-{rev}+{hash}", null if bare mode or old format without hash + $old_hash = ($old_version !== null && str_contains($old_version, '+')) ? substr(strrchr($old_version, '+'), 1) : null; + return new CheckUpdateResult( + old: $old_version, + new: $new_version, + needUpdate: $old_hash === null || $new_hash !== $old_hash, + ); + } + if (!isset($config['regex'])) { + throw new DownloaderException('Either "rev" or "regex" must be specified for git download type.'); + } + + $shell = PHP_OS_FAMILY === 'Windows' ? cmd(false) : shell(false); + $result = $shell->execWithResult(SPC_GIT_EXEC . ' ls-remote ' . escapeshellarg($config['url'])); + if ($result[0] !== 0) { + throw new DownloaderException("Failed to ls-remote from {$config['url']}"); + } + $refs = $result[1]; + $matched_version_branch = []; + + $regex = '/^' . $config['regex'] . '$/'; + foreach ($refs as $ref) { + $matches = null; + if (preg_match('/^[0-9a-f]{40}\s+refs\/heads\/(.+)$/', $ref, $matches)) { + $branch = $matches[1]; + if (preg_match($regex, $branch, $vermatch) && isset($vermatch['version'])) { + $matched_version_branch[$vermatch['version']] = $vermatch[0]; + } + } + } + uksort($matched_version_branch, function ($a, $b) { + return version_compare($b, $a); + }); + if (!empty($matched_version_branch)) { + $version = array_key_first($matched_version_branch); + return new CheckUpdateResult( + old: $old_version, + new: $version, + needUpdate: $old_version === null || version_compare($version, $old_version, '>'), + ); + } + throw new DownloaderException("No matching branch found for regex {$config['regex']}."); + } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php index 7b041288..15626089 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php @@ -9,7 +9,7 @@ use StaticPHP\Artifact\Downloader\DownloadResult; use StaticPHP\Exception\DownloaderException; /** ghrel */ -class GitHubRelease implements DownloadTypeInterface, ValidatorInterface +class GitHubRelease implements DownloadTypeInterface, ValidatorInterface, CheckUpdateInterface { use GitHubTokenSetupTrait; @@ -48,6 +48,7 @@ class GitHubRelease implements DownloadTypeInterface, ValidatorInterface */ public function getLatestGitHubRelease(string $name, string $repo, bool $prefer_stable, string $match_asset, ?string $query = null): array { + logger()->debug("Fetching {$name} GitHub release from {$repo}"); $url = str_replace('{repo}', $repo, self::API_URL); $url .= ($query ?? ''); $headers = $this->getGitHubTokenHeaders(); @@ -95,7 +96,7 @@ class GitHubRelease implements DownloadTypeInterface, ValidatorInterface $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; logger()->debug("Downloading {$name} asset from URL: {$asset_url}"); default_shell()->executeCurlDownload($asset_url, $path, headers: $headers, retries: $downloader->getRetry()); - return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null, version: $this->version); + return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null, version: $this->version, downloader: static::class); } public function validate(string $name, array $config, ArtifactDownloader $downloader, DownloadResult $result): bool @@ -117,4 +118,18 @@ class GitHubRelease implements DownloadTypeInterface, ValidatorInterface logger()->debug("No sha256 digest found for GitHub release asset of {$name}, skipping hash validation"); return true; } + + public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + if (!isset($config['match'])) { + throw new DownloaderException("GitHubRelease downloader requires 'match' config for {$name}"); + } + $this->getLatestGitHubRelease($name, $config['repo'], $config['prefer-stable'] ?? true, $config['match'], $config['query'] ?? null); + $new_version = $this->version ?? $old_version ?? ''; + return new CheckUpdateResult( + old: $old_version, + new: $new_version, + needUpdate: $old_version === null || $new_version !== $old_version, + ); + } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php index 8aa1ac69..a9283722 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php @@ -10,7 +10,7 @@ use StaticPHP\Exception\DownloaderException; /** ghtar */ /** ghtagtar */ -class GitHubTarball implements DownloadTypeInterface +class GitHubTarball implements DownloadTypeInterface, CheckUpdateInterface { use GitHubTokenSetupTrait; @@ -77,6 +77,22 @@ class GitHubTarball implements DownloadTypeInterface [$url, $filename] = $this->getGitHubTarballInfo($name, $config['repo'], $rel_type, $config['prefer-stable'] ?? true, $config['match'] ?? null, $name, $config['query'] ?? null); $path = DOWNLOAD_PATH . "/{$filename}"; default_shell()->executeCurlDownload($url, $path, headers: $this->getGitHubTokenHeaders()); - return DownloadResult::archive($filename, $config, $config['extract'] ?? null, version: $this->version); + return DownloadResult::archive($filename, $config, $config['extract'] ?? null, version: $this->version, downloader: static::class); + } + + public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + $rel_type = match ($config['type']) { + 'ghtar' => 'releases', + 'ghtagtar' => 'tags', + default => throw new DownloaderException("Invalid GitHubTarball type for {$name}"), + }; + $this->getGitHubTarballInfo($name, $config['repo'], $rel_type, $config['prefer-stable'] ?? true, $config['match'] ?? null, $name, $config['query'] ?? null); + $new_version = $this->version ?? $old_version ?? ''; + return new CheckUpdateResult( + old: $old_version, + new: $new_version, + needUpdate: $old_version === null || $new_version !== $old_version, + ); } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php b/src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php index c5cbb3b5..11caa19d 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php +++ b/src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php @@ -26,7 +26,7 @@ class HostedPackageBin implements DownloadTypeInterface public static function getReleaseInfo(): array { if (empty(self::$release_info)) { - $rel = (new GitHubRelease())->getGitHubReleases('hosted', self::BASE_REPO); + $rel = new GitHubRelease()->getGitHubReleases('hosted', self::BASE_REPO); if (empty($rel)) { throw new DownloaderException('No releases found for hosted package-bin'); } @@ -55,7 +55,7 @@ class HostedPackageBin implements DownloadTypeInterface $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; $headers = $this->getGitHubTokenHeaders(); default_shell()->executeCurlDownload($download_url, $path, headers: $headers, retries: $downloader->getRetry()); - return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null, version: $version); + return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null, version: $version, downloader: static::class); } } throw new DownloaderException("No matching asset found for hosted package-bin {$name}: {$find_str}"); diff --git a/src/StaticPHP/Artifact/Downloader/Type/LocalDir.php b/src/StaticPHP/Artifact/Downloader/Type/LocalDir.php index 93315ce3..77ac3d09 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/LocalDir.php +++ b/src/StaticPHP/Artifact/Downloader/Type/LocalDir.php @@ -13,6 +13,6 @@ class LocalDir implements DownloadTypeInterface public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult { logger()->debug("Using local source directory for {$name} from {$config['dirname']}"); - return DownloadResult::local($config['dirname'], $config, extract: $config['extract'] ?? null); + return DownloadResult::local($config['dirname'], $config, extract: $config['extract'] ?? null, downloader: static::class); } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/PIE.php b/src/StaticPHP/Artifact/Downloader/Type/PIE.php index e4f1a117..3a3ccc02 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/PIE.php +++ b/src/StaticPHP/Artifact/Downloader/Type/PIE.php @@ -9,28 +9,13 @@ use StaticPHP\Artifact\Downloader\DownloadResult; use StaticPHP\Exception\DownloaderException; /** pie */ -class PIE implements DownloadTypeInterface +class PIE implements DownloadTypeInterface, CheckUpdateInterface { public const string PACKAGIST_URL = 'https://repo.packagist.org/p2/'; public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult { - $packagist_url = self::PACKAGIST_URL . "{$config['repo']}.json"; - logger()->debug("Fetching {$name} source from packagist index: {$packagist_url}"); - $data = default_shell()->executeCurl($packagist_url, retries: $downloader->getRetry()); - if ($data === false) { - throw new DownloaderException("Failed to fetch packagist index for {$name} from {$packagist_url}"); - } - $data = json_decode($data, true); - if (!isset($data['packages'][$config['repo']]) || !is_array($data['packages'][$config['repo']])) { - throw new DownloaderException("failed to find {$name} repo info from packagist"); - } - // get the first version - $first = $data['packages'][$config['repo']][0] ?? []; - // check 'type' => 'php-ext' or contains 'php-ext' key - if (!isset($first['php-ext'])) { - throw new DownloaderException("failed to find {$name} php-ext info from packagist, maybe not a php extension package"); - } + $first = $this->fetchPackagistInfo($name, $config, $downloader); // get download link from dist $dist_url = $first['dist']['url'] ?? null; $dist_type = $first['dist']['type'] ?? null; @@ -42,6 +27,39 @@ class PIE implements DownloadTypeInterface $filename = "{$name}-{$version}." . ($dist_type === 'zip' ? 'zip' : 'tar.gz'); $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; default_shell()->executeCurlDownload($dist_url, $path, retries: $downloader->getRetry()); - return DownloadResult::archive($filename, $config, $config['extract'] ?? null); + return DownloadResult::archive($filename, $config, $config['extract'] ?? null, downloader: static::class); + } + + public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + $first = $this->fetchPackagistInfo($name, $config, $downloader); + $new_version = $first['version'] ?? null; + if ($new_version === null) { + throw new DownloaderException("failed to find version info for {$name} from packagist"); + } + return new CheckUpdateResult( + old: $old_version, + new: $new_version, + needUpdate: $old_version === null || version_compare($new_version, $old_version, '>'), + ); + } + + protected function fetchPackagistInfo(string $name, array $config, ArtifactDownloader $downloader): array + { + $packagist_url = self::PACKAGIST_URL . "{$config['repo']}.json"; + logger()->debug("Fetching {$name} source from packagist index: {$packagist_url}"); + $data = default_shell()->executeCurl($packagist_url, retries: $downloader->getRetry()); + if ($data === false) { + throw new DownloaderException("Failed to fetch packagist index for {$name} from {$packagist_url}"); + } + $data = json_decode($data, true); + if (!isset($data['packages'][$config['repo']]) || !is_array($data['packages'][$config['repo']])) { + throw new DownloaderException("failed to find {$name} repo info from packagist"); + } + $first = $data['packages'][$config['repo']][0] ?? []; + if (!isset($first['php-ext'])) { + throw new DownloaderException("failed to find {$name} php-ext info from packagist, maybe not a php extension package"); + } + return $first; } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php b/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php index ec6c33fa..372c7f50 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php +++ b/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php @@ -8,7 +8,7 @@ use StaticPHP\Artifact\ArtifactDownloader; use StaticPHP\Artifact\Downloader\DownloadResult; use StaticPHP\Exception\DownloaderException; -class PhpRelease implements DownloadTypeInterface, ValidatorInterface +class PhpRelease implements DownloadTypeInterface, ValidatorInterface, CheckUpdateInterface { public const string PHP_API = 'https://www.php.net/releases/index.php?json&version={version}'; @@ -24,16 +24,7 @@ class PhpRelease implements DownloadTypeInterface, ValidatorInterface $this->sha256 = null; return (new Git())->download($name, ['url' => 'https://github.com/php/php-src.git', 'rev' => 'master'], $downloader); } - - // Fetch PHP release info first - $info = default_shell()->executeCurl(str_replace('{version}', $phpver, self::PHP_API), retries: $downloader->getRetry()); - if ($info === false) { - throw new DownloaderException("Failed to fetch PHP release info for version {$phpver}"); - } - $info = json_decode($info, true); - if (!is_array($info) || !isset($info['version'])) { - throw new DownloaderException("Invalid PHP release info received for version {$phpver}"); - } + $info = $this->fetchPhpReleaseInfo($name, $downloader); $version = $info['version']; foreach ($info['source'] as $source) { if (str_ends_with($source['filename'], '.tar.xz')) { @@ -49,7 +40,7 @@ class PhpRelease implements DownloadTypeInterface, ValidatorInterface logger()->debug("Downloading PHP release {$version} from {$url}"); $path = DOWNLOAD_PATH . "/{$filename}"; default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); - return DownloadResult::archive($filename, config: $config, extract: $config['extract'] ?? null, version: $version); + return DownloadResult::archive($filename, config: $config, extract: $config['extract'] ?? null, version: $version, downloader: static::class); } public function validate(string $name, array $config, ArtifactDownloader $downloader, DownloadResult $result): bool @@ -73,4 +64,41 @@ class PhpRelease implements DownloadTypeInterface, ValidatorInterface logger()->debug("SHA256 checksum validated successfully for {$name}."); return true; } + + public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + $phpver = $downloader->getOption('with-php', '8.4'); + if ($phpver === 'git') { + // git version: delegate to Git checkUpdate with master branch + return (new Git())->checkUpdate($name, ['url' => 'https://github.com/php/php-src.git', 'rev' => 'master'], $old_version, $downloader); + } + $info = $this->fetchPhpReleaseInfo($name, $downloader); + $new_version = $info['version']; + return new CheckUpdateResult( + old: $old_version, + new: $new_version, + needUpdate: $old_version === null || $new_version !== $old_version, + ); + } + + protected function fetchPhpReleaseInfo(string $name, ArtifactDownloader $downloader): array + { + $phpver = $downloader->getOption('with-php', '8.4'); + // Handle 'git' version to clone from php-src repository + if ($phpver === 'git') { + // cannot fetch release info for git version, return empty info to skip validation + throw new DownloaderException("Cannot fetch PHP release info for 'git' version."); + } + + // Fetch PHP release info first + $info = default_shell()->executeCurl(str_replace('{version}', $phpver, self::PHP_API), retries: $downloader->getRetry()); + if ($info === false) { + throw new DownloaderException("Failed to fetch PHP release info for version {$phpver}"); + } + $info = json_decode($info, true); + if (!is_array($info) || !isset($info['version'])) { + throw new DownloaderException("Invalid PHP release info received for version {$phpver}"); + } + return $info; + } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/Url.php b/src/StaticPHP/Artifact/Downloader/Type/Url.php index a56f4dc7..02425fe5 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/Url.php +++ b/src/StaticPHP/Artifact/Downloader/Type/Url.php @@ -18,6 +18,6 @@ class Url implements DownloadTypeInterface logger()->debug("Downloading {$name} from URL: {$url}"); $version = $config['version'] ?? null; default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); - return DownloadResult::archive($filename, config: $config, extract: $config['extract'] ?? null, version: $version); + return DownloadResult::archive($filename, config: $config, extract: $config['extract'] ?? null, version: $version, downloader: static::class); } } diff --git a/src/StaticPHP/Command/CheckUpdateCommand.php b/src/StaticPHP/Command/CheckUpdateCommand.php new file mode 100644 index 00000000..965fb201 --- /dev/null +++ b/src/StaticPHP/Command/CheckUpdateCommand.php @@ -0,0 +1,64 @@ +addArgument('artifact', InputArgument::REQUIRED, 'The name of the artifact(s) to check for updates, comma-separated'); + $this->addOption('json', null, null, 'Output result in JSON format'); + $this->addOption('bare', null, null, 'Check update without requiring the artifact to be downloaded first (old version will be null)'); + + // --with-php option for checking updates with a specific PHP version context + $this->addOption('with-php', null, InputOption::VALUE_REQUIRED, 'PHP version in major.minor format (default 8.4)', '8.4'); + } + + public function handle(): int + { + $artifacts = parse_comma_list($this->input->getArgument('artifact')); + + try { + $downloader = new ArtifactDownloader($this->input->getOptions()); + $bare = (bool) $this->getOption('bare'); + if ($this->getOption('json')) { + $outputs = []; + foreach ($artifacts as $artifact) { + $result = $downloader->checkUpdate($artifact, bare: $bare); + $outputs[$artifact] = [ + 'need-update' => $result->needUpdate, + 'old' => $result->old, + 'new' => $result->new, + ]; + } + $this->output->writeln(json_encode($outputs, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + return static::OK; + } + foreach ($artifacts as $artifact) { + $result = $downloader->checkUpdate($artifact, bare: $bare); + if (!$result->needUpdate) { + $this->output->writeln("Artifact {$artifact} is already up to date (version: {$result->new})"); + } else { + $this->output->writeln("Update available for artifact: {$artifact}"); + $this->output->writeln(" Old version: {$result->old}"); + $this->output->writeln(" New version: {$result->new}"); + } + } + return static::OK; + } catch (SPCException $e) { + $e->setSimpleOutput(); + throw $e; + } + } +} diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index 023ddf84..a02b38c7 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -6,6 +6,7 @@ namespace StaticPHP; use StaticPHP\Command\BuildLibsCommand; use StaticPHP\Command\BuildTargetCommand; +use StaticPHP\Command\CheckUpdateCommand; use StaticPHP\Command\Dev\DumpCapabilitiesCommand; use StaticPHP\Command\Dev\DumpStagesCommand; use StaticPHP\Command\Dev\EnvCommand; @@ -63,6 +64,7 @@ class ConsoleApplication extends Application new SPCConfigCommand(), new DumpLicenseCommand(), new ResetCommand(), + new CheckUpdateCommand(), // dev commands new ShellCommand(), diff --git a/src/StaticPHP/Exception/ExceptionHandler.php b/src/StaticPHP/Exception/ExceptionHandler.php index 20cf9395..9dddc910 100644 --- a/src/StaticPHP/Exception/ExceptionHandler.php +++ b/src/StaticPHP/Exception/ExceptionHandler.php @@ -29,12 +29,6 @@ class ExceptionHandler RegistryException::class, ]; - public const array MINOR_LOG_EXCEPTIONS = [ - InterruptException::class, - WrongUsageException::class, - RegistryException::class, - ]; - /** @var array Build PHP extra info binding */ private static array $build_php_extra_info = []; @@ -57,10 +51,7 @@ class ExceptionHandler }; self::logError($head_msg); - // ---------------------------------------- - $minor_logs = in_array($class, self::MINOR_LOG_EXCEPTIONS, true); - - if ($minor_logs) { + if ($e->isSimpleOutput()) { return self::getReturnCode($e); } @@ -283,6 +274,6 @@ class ExceptionHandler self::printArrayInfo($info); } - self::logError("---------------------------------------------------------\n", color: 'none'); + self::logError("-----------------------------------------------------------\n", color: 'none'); } }