From 2d550a8db44f90bb063487f15111ba330727c2ad Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 13:43:28 +0800 Subject: [PATCH 01/21] Add simple output handling to exception classes --- src/StaticPHP/Exception/InterruptException.php | 5 ++++- src/StaticPHP/Exception/RegistryException.php | 5 ++++- src/StaticPHP/Exception/SPCException.php | 12 ++++++++++++ src/StaticPHP/Exception/WrongUsageException.php | 5 ++++- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/StaticPHP/Exception/InterruptException.php b/src/StaticPHP/Exception/InterruptException.php index 77b5240a3..3f55d7a85 100644 --- a/src/StaticPHP/Exception/InterruptException.php +++ b/src/StaticPHP/Exception/InterruptException.php @@ -7,4 +7,7 @@ /** * Exception caused by manual intervention. */ -class InterruptException extends SPCException {} +class InterruptException extends SPCException +{ + protected bool $simple_output = true; +} diff --git a/src/StaticPHP/Exception/RegistryException.php b/src/StaticPHP/Exception/RegistryException.php index 347a132ad..17d65cf2c 100644 --- a/src/StaticPHP/Exception/RegistryException.php +++ b/src/StaticPHP/Exception/RegistryException.php @@ -4,4 +4,7 @@ namespace StaticPHP\Exception; -class RegistryException extends SPCException {} +class RegistryException extends SPCException +{ + protected bool $simple_output = true; +} diff --git a/src/StaticPHP/Exception/SPCException.php b/src/StaticPHP/Exception/SPCException.php index 307cf6cda..7ec27abec 100644 --- a/src/StaticPHP/Exception/SPCException.php +++ b/src/StaticPHP/Exception/SPCException.php @@ -20,6 +20,8 @@ */ abstract class SPCException extends \Exception { + protected bool $simple_output = false; + /** @var null|array Package information */ private ?array $package_info = null; @@ -155,6 +157,16 @@ public function getExtraLogFiles(): array return $this->extra_log_files; } + public function isSimpleOutput(): bool + { + return $this->simple_output; + } + + public function setSimpleOutput(bool $simple_output = true): void + { + $this->simple_output = $simple_output; + } + /** * Load stack trace information to detect Package, Builder, and Installer context. */ diff --git a/src/StaticPHP/Exception/WrongUsageException.php b/src/StaticPHP/Exception/WrongUsageException.php index 2044a82c0..631a242ab 100644 --- a/src/StaticPHP/Exception/WrongUsageException.php +++ b/src/StaticPHP/Exception/WrongUsageException.php @@ -10,4 +10,7 @@ * This exception is used to indicate that the SPC is being used incorrectly. * Such as when a command is not supported or an invalid argument is provided. */ -class WrongUsageException extends SPCException {} +class WrongUsageException extends SPCException +{ + protected bool $simple_output = true; +} From ed5a516004355e699f72470c39d824367baf76ec Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 13:44:23 +0800 Subject: [PATCH 02/21] 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 | 48 +++++++++----- .../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, 365 insertions(+), 74 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 3302a37bc..dcd75ef7d 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 @@ public function lock(Artifact|string $artifact, string $lock_type, DownloadResul '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 @@ public function lock(Artifact|string $artifact, string $lock_type, DownloadResul '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 @@ public function lock(Artifact|string $artifact, string $lock_type, DownloadResul '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 @@ public function lock(Artifact|string $artifact, string $lock_type, DownloadResul '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 fd3caeaf1..b2773c808 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -6,6 +6,8 @@ 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 @@ public function download(bool $interactive = true): void } } + 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 6fa40bed7..2efe6945e 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 @@ private function __construct( 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 @@ public static function archive( 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 @@ public static function file( 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 @@ public static function file( * @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 @@ public static function git(string $dirname, array $config, mixed $extract = null * @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 @@ public function withVersion(string $version): self $this->extract, $this->verified, $version, - $this->metadata + $this->metadata, + $this->downloader, ); } @@ -154,7 +159,8 @@ public function withMeta(string $key, mixed $value): self $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 30942fe17..2ecc48dff 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/BitBucketTag.php +++ b/src/StaticPHP/Artifact/Downloader/Type/BitBucketTag.php @@ -36,6 +36,6 @@ public function download(string $name, array $config, ArtifactDownloader $downlo $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 000000000..184456484 --- /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 @@ public function download(string $name, array $config, ArtifactDownloader $downlo 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 83c236eb4..f518b396c 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/Git.php +++ b/src/StaticPHP/Artifact/Downloader/Type/Git.php @@ -10,7 +10,7 @@ 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 @@ public function download(string $name, array $config, ArtifactDownloader $downlo // 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 @@ public function download(string $name, array $config, ArtifactDownloader $downlo $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 7b0412886..15626089a 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php @@ -9,7 +9,7 @@ use StaticPHP\Exception\DownloaderException; /** ghrel */ -class GitHubRelease implements DownloadTypeInterface, ValidatorInterface +class GitHubRelease implements DownloadTypeInterface, ValidatorInterface, CheckUpdateInterface { use GitHubTokenSetupTrait; @@ -48,6 +48,7 @@ public function getGitHubReleases(string $name, string $repo, bool $prefer_stabl */ 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 @@ public function download(string $name, array $config, ArtifactDownloader $downlo $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 @@ public function validate(string $name, array $config, ArtifactDownloader $downlo 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 8aa1ac694..a9283722f 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php @@ -10,7 +10,7 @@ /** ghtar */ /** ghtagtar */ -class GitHubTarball implements DownloadTypeInterface +class GitHubTarball implements DownloadTypeInterface, CheckUpdateInterface { use GitHubTokenSetupTrait; @@ -77,6 +77,22 @@ public function download(string $name, array $config, ArtifactDownloader $downlo [$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 c5cbb3b50..11caa19db 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 @@ public function download(string $name, array $config, ArtifactDownloader $downlo $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 93315ce3a..77ac3d093 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 e4f1a1173..3a3ccc02e 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/PIE.php +++ b/src/StaticPHP/Artifact/Downloader/Type/PIE.php @@ -9,11 +9,42 @@ 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 + { + $first = $this->fetchPackagistInfo($name, $config, $downloader); + // get download link from dist + $dist_url = $first['dist']['url'] ?? null; + $dist_type = $first['dist']['type'] ?? null; + if (!$dist_url || !$dist_type) { + throw new DownloaderException("failed to find {$name} dist info from packagist"); + } + $name = str_replace('/', '_', $config['repo']); + $version = $first['version'] ?? 'unknown'; + $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, 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}"); @@ -25,23 +56,10 @@ public function download(string $name, array $config, ArtifactDownloader $downlo 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"); } - // get download link from dist - $dist_url = $first['dist']['url'] ?? null; - $dist_type = $first['dist']['type'] ?? null; - if (!$dist_url || !$dist_type) { - throw new DownloaderException("failed to find {$name} dist info from packagist"); - } - $name = str_replace('/', '_', $config['repo']); - $version = $first['version'] ?? 'unknown'; - $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 $first; } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php b/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php index ec6c33fa4..372c7f50a 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php +++ b/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php @@ -8,7 +8,7 @@ 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 @@ public function download(string $name, array $config, ArtifactDownloader $downlo $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 @@ public function download(string $name, array $config, ArtifactDownloader $downlo 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 @@ public function validate(string $name, array $config, ArtifactDownloader $downlo 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 a56f4dc71..02425fe5d 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/Url.php +++ b/src/StaticPHP/Artifact/Downloader/Type/Url.php @@ -18,6 +18,6 @@ public function download(string $name, array $config, ArtifactDownloader $downlo 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 000000000..965fb2018 --- /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 023ddf840..a02b38c7d 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -6,6 +6,7 @@ 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 @@ public function __construct() 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 20cf9395e..9dddc9102 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 @@ public static function handleSPCException(SPCException $e): int }; 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 @@ private static function printModuleErrorInfo(SPCException $e): void self::printArrayInfo($info); } - self::logError("---------------------------------------------------------\n", color: 'none'); + self::logError("-----------------------------------------------------------\n", color: 'none'); } } From 40e36982d342a7abcaba4277f8ebee5ee9a52c02 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 13:55:52 +0800 Subject: [PATCH 03/21] Add custom binary check-update support for artifacts --- src/Package/Artifact/go_xcaddy.php | 21 ++++++++++++++ src/Package/Artifact/zig.php | 29 +++++++++++++++++++ src/StaticPHP/Artifact/Artifact.php | 21 ++++++++++++++ src/StaticPHP/Artifact/ArtifactDownloader.php | 9 ++++-- .../Artifact/CustomBinaryCheckUpdate.php | 11 +++++++ src/StaticPHP/Registry/ArtifactLoader.php | 22 ++++++++++++++ 6 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 src/StaticPHP/Attribute/Artifact/CustomBinaryCheckUpdate.php diff --git a/src/Package/Artifact/go_xcaddy.php b/src/Package/Artifact/go_xcaddy.php index 5fa7327e3..056a10e38 100644 --- a/src/Package/Artifact/go_xcaddy.php +++ b/src/Package/Artifact/go_xcaddy.php @@ -6,8 +6,10 @@ use StaticPHP\Artifact\ArtifactDownloader; use StaticPHP\Artifact\Downloader\DownloadResult; +use StaticPHP\Artifact\Downloader\Type\CheckUpdateResult; use StaticPHP\Attribute\Artifact\AfterBinaryExtract; use StaticPHP\Attribute\Artifact\CustomBinary; +use StaticPHP\Attribute\Artifact\CustomBinaryCheckUpdate; use StaticPHP\Exception\DownloaderException; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\GlobalEnvManager; @@ -65,6 +67,25 @@ public function downBinary(ArtifactDownloader $downloader): DownloadResult return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $version], extract: "{$pkgroot}/go-xcaddy", verified: true, version: $version); } + #[CustomBinaryCheckUpdate('go-xcaddy', [ + 'linux-x86_64', + 'linux-aarch64', + 'macos-x86_64', + 'macos-aarch64', + ])] + public function checkUpdateBinary(?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + [$version] = explode("\n", default_shell()->executeCurl('https://go.dev/VERSION?m=text') ?: ''); + if ($version === '') { + throw new \RuntimeException('Failed to get latest Go version from https://go.dev/VERSION?m=text'); + } + return new CheckUpdateResult( + old: $old_version, + new: $version, + needUpdate: $old_version === null || $version !== $old_version, + ); + } + #[AfterBinaryExtract('go-xcaddy', [ 'linux-x86_64', 'linux-aarch64', diff --git a/src/Package/Artifact/zig.php b/src/Package/Artifact/zig.php index 9a1430637..0d334e5f1 100644 --- a/src/Package/Artifact/zig.php +++ b/src/Package/Artifact/zig.php @@ -6,8 +6,10 @@ use StaticPHP\Artifact\ArtifactDownloader; use StaticPHP\Artifact\Downloader\DownloadResult; +use StaticPHP\Artifact\Downloader\Type\CheckUpdateResult; use StaticPHP\Attribute\Artifact\AfterBinaryExtract; use StaticPHP\Attribute\Artifact\CustomBinary; +use StaticPHP\Attribute\Artifact\CustomBinaryCheckUpdate; use StaticPHP\Exception\DownloaderException; use StaticPHP\Runtime\SystemTarget; @@ -59,6 +61,33 @@ public function downBinary(ArtifactDownloader $downloader): DownloadResult return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $latest_version], extract: PKG_ROOT_PATH . '/zig', verified: true, version: $latest_version); } + #[CustomBinaryCheckUpdate('zig', [ + 'linux-x86_64', + 'linux-aarch64', + 'macos-x86_64', + 'macos-aarch64', + ])] + public function checkUpdateBinary(?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + $index_json = default_shell()->executeCurl('https://ziglang.org/download/index.json', retries: $downloader->getRetry()); + $index_json = json_decode($index_json ?: '', true); + $latest_version = null; + foreach ($index_json as $version => $data) { + if ($version !== 'master') { + $latest_version = $version; + break; + } + } + if (!$latest_version) { + throw new DownloaderException('Could not determine latest Zig version'); + } + return new CheckUpdateResult( + old: $old_version, + new: $latest_version, + needUpdate: $old_version === null || version_compare($latest_version, $old_version, '>'), + ); + } + #[AfterBinaryExtract('zig', [ 'linux-x86_64', 'linux-aarch64', diff --git a/src/StaticPHP/Artifact/Artifact.php b/src/StaticPHP/Artifact/Artifact.php index 6dc35ad58..8bdd86a85 100644 --- a/src/StaticPHP/Artifact/Artifact.php +++ b/src/StaticPHP/Artifact/Artifact.php @@ -30,6 +30,9 @@ class Artifact /** @var array Bind custom binary fetcher callbacks */ protected mixed $custom_binary_callbacks = []; + /** @var array Bind custom binary check-update callbacks */ + protected array $custom_binary_check_update_callbacks = []; + /** @var null|callable Bind custom source extract callback (completely takes over extraction) */ protected mixed $source_extract_callback = null; @@ -433,6 +436,24 @@ public function setCustomBinaryCallback(string $target_os, callable $callback): $this->custom_binary_callbacks[$target_os] = $callback; } + /** + * Set custom binary check-update callback for a specific target OS. + * + * @param string $target_os Target OS platform string (e.g. linux-x86_64) + * @param callable $callback Custom binary check-update callback + */ + public function setCustomBinaryCheckUpdateCallback(string $target_os, callable $callback): void + { + ConfigValidator::validatePlatformString($target_os); + $this->custom_binary_check_update_callbacks[$target_os] = $callback; + } + + public function getCustomBinaryCheckUpdateCallback(): ?callable + { + $current_platform = SystemTarget::getCurrentPlatformString(); + return $this->custom_binary_check_update_callbacks[$current_platform] ?? null; + } + // ==================== Extraction Callbacks ==================== /** diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index b2773c808..7740d27e5 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -358,8 +358,13 @@ public function checkUpdate(string $artifact_name, bool $prefer_source = false, /** @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."); + } // custom binary: delegate to registered check-update callback + if (($callback = $artifact->getCustomBinaryCheckUpdateCallback()) !== null) { + return ApplicationContext::invoke($callback, [ + ArtifactDownloader::class => $this, + 'old_version' => $info['version'], + ]); + } throw new WrongUsageException("Artifact '{$artifact_name}' downloader does not support update checking, exit."); } public function getRetry(): int diff --git a/src/StaticPHP/Attribute/Artifact/CustomBinaryCheckUpdate.php b/src/StaticPHP/Attribute/Artifact/CustomBinaryCheckUpdate.php new file mode 100644 index 000000000..aa59af1a2 --- /dev/null +++ b/src/StaticPHP/Attribute/Artifact/CustomBinaryCheckUpdate.php @@ -0,0 +1,11 @@ +getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { self::processCustomSourceAttribute($ref, $method, $class_instance); self::processCustomBinaryAttribute($ref, $method, $class_instance); + self::processCustomBinaryCheckUpdateAttribute($ref, $method, $class_instance); self::processSourceExtractAttribute($ref, $method, $class_instance); self::processBinaryExtractAttribute($ref, $method, $class_instance); self::processAfterSourceExtractAttribute($ref, $method, $class_instance); @@ -118,6 +120,26 @@ private static function processCustomBinaryAttribute(\ReflectionClass $ref, \Ref } } + /** + * Process #[CustomBinaryCheckUpdate] attribute. + */ + private static function processCustomBinaryCheckUpdateAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void + { + $attributes = $method->getAttributes(CustomBinaryCheckUpdate::class); + foreach ($attributes as $attribute) { + /** @var CustomBinaryCheckUpdate $instance */ + $instance = $attribute->newInstance(); + $artifact_name = $instance->artifact_name; + if (isset(self::$artifacts[$artifact_name])) { + foreach ($instance->support_os as $os) { + self::$artifacts[$artifact_name]->setCustomBinaryCheckUpdateCallback($os, [$class_instance, $method->getName()]); + } + } else { + throw new ValidationException("Artifact '{$artifact_name}' not found for #[CustomBinaryCheckUpdate] on '{$ref->getName()}::{$method->getName()}'"); + } + } + } + /** * Process #[SourceExtract] attribute. * This attribute allows completely taking over the source extraction process. From 550f6cad6048ec5dea7c7752d73d0cc1ac0a7fdb Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 14:02:32 +0800 Subject: [PATCH 04/21] Replace RuntimeException with DownloaderException for Go version retrieval failure --- src/Package/Artifact/go_xcaddy.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Package/Artifact/go_xcaddy.php b/src/Package/Artifact/go_xcaddy.php index 056a10e38..ca61bd465 100644 --- a/src/Package/Artifact/go_xcaddy.php +++ b/src/Package/Artifact/go_xcaddy.php @@ -77,7 +77,7 @@ public function checkUpdateBinary(?string $old_version, ArtifactDownloader $down { [$version] = explode("\n", default_shell()->executeCurl('https://go.dev/VERSION?m=text') ?: ''); if ($version === '') { - throw new \RuntimeException('Failed to get latest Go version from https://go.dev/VERSION?m=text'); + throw new DownloaderException('Failed to get latest Go version from https://go.dev/VERSION?m=text'); } return new CheckUpdateResult( old: $old_version, From 0a07f6b27cfc1454f9eb3e51cb656521d66b50f2 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 14:07:05 +0800 Subject: [PATCH 05/21] cs fix --- src/StaticPHP/Artifact/ArtifactDownloader.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index 7740d27e5..219dfb8b2 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -364,7 +364,8 @@ public function checkUpdate(string $artifact_name, bool $prefer_source = false, ArtifactDownloader::class => $this, 'old_version' => $info['version'], ]); - } throw new WrongUsageException("Artifact '{$artifact_name}' downloader does not support update checking, exit."); + } + throw new WrongUsageException("Artifact '{$artifact_name}' downloader does not support update checking, exit."); } public function getRetry(): int From a7b04d908144c2a17e6fc65d82cfe7f1815852ef Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Sat, 28 Feb 2026 14:16:57 +0800 Subject: [PATCH 06/21] Update src/StaticPHP/Artifact/Downloader/Type/Git.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/StaticPHP/Artifact/Downloader/Type/Git.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/StaticPHP/Artifact/Downloader/Type/Git.php b/src/StaticPHP/Artifact/Downloader/Type/Git.php index f518b396c..4a7120052 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/Git.php +++ b/src/StaticPHP/Artifact/Downloader/Type/Git.php @@ -21,7 +21,8 @@ public function download(string $name, array $config, ArtifactDownloader $downlo // direct branch clone if (isset($config['rev'])) { default_shell()->executeGitClone($config['url'], $config['rev'], $path, $shallow, $config['submodules'] ?? null); - $hash_result = shell(false)->execWithResult(SPC_GIT_EXEC . ' -C ' . escapeshellarg($path) . ' rev-parse HEAD'); + $shell = PHP_OS_FAMILY === 'Windows' ? cmd(false) : shell(false); + $hash_result = $shell->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); From 64b0e7290873ee1ec5df1452ec4d8049882b8c87 Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Sat, 28 Feb 2026 14:17:48 +0800 Subject: [PATCH 07/21] Update src/StaticPHP/Artifact/Downloader/Type/PIE.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/StaticPHP/Artifact/Downloader/Type/PIE.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticPHP/Artifact/Downloader/Type/PIE.php b/src/StaticPHP/Artifact/Downloader/Type/PIE.php index 3a3ccc02e..a84cffe51 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/PIE.php +++ b/src/StaticPHP/Artifact/Downloader/Type/PIE.php @@ -27,7 +27,7 @@ public function download(string $name, array $config, ArtifactDownloader $downlo $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, downloader: static::class); + 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 From 6ef5e9e067f9fb21169efbb230dc3a1b47668915 Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Sat, 28 Feb 2026 14:18:32 +0800 Subject: [PATCH 08/21] Update src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php b/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php index 184456484..1adcdfeae 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php +++ b/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php @@ -13,7 +13,7 @@ interface CheckUpdateInterface * * @param string $name the name of the artifact * @param array $config the configuration for the artifact - * @param string $old_version old version or identifier of the artifact to compare against + * @param null|string $old_version old version or identifier of the artifact to compare against * @param ArtifactDownloader $downloader the artifact downloader instance */ public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult; From 28f4a5c52387e40d02112fdc9fcc128ed03c56e2 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 14:35:48 +0800 Subject: [PATCH 09/21] Add support for custom source check-update callbacks in artifacts --- src/StaticPHP/Artifact/Artifact.php | 16 +++++ src/StaticPHP/Artifact/ArtifactDownloader.php | 71 ++++++++++++++++--- .../Artifact/CustomSourceCheckUpdate.php | 11 +++ src/StaticPHP/Registry/ArtifactLoader.php | 20 ++++++ 4 files changed, 107 insertions(+), 11 deletions(-) create mode 100644 src/StaticPHP/Attribute/Artifact/CustomSourceCheckUpdate.php diff --git a/src/StaticPHP/Artifact/Artifact.php b/src/StaticPHP/Artifact/Artifact.php index 8bdd86a85..841775e36 100644 --- a/src/StaticPHP/Artifact/Artifact.php +++ b/src/StaticPHP/Artifact/Artifact.php @@ -27,6 +27,9 @@ class Artifact /** @var null|callable Bind custom source fetcher callback */ protected mixed $custom_source_callback = null; + /** @var null|callable Bind custom source check-update callback */ + protected mixed $custom_source_check_update_callback = null; + /** @var array Bind custom binary fetcher callbacks */ protected mixed $custom_binary_callbacks = []; @@ -408,6 +411,19 @@ public function getCustomSourceCallback(): ?callable return $this->custom_source_callback ?? null; } + /** + * Set custom source check-update callback. + */ + public function setCustomSourceCheckUpdateCallback(callable $callback): void + { + $this->custom_source_check_update_callback = $callback; + } + + public function getCustomSourceCheckUpdateCallback(): ?callable + { + return $this->custom_source_check_update_callback ?? null; + } + public function getCustomBinaryCallback(): ?callable { $current_platform = SystemTarget::getCurrentPlatformString(); diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index 219dfb8b2..86c06c19f 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -332,17 +332,14 @@ public function checkUpdate(string $artifact_name, bool $prefer_source = false, 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."); + [$first, $second] = $prefer_source + ? [fn () => $this->probeSourceCheckUpdate($artifact, $artifact_name), fn () => $this->probeBinaryCheckUpdate($artifact, $artifact_name)] + : [fn () => $this->probeBinaryCheckUpdate($artifact, $artifact_name), fn () => $this->probeSourceCheckUpdate($artifact, $artifact_name)]; + $result = $first() ?? $second(); + if ($result !== null) { + return $result; } - $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); + throw new WrongUsageException("Artifact '{$artifact_name}' downloader does not support update checking."); } $cache = ApplicationContext::get(ArtifactCache::class); if ($prefer_source) { @@ -358,7 +355,15 @@ public function checkUpdate(string $artifact_name, bool $prefer_source = false, /** @var CheckUpdateInterface $downloader */ $downloader = new $cls(); return $downloader->checkUpdate($artifact_name, $info['config'], $info['version'], $this); - } // custom binary: delegate to registered check-update callback + } + // custom source: delegate to registered check-update callback + if (($info['lock_type'] ?? null) === 'source' && ($callback = $artifact->getCustomSourceCheckUpdateCallback()) !== null) { + return ApplicationContext::invoke($callback, [ + ArtifactDownloader::class => $this, + 'old_version' => $info['version'], + ]); + } + // custom binary: delegate to registered check-update callback if (($callback = $artifact->getCustomBinaryCheckUpdateCallback()) !== null) { return ApplicationContext::invoke($callback, [ ArtifactDownloader::class => $this, @@ -383,6 +388,50 @@ public function getOption(string $name, mixed $default = null): mixed return $this->options[$name] ?? $default; } + private function probeSourceCheckUpdate(Artifact $artifact, string $artifact_name): ?CheckUpdateResult + { + if (($callback = $artifact->getCustomSourceCheckUpdateCallback()) !== null) { + return ApplicationContext::invoke($callback, [ + ArtifactDownloader::class => $this, + 'old_version' => null, + ]); + } + $config = $artifact->getDownloadConfig('source'); + if (!is_array($config)) { + return null; + } + $cls = $this->downloaders[$config['type']] ?? null; + if (!is_a($cls, CheckUpdateInterface::class, true)) { + return null; + } + /** @var CheckUpdateInterface $dl */ + $dl = new $cls(); + return $dl->checkUpdate($artifact_name, $config, null, $this); + } + + private function probeBinaryCheckUpdate(Artifact $artifact, string $artifact_name): ?CheckUpdateResult + { + // custom binary callback takes precedence over config-based binary + if (($callback = $artifact->getCustomBinaryCheckUpdateCallback()) !== null) { + return ApplicationContext::invoke($callback, [ + ArtifactDownloader::class => $this, + 'old_version' => null, + ]); + } + $binary_config = $artifact->getDownloadConfig('binary'); + $platform_config = is_array($binary_config) ? ($binary_config[SystemTarget::getCurrentPlatformString()] ?? null) : null; + if (!is_array($platform_config)) { + return null; + } + $cls = $this->downloaders[$platform_config['type']] ?? null; + if (!is_a($cls, CheckUpdateInterface::class, true)) { + return null; + } + /** @var CheckUpdateInterface $dl */ + $dl = new $cls(); + return $dl->checkUpdate($artifact_name, $platform_config, null, $this); + } + private function downloadWithType(Artifact $artifact, int $current, int $total, bool $parallel = false, bool $interactive = true): int { $queue = $this->generateQueue($artifact); diff --git a/src/StaticPHP/Attribute/Artifact/CustomSourceCheckUpdate.php b/src/StaticPHP/Attribute/Artifact/CustomSourceCheckUpdate.php new file mode 100644 index 000000000..df6e07d65 --- /dev/null +++ b/src/StaticPHP/Attribute/Artifact/CustomSourceCheckUpdate.php @@ -0,0 +1,11 @@ +getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { self::processCustomSourceAttribute($ref, $method, $class_instance); + self::processCustomSourceCheckUpdateAttribute($ref, $method, $class_instance); self::processCustomBinaryAttribute($ref, $method, $class_instance); self::processCustomBinaryCheckUpdateAttribute($ref, $method, $class_instance); self::processSourceExtractAttribute($ref, $method, $class_instance); @@ -100,6 +102,24 @@ private static function processCustomSourceAttribute(\ReflectionClass $ref, \Ref } } + /** + * Process #[CustomSourceCheckUpdate] attribute. + */ + private static function processCustomSourceCheckUpdateAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void + { + $attributes = $method->getAttributes(CustomSourceCheckUpdate::class); + foreach ($attributes as $attribute) { + /** @var CustomSourceCheckUpdate $instance */ + $instance = $attribute->newInstance(); + $artifact_name = $instance->artifact_name; + if (isset(self::$artifacts[$artifact_name])) { + self::$artifacts[$artifact_name]->setCustomSourceCheckUpdateCallback([$class_instance, $method->getName()]); + } else { + throw new ValidationException("Artifact '{$artifact_name}' not found for #[CustomSourceCheckUpdate] on '{$ref->getName()}::{$method->getName()}'"); + } + } + } + /** * Process #[CustomBinary] attribute. */ From 029f8efa120672547e40862b7c2d0fc9b5ce5739 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 14:55:18 +0800 Subject: [PATCH 10/21] Avoid empty output --- src/StaticPHP/Command/CheckUpdateCommand.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/StaticPHP/Command/CheckUpdateCommand.php b/src/StaticPHP/Command/CheckUpdateCommand.php index 965fb2018..9a2d5f5cc 100644 --- a/src/StaticPHP/Command/CheckUpdateCommand.php +++ b/src/StaticPHP/Command/CheckUpdateCommand.php @@ -51,8 +51,9 @@ public function handle(): int $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}"); + [$old, $new] = [$result->old ?? 'unavailable', $result->new ?? 'unknown']; + $this->output->writeln(" Old version: {$old}"); + $this->output->writeln(" New version: {$new}"); } } return static::OK; From 4f2ca17bde89314b6fbadb41f059aa2321dfd520 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 15:16:31 +0800 Subject: [PATCH 11/21] cs fix --- src/StaticPHP/Artifact/ArtifactDownloader.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index 86c06c19f..b5e540786 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -356,14 +356,14 @@ public function checkUpdate(string $artifact_name, bool $prefer_source = false, $downloader = new $cls(); return $downloader->checkUpdate($artifact_name, $info['config'], $info['version'], $this); } - // custom source: delegate to registered check-update callback + if (($info['lock_type'] ?? null) === 'source' && ($callback = $artifact->getCustomSourceCheckUpdateCallback()) !== null) { return ApplicationContext::invoke($callback, [ ArtifactDownloader::class => $this, 'old_version' => $info['version'], ]); } - // custom binary: delegate to registered check-update callback + if (($callback = $artifact->getCustomBinaryCheckUpdateCallback()) !== null) { return ApplicationContext::invoke($callback, [ ArtifactDownloader::class => $this, From 174ef3dba7dcae8f87adc02defc6a21e3f985f87 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 15:36:04 +0800 Subject: [PATCH 12/21] Refactor ReturnCode constants for clarity and consistency --- src/StaticPHP/Command/ReturnCode.php | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/StaticPHP/Command/ReturnCode.php b/src/StaticPHP/Command/ReturnCode.php index d152101ef..5accebc3a 100644 --- a/src/StaticPHP/Command/ReturnCode.php +++ b/src/StaticPHP/Command/ReturnCode.php @@ -17,26 +17,28 @@ trait ReturnCode { public const int OK = 0; - public const SUCCESS = 0; // alias of OK + public const SUCCESS = 0; // alias - public const int INTERNAL_ERROR = 1; // unsorted or internal error + public const FAILURE = 1; // generic failure - /** @deprecated Use specified error code instead */ - public const FAILURE = 1; + // 64-69: reserved for standard errors + public const int USER_ERROR = 64; // wrong usage, bad arguments - public const int USER_ERROR = 2; // wrong usage or user error + public const int VALIDATION_ERROR = 65; // invalid input or config values - public const int ENVIRONMENT_ERROR = 3; // environment not suitable for operation + public const int ENVIRONMENT_ERROR = 69; // required tools/env not available - public const int VALIDATION_ERROR = 4; // validation failed + // 70+: application-specific errors + public const int INTERNAL_ERROR = 70; // internal logic error or unexpected state - public const int FILE_SYSTEM_ERROR = 5; // file system related error + public const int BUILD_ERROR = 72; // build / compile process failed - public const int DOWNLOAD_ERROR = 6; // network related error + public const int PATCH_ERROR = 73; // patching or modifying files failed - public const int BUILD_ERROR = 7; // build process error + public const int FILE_SYSTEM_ERROR = 74; // filesystem / IO error - public const int PATCH_ERROR = 8; // patching process error + public const int DOWNLOAD_ERROR = 75; // network / remote resource error - public const int INTERRUPT_SIGNAL = 130; // process interrupted by user (e.g., Ctrl+C) + // 128+: reserved for standard signals and interrupts + public const int INTERRUPT_SIGNAL = 130; // SIGINT (Ctrl+C) } From dc0a80975f9d878a13c3d3428d363b16571287a7 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 17:07:24 +0800 Subject: [PATCH 13/21] Add PECL download type and support for PECL artifacts --- config/downloader.php | 2 + config/pkg/ext/builtin-extensions.yml | 29 +++++++ config/pkg/ext/ext-amqp.yml | 6 +- config/pkg/ext/ext-apcu.yml | 6 +- config/pkg/ext/ext-ast.yml | 6 ++ config/pkg/ext/ext-mbregex.yml | 10 --- config/pkg/ext/ext-mbstring.yml | 4 - config/pkg/ext/ext-phar.yml | 4 - config/pkg/ext/ext-readline.yml | 11 --- src/StaticPHP/Artifact/ArtifactCache.php | 4 +- .../Artifact/Downloader/Type/Git.php | 4 +- .../Artifact/Downloader/Type/PECL.php | 79 +++++++++++++++++++ src/StaticPHP/Config/ConfigValidator.php | 1 + src/StaticPHP/Runtime/Shell/DefaultShell.php | 9 ++- 14 files changed, 130 insertions(+), 45 deletions(-) create mode 100644 config/pkg/ext/ext-ast.yml delete mode 100644 config/pkg/ext/ext-mbregex.yml delete mode 100644 config/pkg/ext/ext-mbstring.yml delete mode 100644 config/pkg/ext/ext-phar.yml delete mode 100644 config/pkg/ext/ext-readline.yml create mode 100644 src/StaticPHP/Artifact/Downloader/Type/PECL.php diff --git a/config/downloader.php b/config/downloader.php index 48710a888..2d81a57e3 100644 --- a/config/downloader.php +++ b/config/downloader.php @@ -10,6 +10,7 @@ use StaticPHP\Artifact\Downloader\Type\GitHubTarball; use StaticPHP\Artifact\Downloader\Type\HostedPackageBin; use StaticPHP\Artifact\Downloader\Type\LocalDir; +use StaticPHP\Artifact\Downloader\Type\PECL; use StaticPHP\Artifact\Downloader\Type\PhpRelease; use StaticPHP\Artifact\Downloader\Type\PIE; use StaticPHP\Artifact\Downloader\Type\Url; @@ -24,6 +25,7 @@ 'ghtagtar' => GitHubTarball::class, 'local' => LocalDir::class, 'pie' => PIE::class, + 'pecl' => PECL::class, 'url' => Url::class, 'php-release' => PhpRelease::class, 'hosted' => HostedPackageBin::class, diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index d12cd2191..0149fe974 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -1,5 +1,19 @@ ext-bcmath: type: php-extension +ext-mbregex: + type: php-extension + depends: + - onig + - ext-mbstring + php-extension: + arg-type: custom + build-shared: false + build-static: true + display-name: mbstring +ext-mbstring: + type: php-extension + php-extension: + arg-type: custom ext-openssl: type: php-extension depends: @@ -10,6 +24,21 @@ ext-openssl: arg-type: custom arg-type@windows: with build-with-php: true +ext-phar: + type: php-extension + depends: + - zlib +ext-readline: + type: php-extension + depends: + - libedit + php-extension: + support: + Windows: wip + BSD: wip + arg-type: with-path + build-shared: false + build-static: true ext-zlib: type: php-extension depends: diff --git a/config/pkg/ext/ext-amqp.yml b/config/pkg/ext/ext-amqp.yml index 937569144..1c8023602 100644 --- a/config/pkg/ext/ext-amqp.yml +++ b/config/pkg/ext/ext-amqp.yml @@ -2,10 +2,8 @@ ext-amqp: type: php-extension artifact: source: - type: url - url: 'https://pecl.php.net/get/amqp' - extract: php-src/ext/amqp - filename: amqp.tgz + type: pecl + name: amqp metadata: license-files: [LICENSE] license: PHP-3.01 diff --git a/config/pkg/ext/ext-apcu.yml b/config/pkg/ext/ext-apcu.yml index 289de301c..331b04f76 100644 --- a/config/pkg/ext/ext-apcu.yml +++ b/config/pkg/ext/ext-apcu.yml @@ -2,10 +2,8 @@ ext-apcu: type: php-extension artifact: source: - type: url - url: 'https://pecl.php.net/get/APCu' - extract: php-src/ext/apcu - filename: apcu.tgz + type: pecl + name: APCu metadata: license-files: [LICENSE] license: PHP-3.01 diff --git a/config/pkg/ext/ext-ast.yml b/config/pkg/ext/ext-ast.yml new file mode 100644 index 000000000..0684959dd --- /dev/null +++ b/config/pkg/ext/ext-ast.yml @@ -0,0 +1,6 @@ +ext-ast: + type: php-extension + artifact: + source: + type: pecl + name: ast diff --git a/config/pkg/ext/ext-mbregex.yml b/config/pkg/ext/ext-mbregex.yml deleted file mode 100644 index ae59f0235..000000000 --- a/config/pkg/ext/ext-mbregex.yml +++ /dev/null @@ -1,10 +0,0 @@ -ext-mbregex: - type: php-extension - depends: - - onig - - ext-mbstring - php-extension: - arg-type: custom - build-shared: false - build-static: true - display-name: mbstring diff --git a/config/pkg/ext/ext-mbstring.yml b/config/pkg/ext/ext-mbstring.yml deleted file mode 100644 index 6583ca616..000000000 --- a/config/pkg/ext/ext-mbstring.yml +++ /dev/null @@ -1,4 +0,0 @@ -ext-mbstring: - type: php-extension - php-extension: - arg-type: custom diff --git a/config/pkg/ext/ext-phar.yml b/config/pkg/ext/ext-phar.yml deleted file mode 100644 index 3625d2c00..000000000 --- a/config/pkg/ext/ext-phar.yml +++ /dev/null @@ -1,4 +0,0 @@ -ext-phar: - type: php-extension - depends: - - zlib diff --git a/config/pkg/ext/ext-readline.yml b/config/pkg/ext/ext-readline.yml deleted file mode 100644 index 19b1886c8..000000000 --- a/config/pkg/ext/ext-readline.yml +++ /dev/null @@ -1,11 +0,0 @@ -ext-readline: - type: php-extension - depends: - - libedit - php-extension: - support: - Windows: wip - BSD: wip - arg-type: with-path - build-shared: false - build-static: true diff --git a/src/StaticPHP/Artifact/ArtifactCache.php b/src/StaticPHP/Artifact/ArtifactCache.php index dcd75ef7d..ea3bde412 100644 --- a/src/StaticPHP/Artifact/ArtifactCache.php +++ b/src/StaticPHP/Artifact/ArtifactCache.php @@ -163,7 +163,7 @@ public function lock(Artifact|string $artifact, string $lock_type, DownloadResul throw new SPCInternalException("Invalid lock type '{$lock_type}' for artifact {$artifact_name}"); } // save cache to file - file_put_contents($this->cache_file, json_encode($this->cache, JSON_PRETTY_PRINT)); + file_put_contents($this->cache_file, json_encode($this->cache, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } /** @@ -281,7 +281,7 @@ public function removeBinary(string $artifact_name, string $platform, bool $dele */ public function save(): void { - file_put_contents($this->cache_file, json_encode($this->cache, JSON_PRETTY_PRINT)); + file_put_contents($this->cache_file, json_encode($this->cache, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } private function isObjectDownloaded(?array $object, bool $compare_hash = false): bool diff --git a/src/StaticPHP/Artifact/Downloader/Type/Git.php b/src/StaticPHP/Artifact/Downloader/Type/Git.php index 4a7120052..1ee9da4da 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/Git.php +++ b/src/StaticPHP/Artifact/Downloader/Type/Git.php @@ -22,7 +22,7 @@ public function download(string $name, array $config, ArtifactDownloader $downlo if (isset($config['rev'])) { default_shell()->executeGitClone($config['url'], $config['rev'], $path, $shallow, $config['submodules'] ?? null); $shell = PHP_OS_FAMILY === 'Windows' ? cmd(false) : shell(false); - $hash_result = $shell->execWithResult(SPC_GIT_EXEC . ' -C ' . escapeshellarg($path) . ' rev-parse HEAD'); + $hash_result = $shell->execWithResult(SPC_GIT_EXEC . ' -C ' . escapeshellarg($path) . ' rev-parse --short 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); @@ -80,7 +80,7 @@ public function checkUpdate(string $name, array $config, ?string $old_version, A 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_hash = substr($result[1][0], 0, 7); $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; diff --git a/src/StaticPHP/Artifact/Downloader/Type/PECL.php b/src/StaticPHP/Artifact/Downloader/Type/PECL.php new file mode 100644 index 000000000..0b14b05d4 --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/PECL.php @@ -0,0 +1,79 @@ +VERSIONSTATE per release */ + private const string PECL_REST_URL = 'https://pecl.php.net/rest/r/%s/allreleases.xml'; + + public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + [, $version] = $this->fetchPECLInfo($name, $config, $downloader); + return new CheckUpdateResult( + old: $old_version, + new: $version, + needUpdate: $old_version === null || version_compare($version, $old_version, '>'), + ); + } + + public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult + { + [$filename, $version] = $this->fetchPECLInfo($name, $config, $downloader); + $url = self::PECL_BASE_URL . '/get/' . $filename; + $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; + logger()->debug("Downloading {$name} from URL: {$url}"); + default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); + $extract = $config['extract'] ?? ('php-src/ext/' . $this->getExtractName($name)); + return DownloadResult::archive($filename, $config, $extract, version: $version, downloader: static::class); + } + + protected function fetchPECLInfo(string $name, array $config, ArtifactDownloader $downloader): array + { + $peclName = strtolower($config['name'] ?? $this->getExtractName($name)); + $url = sprintf(self::PECL_REST_URL, $peclName); + logger()->debug("Fetching PECL release list for {$name} from REST API"); + $xml = default_shell()->executeCurl($url, retries: $downloader->getRetry()); + if ($xml === false) { + throw new DownloaderException("Failed to fetch PECL release list for {$name}"); + } + // Match VERSIONSTATE + preg_match_all('/(?P[^<]+)<\/v>(?P[^<]+)<\/s><\/r>/', $xml, $matches); + if (empty($matches['version'])) { + throw new DownloaderException("Failed to parse PECL release list for {$name}"); + } + $versions = []; + logger()->debug('Matched ' . count($matches['version']) . " releases for {$name} from PECL"); + foreach ($matches['version'] as $i => $version) { + if ($matches['state'][$i] !== 'stable') { + continue; + } + $versions[$version] = $peclName . '-' . $version . '.tgz'; + } + if (empty($versions)) { + throw new DownloaderException("No stable releases found for {$name} on PECL"); + } + uksort($versions, 'version_compare'); + $filename = end($versions); + $version = array_key_last($versions); + return [$filename, $version, $versions]; + } + + /** + * Derive the lowercase PECL package / extract name from the artifact name. + * e.g. "ext-apcu" -> "apcu", "ext-ast" -> "ast" + */ + private function getExtractName(string $name): string + { + return strtolower(preg_replace('/^ext-/i', '', $name)); + } +} diff --git a/src/StaticPHP/Config/ConfigValidator.php b/src/StaticPHP/Config/ConfigValidator.php index fbf883213..83f9ca41c 100644 --- a/src/StaticPHP/Config/ConfigValidator.php +++ b/src/StaticPHP/Config/ConfigValidator.php @@ -89,6 +89,7 @@ class ConfigValidator 'bitbuckettag' => [['repo'], ['extract']], 'local' => [['dirname'], ['extract']], 'pie' => [['repo'], ['extract']], + 'pecl' => [['name'], ['extract']], 'php-release' => [[], ['extract']], 'custom' => [[], ['func']], ]; diff --git a/src/StaticPHP/Runtime/Shell/DefaultShell.php b/src/StaticPHP/Runtime/Shell/DefaultShell.php index 5b50d1528..66dfb7ab0 100644 --- a/src/StaticPHP/Runtime/Shell/DefaultShell.php +++ b/src/StaticPHP/Runtime/Shell/DefaultShell.php @@ -25,7 +25,7 @@ public function exec(string $cmd): static /** * Execute a cURL command to fetch data from a URL. */ - public function executeCurl(string $url, string $method = 'GET', array $headers = [], array $hooks = [], int $retries = 0): false|string + public function executeCurl(string $url, string $method = 'GET', array $headers = [], array $hooks = [], int $retries = 0, bool $compressed = false): false|string { foreach ($hooks as $hook) { $hook($method, $url, $headers); @@ -39,7 +39,8 @@ public function executeCurl(string $url, string $method = 'GET', array $headers }; $header_arg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $headers)); $retry_arg = $retries > 0 ? "--retry {$retries}" : ''; - $cmd = SPC_CURL_EXEC . " -sfSL {$retry_arg} {$method_arg} {$header_arg} {$url_arg}"; + $compressed_arg = $compressed ? '--compressed' : ''; + $cmd = SPC_CURL_EXEC . " -sfSL --max-time 3600 {$retry_arg} {$compressed_arg} {$method_arg} {$header_arg} {$url_arg}"; $this->logCommandInfo($cmd); $result = $this->passthru($cmd, capture_output: true, throw_on_error: false); @@ -72,7 +73,7 @@ public function executeCurlDownload(string $url, string $path, array $headers = $header_arg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $headers)); $retry_arg = $retries > 0 ? "--retry {$retries}" : ''; $check = $this->console_putput ? '#' : 's'; - $cmd = clean_spaces(SPC_CURL_EXEC . " -{$check}fSL {$retry_arg} {$header_arg} -o {$path_arg} {$url_arg}"); + $cmd = clean_spaces(SPC_CURL_EXEC . " -{$check}fSL --max-time 3600 {$retry_arg} {$header_arg} -o {$path_arg} {$url_arg}"); $this->logCommandInfo($cmd); logger()->debug('[CURL DOWNLOAD] ' . $cmd); $this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true); @@ -93,7 +94,7 @@ public function executeGitClone(string $url, string $branch, string $path, bool $path_arg = escapeshellarg($path); $shallow_arg = $shallow ? '--depth 1 --single-branch' : ''; $submodules_arg = ($submodules === null && $shallow) ? '--recursive --shallow-submodules' : ($submodules === null ? '--recursive' : ''); - $cmd = clean_spaces("{$git} clone --config core.autocrlf=false --branch {$branch_arg} {$shallow_arg} {$submodules_arg} {$url_arg} {$path_arg}"); + $cmd = clean_spaces("{$git} clone -c http.lowSpeedLimit=1 -c http.lowSpeedTime=3600 --config core.autocrlf=false --branch {$branch_arg} {$shallow_arg} {$submodules_arg} {$url_arg} {$path_arg}"); $this->logCommandInfo($cmd); logger()->debug("[GIT CLONE] {$cmd}"); $this->passthru($cmd, $this->console_putput); From 12d4009a21c418c6c303125e8901c32b5c79e0bb Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 4 Mar 2026 16:32:16 +0800 Subject: [PATCH 14/21] Update PHP release handling to use configurable mirror and improve URL management --- config/artifact/php-src.yml | 4 +++ .../Artifact/Downloader/Type/PhpRelease.php | 28 +++++++++++++------ src/StaticPHP/Config/ConfigValidator.php | 2 +- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/config/artifact/php-src.yml b/config/artifact/php-src.yml index 32bcb6cf0..e304db9db 100644 --- a/config/artifact/php-src.yml +++ b/config/artifact/php-src.yml @@ -5,3 +5,7 @@ php-src: license: PHP-3.01 source: type: php-release + domain: 'https://www.php.net' + source-mirror: + type: php-release + domain: 'https://phpmirror.static-php.dev' diff --git a/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php b/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php index 372c7f50a..b1fad70e8 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php +++ b/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php @@ -10,9 +10,15 @@ class PhpRelease implements DownloadTypeInterface, ValidatorInterface, CheckUpdateInterface { - public const string PHP_API = 'https://www.php.net/releases/index.php?json&version={version}'; + public const string DEFAULT_PHP_DOMAIN = 'https://www.php.net'; - public const string DOWNLOAD_URL = 'https://www.php.net/distributions/php-{version}.tar.xz'; + public const string API_URL = '/releases/index.php?json&version={version}'; + + public const string DOWNLOAD_URL = '/distributions/php-{version}.tar.xz'; + + public const string GIT_URL = 'https://github.com/php/php-src.git'; + + public const string GIT_REV = 'master'; private ?string $sha256 = ''; @@ -22,9 +28,9 @@ public function download(string $name, array $config, ArtifactDownloader $downlo // Handle 'git' version to clone from php-src repository if ($phpver === 'git') { $this->sha256 = null; - return (new Git())->download($name, ['url' => 'https://github.com/php/php-src.git', 'rev' => 'master'], $downloader); + return (new Git())->download($name, ['url' => self::GIT_URL, 'rev' => self::GIT_REV], $downloader); } - $info = $this->fetchPhpReleaseInfo($name, $downloader); + $info = $this->fetchPhpReleaseInfo($name, $config, $downloader); $version = $info['version']; foreach ($info['source'] as $source) { if (str_ends_with($source['filename'], '.tar.xz')) { @@ -36,7 +42,8 @@ public function download(string $name, array $config, ArtifactDownloader $downlo if (!isset($filename)) { throw new DownloaderException("No suitable source tarball found for PHP version {$version}"); } - $url = str_replace('{version}', $version, self::DOWNLOAD_URL); + $url = $config['domain'] ?? self::DEFAULT_PHP_DOMAIN; + $url .= str_replace('{version}', $version, self::DOWNLOAD_URL); logger()->debug("Downloading PHP release {$version} from {$url}"); $path = DOWNLOAD_PATH . "/{$filename}"; default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); @@ -72,7 +79,7 @@ public function checkUpdate(string $name, array $config, ?string $old_version, A // 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); + $info = $this->fetchPhpReleaseInfo($name, $config, $downloader); $new_version = $info['version']; return new CheckUpdateResult( old: $old_version, @@ -81,7 +88,7 @@ public function checkUpdate(string $name, array $config, ?string $old_version, A ); } - protected function fetchPhpReleaseInfo(string $name, ArtifactDownloader $downloader): array + protected function fetchPhpReleaseInfo(string $name, array $config, ArtifactDownloader $downloader): array { $phpver = $downloader->getOption('with-php', '8.4'); // Handle 'git' version to clone from php-src repository @@ -90,8 +97,13 @@ protected function fetchPhpReleaseInfo(string $name, ArtifactDownloader $downloa throw new DownloaderException("Cannot fetch PHP release info for 'git' version."); } + $url = $config['domain'] ?? self::DEFAULT_PHP_DOMAIN; + $url .= self::API_URL; + $url = str_replace('{version}', $phpver, $url); + logger()->debug("Fetching PHP release info for version {$phpver} from {$url}"); + // Fetch PHP release info first - $info = default_shell()->executeCurl(str_replace('{version}', $phpver, self::PHP_API), retries: $downloader->getRetry()); + $info = default_shell()->executeCurl($url, retries: $downloader->getRetry()); if ($info === false) { throw new DownloaderException("Failed to fetch PHP release info for version {$phpver}"); } diff --git a/src/StaticPHP/Config/ConfigValidator.php b/src/StaticPHP/Config/ConfigValidator.php index 83f9ca41c..f011482c7 100644 --- a/src/StaticPHP/Config/ConfigValidator.php +++ b/src/StaticPHP/Config/ConfigValidator.php @@ -90,7 +90,7 @@ class ConfigValidator 'local' => [['dirname'], ['extract']], 'pie' => [['repo'], ['extract']], 'pecl' => [['name'], ['extract']], - 'php-release' => [[], ['extract']], + 'php-release' => [['domain'], ['extract']], 'custom' => [[], ['func']], ]; From 671ebd258284856fc5fa5770732233f2330b259d Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 4 Mar 2026 16:32:49 +0800 Subject: [PATCH 15/21] Use gmp mirror site --- config/pkg/lib/gmp.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/pkg/lib/gmp.yml b/config/pkg/lib/gmp.yml index bdc13b559..c14697748 100644 --- a/config/pkg/lib/gmp.yml +++ b/config/pkg/lib/gmp.yml @@ -3,7 +3,7 @@ gmp: artifact: source: type: filelist - url: 'https://gmplib.org/download/gmp/' + url: 'https://ftp.gnu.org/gnu/gmp/' regex: '/href="(?gmp-(?[^"]+)\.tar\.xz)"/' source-mirror: type: url From 00c08e0c0ca27cb37848b1f1d57aa66c783c8966 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 5 Mar 2026 08:11:41 +0800 Subject: [PATCH 16/21] Use no optional libs for libxml2 --- config/pkg/lib/libxml2.yml | 3 +-- src/Package/Library/libxml2.php | 12 ++++-------- src/StaticPHP/Artifact/ArtifactCache.php | 6 ++++++ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/config/pkg/lib/libxml2.yml b/config/pkg/lib/libxml2.yml index db88e8b14..7e86b5af5 100644 --- a/config/pkg/lib/libxml2.yml +++ b/config/pkg/lib/libxml2.yml @@ -10,9 +10,8 @@ libxml2: license: MIT depends@unix: - libiconv - suggests@unix: - - xz - zlib + - xz headers: - libxml2 pkg-configs: diff --git a/src/Package/Library/libxml2.php b/src/Package/Library/libxml2.php index 7c35d6855..3f8b3e71f 100644 --- a/src/Package/Library/libxml2.php +++ b/src/Package/Library/libxml2.php @@ -17,17 +17,13 @@ class libxml2 public function buildForLinux(LibraryPackage $lib): void { UnixCMakeExecutor::create($lib) - ->optionalPackage( - 'zlib', - '-DLIBXML2_WITH_ZLIB=ON ' . - "-DZLIB_LIBRARY={$lib->getLibDir()}/libz.a " . - "-DZLIB_INCLUDE_DIR={$lib->getIncludeDir()}", - '-DLIBXML2_WITH_ZLIB=OFF', - ) - ->optionalPackage('xz', ...cmake_boolean_args('LIBXML2_WITH_LZMA')) ->addConfigureArgs( '-DLIBXML2_WITH_ICONV=ON', '-DIconv_IS_BUILT_IN=OFF', + '-DLIBXML2_WITH_ZLIB=ON', + "-DZLIB_LIBRARY={$lib->getLibDir()}/libz.a", + "-DZLIB_INCLUDE_DIR={$lib->getIncludeDir()}", + '-DLIBXML2_WITH_LZMA=ON', '-DLIBXML2_WITH_ICU=OFF', // optional, but discouraged: https://gitlab.gnome.org/GNOME/libxml2/-/blob/master/README.md '-DLIBXML2_WITH_PYTHON=OFF', '-DLIBXML2_WITH_PROGRAMS=OFF', diff --git a/src/StaticPHP/Artifact/ArtifactCache.php b/src/StaticPHP/Artifact/ArtifactCache.php index dcd75ef7d..2cdd0d0ae 100644 --- a/src/StaticPHP/Artifact/ArtifactCache.php +++ b/src/StaticPHP/Artifact/ArtifactCache.php @@ -19,6 +19,7 @@ class ArtifactCache * dirname?: string, * extract: null|'&custom'|string, * hash: null|string, + * time: int, * downloader: null|string * }, * binary: array{ @@ -29,6 +30,7 @@ class ArtifactCache * dirname?: string, * extract: null|'&custom'|string, * hash: null|string, + * time: int, * version?: null|string, * downloader: null|string * } @@ -108,6 +110,7 @@ public function lock(Artifact|string $artifact, string $lock_type, DownloadResul 'filename' => $download_result->filename, 'extract' => $download_result->extract, 'hash' => sha1_file(DOWNLOAD_PATH . '/' . $download_result->filename), + 'time' => time(), 'version' => $download_result->version, 'config' => $download_result->config, 'downloader' => $download_result->downloader, @@ -119,6 +122,7 @@ public function lock(Artifact|string $artifact, string $lock_type, DownloadResul 'filename' => $download_result->filename, 'extract' => $download_result->extract, 'hash' => sha1_file(DOWNLOAD_PATH . '/' . $download_result->filename), + 'time' => time(), 'version' => $download_result->version, 'config' => $download_result->config, 'downloader' => $download_result->downloader, @@ -130,6 +134,7 @@ public function lock(Artifact|string $artifact, string $lock_type, DownloadResul 'dirname' => $download_result->dirname, 'extract' => $download_result->extract, 'hash' => trim(exec('cd ' . escapeshellarg(DOWNLOAD_PATH . '/' . $download_result->dirname) . ' && ' . SPC_GIT_EXEC . ' rev-parse HEAD')), + 'time' => time(), 'version' => $download_result->version, 'config' => $download_result->config, 'downloader' => $download_result->downloader, @@ -141,6 +146,7 @@ public function lock(Artifact|string $artifact, string $lock_type, DownloadResul 'dirname' => $download_result->dirname, 'extract' => $download_result->extract, 'hash' => null, + 'time' => time(), 'version' => $download_result->version, 'config' => $download_result->config, 'downloader' => $download_result->downloader, From f7277cc01238a4306e574853a16ab89447a72aa0 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 5 Mar 2026 08:17:13 +0800 Subject: [PATCH 17/21] Improve output formatting for update checks in CheckUpdateCommand --- src/StaticPHP/Command/CheckUpdateCommand.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/StaticPHP/Command/CheckUpdateCommand.php b/src/StaticPHP/Command/CheckUpdateCommand.php index 9a2d5f5cc..4fac0f63b 100644 --- a/src/StaticPHP/Command/CheckUpdateCommand.php +++ b/src/StaticPHP/Command/CheckUpdateCommand.php @@ -48,12 +48,10 @@ public function handle(): int 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})"); + $this->output->writeln("Artifact {$artifact} is already up to date ({$result->new})"); } else { - $this->output->writeln("Update available for artifact: {$artifact}"); [$old, $new] = [$result->old ?? 'unavailable', $result->new ?? 'unknown']; - $this->output->writeln(" Old version: {$old}"); - $this->output->writeln(" New version: {$new}"); + $this->output->writeln("Update available for {$artifact}: {$old} -> {$new}"); } } return static::OK; From 715f33ac4dc1300b208996fc07fc9cd8f8c391ec Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 5 Mar 2026 08:17:41 +0800 Subject: [PATCH 18/21] Add log filtering to prevent sensitive data leakage --- .../Downloader/Type/GitHubTokenSetupTrait.php | 2 ++ src/StaticPHP/Exception/ExceptionHandler.php | 2 +- src/StaticPHP/Runtime/Shell/Shell.php | 26 +++++++++---------- src/bootstrap.php | 2 +- src/globals/functions.php | 26 +++++++++++++++++++ 5 files changed, 43 insertions(+), 15 deletions(-) diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php index 90c425075..34e350d4d 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php @@ -16,10 +16,12 @@ public static function getGitHubTokenHeadersStatic(): array // GITHUB_TOKEN support if (($token = getenv('GITHUB_TOKEN')) !== false && ($user = getenv('GITHUB_USER')) !== false) { logger()->debug("Using 'GITHUB_TOKEN' with user {$user} for authentication"); + spc_add_log_filter([$user, $token]); return ['Authorization: Basic ' . base64_encode("{$user}:{$token}")]; } if (($token = getenv('GITHUB_TOKEN')) !== false) { logger()->debug("Using 'GITHUB_TOKEN' for authentication"); + spc_add_log_filter($token); return ["Authorization: Bearer {$token}"]; } return []; diff --git a/src/StaticPHP/Exception/ExceptionHandler.php b/src/StaticPHP/Exception/ExceptionHandler.php index 9dddc9102..053d82a3f 100644 --- a/src/StaticPHP/Exception/ExceptionHandler.php +++ b/src/StaticPHP/Exception/ExceptionHandler.php @@ -115,7 +115,7 @@ private static function logError($message, int $indent_space = 0, bool $output_l $msg = explode("\n", (string) $message); foreach ($msg as $v) { $line = str_pad($v, strlen($v) + $indent_space, ' ', STR_PAD_LEFT); - fwrite($spc_log, strip_ansi_colors($line) . PHP_EOL); + spc_write_log($spc_log, strip_ansi_colors($line) . PHP_EOL); if ($output_log) { InteractiveTerm::plain(ConsoleColor::$color($line) . '', 'error'); } diff --git a/src/StaticPHP/Runtime/Shell/Shell.php b/src/StaticPHP/Runtime/Shell/Shell.php index 2d0d90b8c..f9f4f1759 100644 --- a/src/StaticPHP/Runtime/Shell/Shell.php +++ b/src/StaticPHP/Runtime/Shell/Shell.php @@ -114,22 +114,22 @@ protected function logCommandInfo(string $cmd): void if (!$this->enable_log_file) { return; } - // write executed command to log file using fwrite + // write executed command to log file using spc_write_log $log_file = fopen(SPC_SHELL_LOG, 'a'); - fwrite($log_file, "\n>>>>>>>>>>>>>>>>>>>>>>>>>> [" . date('Y-m-d H:i:s') . "]\n"); - fwrite($log_file, "> Executing command: {$cmd}\n"); + spc_write_log($log_file, "\n>>>>>>>>>>>>>>>>>>>>>>>>>> [" . date('Y-m-d H:i:s') . "]\n"); + spc_write_log($log_file, "> Executing command: {$cmd}\n"); // get the backtrace to find the file and line number $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); if (isset($backtrace[1]['file'], $backtrace[1]['line'])) { $file = $backtrace[1]['file']; $line = $backtrace[1]['line']; - fwrite($log_file, "> Called from: {$file} at line {$line}\n"); + spc_write_log($log_file, "> Called from: {$file} at line {$line}\n"); } - fwrite($log_file, "> Environment variables: {$this->getEnvString()}\n"); + spc_write_log($log_file, "> Environment variables: {$this->getEnvString()}\n"); if ($this->cd !== null) { - fwrite($log_file, "> Working dir: {$this->cd}\n"); + spc_write_log($log_file, "> Working dir: {$this->cd}\n"); } - fwrite($log_file, "\n"); + spc_write_log($log_file, "\n"); } /** @@ -154,7 +154,7 @@ protected function passthru( ): array { $file_res = null; if ($this->enable_log_file) { - // write executed command to the log file using fwrite + // write executed command to the log file using spc_write_log $file_res = fopen(SPC_SHELL_LOG, 'a'); } if ($console_output) { @@ -194,10 +194,10 @@ protected function passthru( foreach ([$pipes[1], $pipes[2]] as $pipe) { while (($chunk = fread($pipe, 8192)) !== false && $chunk !== '') { if ($console_output) { - fwrite($console_res, $chunk); + spc_write_log($console_res, $chunk); } if ($file_res !== null) { - fwrite($file_res, $chunk); + spc_write_log($file_res, $chunk); } if ($capture_output) { $output_value .= $chunk; @@ -207,7 +207,7 @@ protected function passthru( // check exit code if ($throw_on_error && $status['exitcode'] !== 0) { if ($file_res !== null) { - fwrite($file_res, "Command exited with non-zero code: {$status['exitcode']}\n"); + spc_write_log($file_res, "Command exited with non-zero code: {$status['exitcode']}\n"); } throw new ExecutionException( cmd: $original_command ?? $cmd, @@ -238,10 +238,10 @@ protected function passthru( foreach ($read as $pipe) { while (($chunk = fread($pipe, 8192)) !== false && $chunk !== '') { if ($console_output) { - fwrite($console_res, $chunk); + spc_write_log($console_res, $chunk); } if ($file_res !== null) { - fwrite($file_res, $chunk); + spc_write_log($file_res, $chunk); } if ($capture_output) { $output_value .= $chunk; diff --git a/src/bootstrap.php b/src/bootstrap.php index 95384b719..7856c0b29 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -52,7 +52,7 @@ $log_file_fd = fopen(SPC_OUTPUT_LOG, 'a'); $ob_logger->addLogCallback(function ($level, $output) use ($log_file_fd) { if ($log_file_fd) { - fwrite($log_file_fd, strip_ansi_colors($output) . "\n"); + spc_write_log($log_file_fd, strip_ansi_colors($output) . "\n"); } return true; }); diff --git a/src/globals/functions.php b/src/globals/functions.php index 712cf621e..ee279328d 100644 --- a/src/globals/functions.php +++ b/src/globals/functions.php @@ -132,6 +132,32 @@ function patch_point(): string return ''; } +// Add log filter value(s) to prevent secret leak +function spc_add_log_filter(array|string $filter): void +{ + global $spc_log_filters; + if (!is_array($spc_log_filters)) { + $spc_log_filters = []; + } + if (is_string($filter)) { + if (!in_array($filter, $spc_log_filters, true)) { + $spc_log_filters[] = $filter; + } + } elseif (is_array($filter)) { + $spc_log_filters = array_values(array_unique(array_merge($spc_log_filters, $filter))); + } +} + +function spc_write_log(mixed $stream, string $data): false|int +{ + // get filter + global $spc_log_filters; + if (is_array($spc_log_filters)) { + $data = str_replace($spc_log_filters, '***', $data); + } + return fwrite($stream, $data); +} + function patch_point_interrupt(int $retcode, string $msg = ''): InterruptException { return new InterruptException(message: $msg, code: $retcode); From 5298ee4f971a510908026b518a532d6e6240e16f Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 5 Mar 2026 08:21:44 +0800 Subject: [PATCH 19/21] Use constant back due to config validation problem --- config/downloader.php | 32 ------------------- src/StaticPHP/Artifact/ArtifactDownloader.php | 25 ++++++++++++++- 2 files changed, 24 insertions(+), 33 deletions(-) delete mode 100644 config/downloader.php diff --git a/config/downloader.php b/config/downloader.php deleted file mode 100644 index 2d81a57e3..000000000 --- a/config/downloader.php +++ /dev/null @@ -1,32 +0,0 @@ - */ -return [ - 'bitbuckettag' => BitBucketTag::class, - 'filelist' => FileList::class, - 'git' => Git::class, - 'ghrel' => GitHubRelease::class, - 'ghtar' => GitHubTarball::class, - 'ghtagtar' => GitHubTarball::class, - 'local' => LocalDir::class, - 'pie' => PIE::class, - 'pecl' => PECL::class, - 'url' => Url::class, - 'php-release' => PhpRelease::class, - 'hosted' => HostedPackageBin::class, -]; diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index b5e540786..8b64b60ca 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -6,11 +6,19 @@ use Psr\Log\LogLevel; use StaticPHP\Artifact\Downloader\DownloadResult; +use StaticPHP\Artifact\Downloader\Type\BitBucketTag; use StaticPHP\Artifact\Downloader\Type\CheckUpdateInterface; use StaticPHP\Artifact\Downloader\Type\CheckUpdateResult; use StaticPHP\Artifact\Downloader\Type\DownloadTypeInterface; +use StaticPHP\Artifact\Downloader\Type\FileList; use StaticPHP\Artifact\Downloader\Type\Git; +use StaticPHP\Artifact\Downloader\Type\GitHubRelease; +use StaticPHP\Artifact\Downloader\Type\GitHubTarball; +use StaticPHP\Artifact\Downloader\Type\HostedPackageBin; use StaticPHP\Artifact\Downloader\Type\LocalDir; +use StaticPHP\Artifact\Downloader\Type\PECL; +use StaticPHP\Artifact\Downloader\Type\PhpRelease; +use StaticPHP\Artifact\Downloader\Type\PIE; use StaticPHP\Artifact\Downloader\Type\Url; use StaticPHP\Artifact\Downloader\Type\ValidatorInterface; use StaticPHP\DI\ApplicationContext; @@ -31,6 +39,21 @@ */ class ArtifactDownloader { + public const array DOWNLOADERS = [ + 'bitbuckettag' => BitBucketTag::class, + 'filelist' => FileList::class, + 'git' => Git::class, + 'ghrel' => GitHubRelease::class, + 'ghtar' => GitHubTarball::class, + 'ghtagtar' => GitHubTarball::class, + 'local' => LocalDir::class, + 'pie' => PIE::class, + 'pecl' => PECL::class, + 'url' => Url::class, + 'php-release' => PhpRelease::class, + 'hosted' => HostedPackageBin::class, + ]; + /** @var array> */ protected array $downloaders = []; @@ -198,7 +221,7 @@ public function __construct(protected array $options = []) $this->_before_files = FileSystem::scanDirFiles(DOWNLOAD_PATH, false, true, true) ?: []; // load downloaders - $this->downloaders = require ROOT_DIR . '/config/downloader.php'; + $this->downloaders = self::DOWNLOADERS; } /** From abdaaab6e67672864b7d548c267ae1ff8fa7e871 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 5 Mar 2026 11:11:02 +0800 Subject: [PATCH 20/21] Refactor CheckUpdateResult logic to simplify version comparison --- src/Package/Artifact/zig.php | 5 ++++- src/StaticPHP/Artifact/Downloader/Type/FileList.php | 2 +- src/StaticPHP/Artifact/Downloader/Type/Git.php | 2 +- src/StaticPHP/Artifact/Downloader/Type/PECL.php | 2 +- src/StaticPHP/Artifact/Downloader/Type/PIE.php | 2 +- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Package/Artifact/zig.php b/src/Package/Artifact/zig.php index 0d334e5f1..b42eee3ae 100644 --- a/src/Package/Artifact/zig.php +++ b/src/Package/Artifact/zig.php @@ -72,6 +72,9 @@ public function checkUpdateBinary(?string $old_version, ArtifactDownloader $down $index_json = default_shell()->executeCurl('https://ziglang.org/download/index.json', retries: $downloader->getRetry()); $index_json = json_decode($index_json ?: '', true); $latest_version = null; + if (!is_array($index_json)) { + throw new DownloaderException('Failed to fetch Zig version index for update check'); + } foreach ($index_json as $version => $data) { if ($version !== 'master') { $latest_version = $version; @@ -84,7 +87,7 @@ public function checkUpdateBinary(?string $old_version, ArtifactDownloader $down return new CheckUpdateResult( old: $old_version, new: $latest_version, - needUpdate: $old_version === null || version_compare($latest_version, $old_version, '>'), + needUpdate: $old_version === null || $latest_version !== $old_version, ); } diff --git a/src/StaticPHP/Artifact/Downloader/Type/FileList.php b/src/StaticPHP/Artifact/Downloader/Type/FileList.php index b32645330..b868210de 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/FileList.php +++ b/src/StaticPHP/Artifact/Downloader/Type/FileList.php @@ -32,7 +32,7 @@ public function checkUpdate(string $name, array $config, ?string $old_version, A return new CheckUpdateResult( old: $old_version, new: $version, - needUpdate: $old_version === null || version_compare($version, $old_version, '>'), + needUpdate: $old_version === null || $version !== $old_version, ); } diff --git a/src/StaticPHP/Artifact/Downloader/Type/Git.php b/src/StaticPHP/Artifact/Downloader/Type/Git.php index 1ee9da4da..d5822e697 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/Git.php +++ b/src/StaticPHP/Artifact/Downloader/Type/Git.php @@ -120,7 +120,7 @@ public function checkUpdate(string $name, array $config, ?string $old_version, A return new CheckUpdateResult( old: $old_version, new: $version, - needUpdate: $old_version === null || version_compare($version, $old_version, '>'), + needUpdate: $old_version === null || $version !== $old_version, ); } throw new DownloaderException("No matching branch found for regex {$config['regex']}."); diff --git a/src/StaticPHP/Artifact/Downloader/Type/PECL.php b/src/StaticPHP/Artifact/Downloader/Type/PECL.php index 0b14b05d4..78ceed3a8 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/PECL.php +++ b/src/StaticPHP/Artifact/Downloader/Type/PECL.php @@ -22,7 +22,7 @@ public function checkUpdate(string $name, array $config, ?string $old_version, A return new CheckUpdateResult( old: $old_version, new: $version, - needUpdate: $old_version === null || version_compare($version, $old_version, '>'), + needUpdate: $old_version === null || $version !== $old_version, ); } diff --git a/src/StaticPHP/Artifact/Downloader/Type/PIE.php b/src/StaticPHP/Artifact/Downloader/Type/PIE.php index a84cffe51..14996c5af 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/PIE.php +++ b/src/StaticPHP/Artifact/Downloader/Type/PIE.php @@ -40,7 +40,7 @@ public function checkUpdate(string $name, array $config, ?string $old_version, A return new CheckUpdateResult( old: $old_version, new: $new_version, - needUpdate: $old_version === null || version_compare($new_version, $old_version, '>'), + needUpdate: $old_version === null || $new_version !== $old_version, ); } From 84f6dab882544dd2f0011229e4196463fcc55025 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 5 Mar 2026 11:11:31 +0800 Subject: [PATCH 21/21] Add parallel update checking and improve artifact update handling --- src/StaticPHP/Artifact/ArtifactCache.php | 10 +++ src/StaticPHP/Artifact/ArtifactDownloader.php | 86 ++++++++++++++++++- .../Downloader/Type/CheckUpdateResult.php | 3 +- src/StaticPHP/Command/CheckUpdateCommand.php | 34 ++++++-- src/globals/defines.php | 2 +- 5 files changed, 122 insertions(+), 13 deletions(-) diff --git a/src/StaticPHP/Artifact/ArtifactCache.php b/src/StaticPHP/Artifact/ArtifactCache.php index cd9ad640d..7626831af 100644 --- a/src/StaticPHP/Artifact/ArtifactCache.php +++ b/src/StaticPHP/Artifact/ArtifactCache.php @@ -282,6 +282,16 @@ public function removeBinary(string $artifact_name, string $platform, bool $dele logger()->debug("Removed binary cache entry for [{$artifact_name}] on platform [{$platform}]"); } + /** + * Get the names of all artifacts that have at least one downloaded entry (source or binary). + * + * @return array Artifact names + */ + public function getCachedArtifactNames(): array + { + return array_keys($this->cache); + } + /** * Save cache to file. */ diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index 8b64b60ca..a9a259157 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -362,7 +362,8 @@ public function checkUpdate(string $artifact_name, bool $prefer_source = false, if ($result !== null) { return $result; } - throw new WrongUsageException("Artifact '{$artifact_name}' downloader does not support update checking."); + // logger()->warning("Artifact '{$artifact_name}' downloader does not support update checking, skipping."); + return new CheckUpdateResult(old: null, new: null, needUpdate: false, unsupported: true); } $cache = ApplicationContext::get(ArtifactCache::class); if ($prefer_source) { @@ -393,7 +394,33 @@ public function checkUpdate(string $artifact_name, bool $prefer_source = false, 'old_version' => $info['version'], ]); } - throw new WrongUsageException("Artifact '{$artifact_name}' downloader does not support update checking, exit."); + // logger()->warning("Artifact '{$artifact_name}' downloader does not support update checking, skipping."); + return new CheckUpdateResult(old: null, new: null, needUpdate: false, unsupported: true); + } + + /** + * Check updates for multiple artifacts, with optional parallel processing. + * + * @param array $artifact_names Artifact names to check + * @param bool $prefer_source Whether to prefer source over binary + * @param bool $bare Check without requiring artifact to be downloaded first + * @param null|callable $onResult Called immediately with (string $name, CheckUpdateResult) as each result arrives + * @return array Results keyed by artifact name + */ + public function checkUpdates(array $artifact_names, bool $prefer_source = false, bool $bare = false, ?callable $onResult = null): array + { + if ($this->parallel > 1 && count($artifact_names) > 1) { + return $this->checkUpdatesWithConcurrency($artifact_names, $prefer_source, $bare, $onResult); + } + $results = []; + foreach ($artifact_names as $name) { + $result = $this->checkUpdate($name, $prefer_source, $bare); + $results[$name] = $result; + if ($onResult !== null) { + ($onResult)($name, $result); + } + } + return $results; } public function getRetry(): int @@ -411,6 +438,61 @@ public function getOption(string $name, mixed $default = null): mixed return $this->options[$name] ?? $default; } + private function checkUpdatesWithConcurrency(array $artifact_names, bool $prefer_source, bool $bare, ?callable $onResult): array + { + $results = []; + $fiber_pool = []; + $remaining = $artifact_names; + + Shell::passthruCallback(function () { + \Fiber::suspend(); + }); + + try { + while (!empty($remaining) || !empty($fiber_pool)) { + // fill pool + while (count($fiber_pool) < $this->parallel && !empty($remaining)) { + $name = array_shift($remaining); + $fiber = new \Fiber(function () use ($name, $prefer_source, $bare) { + return [$name, $this->checkUpdate($name, $prefer_source, $bare)]; + }); + $fiber->start(); + $fiber_pool[$name] = $fiber; + } + // check pool + foreach ($fiber_pool as $fiber_name => $fiber) { + if ($fiber->isTerminated()) { + // getReturn() re-throws if the fiber threw — propagates immediately + [$artifact_name, $result] = $fiber->getReturn(); + $results[$artifact_name] = $result; + if ($onResult !== null) { + ($onResult)($artifact_name, $result); + } + unset($fiber_pool[$fiber_name]); + } else { + $fiber->resume(); + } + } + } + } catch (\Throwable $e) { + // terminate all still-suspended fibers so their curl processes don't hang + foreach ($fiber_pool as $fiber) { + if (!$fiber->isTerminated()) { + try { + $fiber->throw($e); + } catch (\Throwable) { + // ignore — we only care about stopping them + } + } + } + throw $e; + } finally { + Shell::passthruCallback(null); + } + + return $results; + } + private function probeSourceCheckUpdate(Artifact $artifact, string $artifact_name): ?CheckUpdateResult { if (($callback = $artifact->getCustomSourceCheckUpdateCallback()) !== null) { diff --git a/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateResult.php b/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateResult.php index 468b643b0..7e46e4ad7 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateResult.php +++ b/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateResult.php @@ -8,7 +8,8 @@ { public function __construct( public ?string $old, - public string $new, + public ?string $new, public bool $needUpdate, + public bool $unsupported = false, ) {} } diff --git a/src/StaticPHP/Command/CheckUpdateCommand.php b/src/StaticPHP/Command/CheckUpdateCommand.php index 4fac0f63b..1663337c6 100644 --- a/src/StaticPHP/Command/CheckUpdateCommand.php +++ b/src/StaticPHP/Command/CheckUpdateCommand.php @@ -4,7 +4,10 @@ namespace StaticPHP\Command; +use StaticPHP\Artifact\ArtifactCache; use StaticPHP\Artifact\ArtifactDownloader; +use StaticPHP\Artifact\Downloader\Type\CheckUpdateResult; +use StaticPHP\DI\ApplicationContext; use StaticPHP\Exception\SPCException; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputArgument; @@ -17,9 +20,10 @@ class CheckUpdateCommand extends BaseCommand public function configure(): void { - $this->addArgument('artifact', InputArgument::REQUIRED, 'The name of the artifact(s) to check for updates, comma-separated'); + $this->addArgument('artifact', InputArgument::OPTIONAL, 'The name of the artifact(s) to check for updates, comma-separated (default: all downloaded artifacts)'); $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)'); + $this->addOption('parallel', 'p', InputOption::VALUE_REQUIRED, 'Number of parallel update checks (default: 10)', 10); // --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'); @@ -27,17 +31,27 @@ public function configure(): void public function handle(): int { - $artifacts = parse_comma_list($this->input->getArgument('artifact')); + $artifact_arg = $this->input->getArgument('artifact'); + if ($artifact_arg === null) { + $artifacts = ApplicationContext::get(ArtifactCache::class)->getCachedArtifactNames(); + if (empty($artifacts)) { + $this->output->writeln('No downloaded artifacts found.'); + return static::OK; + } + } else { + $artifacts = parse_comma_list($artifact_arg); + } try { $downloader = new ArtifactDownloader($this->input->getOptions()); $bare = (bool) $this->getOption('bare'); if ($this->getOption('json')) { + $results = $downloader->checkUpdates($artifacts, bare: $bare); $outputs = []; - foreach ($artifacts as $artifact) { - $result = $downloader->checkUpdate($artifact, bare: $bare); + foreach ($results as $artifact => $result) { $outputs[$artifact] = [ 'need-update' => $result->needUpdate, + 'unsupported' => $result->unsupported, 'old' => $result->old, 'new' => $result->new, ]; @@ -45,15 +59,17 @@ public function handle(): int $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 ({$result->new})"); + $downloader->checkUpdates($artifacts, bare: $bare, onResult: function (string $artifact, CheckUpdateResult $result) { + if ($result->unsupported) { + $this->output->writeln("Artifact {$artifact} does not support update checking, skipped"); + } elseif (!$result->needUpdate) { + $ver = $result->new ? "({$result->new})" : ''; + $this->output->writeln("Artifact {$artifact} is already up to date {$ver}"); } else { [$old, $new] = [$result->old ?? 'unavailable', $result->new ?? 'unknown']; $this->output->writeln("Update available for {$artifact}: {$old} -> {$new}"); } - } + }); return static::OK; } catch (SPCException $e) { $e->setSimpleOutput(); diff --git a/src/globals/defines.php b/src/globals/defines.php index dbcb63f22..38490046e 100644 --- a/src/globals/defines.php +++ b/src/globals/defines.php @@ -104,7 +104,7 @@ 'local' => 'local dir', 'pie' => 'PHP Installer for Extensions (PIE)', 'url' => 'url', - 'php-release' => 'php.net', + 'php-release' => 'PHP website release', 'custom' => 'custom downloader', ];