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