mirror of
https://github.com/crazywhalecc/static-php-cli.git
synced 2026-03-17 20:34:51 +08:00
[3.0] Add check-update command and CheckUpdateInterface for artifacts (#1044)
This commit is contained in:
commit
705435eccb
@ -5,3 +5,7 @@ php-src:
|
|||||||
license: PHP-3.01
|
license: PHP-3.01
|
||||||
source:
|
source:
|
||||||
type: php-release
|
type: php-release
|
||||||
|
domain: 'https://www.php.net'
|
||||||
|
source-mirror:
|
||||||
|
type: php-release
|
||||||
|
domain: 'https://phpmirror.static-php.dev'
|
||||||
|
|||||||
@ -1,30 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
use StaticPHP\Artifact\Downloader\Type\BitBucketTag;
|
|
||||||
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\PhpRelease;
|
|
||||||
use StaticPHP\Artifact\Downloader\Type\PIE;
|
|
||||||
use StaticPHP\Artifact\Downloader\Type\Url;
|
|
||||||
|
|
||||||
/* @return array<string, DownloadTypeInterface> */
|
|
||||||
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,
|
|
||||||
'url' => Url::class,
|
|
||||||
'php-release' => PhpRelease::class,
|
|
||||||
'hosted' => HostedPackageBin::class,
|
|
||||||
];
|
|
||||||
@ -1,5 +1,19 @@
|
|||||||
ext-bcmath:
|
ext-bcmath:
|
||||||
type: php-extension
|
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:
|
ext-openssl:
|
||||||
type: php-extension
|
type: php-extension
|
||||||
depends:
|
depends:
|
||||||
@ -10,6 +24,21 @@ ext-openssl:
|
|||||||
arg-type: custom
|
arg-type: custom
|
||||||
arg-type@windows: with
|
arg-type@windows: with
|
||||||
build-with-php: true
|
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:
|
ext-zlib:
|
||||||
type: php-extension
|
type: php-extension
|
||||||
depends:
|
depends:
|
||||||
|
|||||||
@ -2,10 +2,8 @@ ext-amqp:
|
|||||||
type: php-extension
|
type: php-extension
|
||||||
artifact:
|
artifact:
|
||||||
source:
|
source:
|
||||||
type: url
|
type: pecl
|
||||||
url: 'https://pecl.php.net/get/amqp'
|
name: amqp
|
||||||
extract: php-src/ext/amqp
|
|
||||||
filename: amqp.tgz
|
|
||||||
metadata:
|
metadata:
|
||||||
license-files: [LICENSE]
|
license-files: [LICENSE]
|
||||||
license: PHP-3.01
|
license: PHP-3.01
|
||||||
|
|||||||
@ -2,10 +2,8 @@ ext-apcu:
|
|||||||
type: php-extension
|
type: php-extension
|
||||||
artifact:
|
artifact:
|
||||||
source:
|
source:
|
||||||
type: url
|
type: pecl
|
||||||
url: 'https://pecl.php.net/get/APCu'
|
name: APCu
|
||||||
extract: php-src/ext/apcu
|
|
||||||
filename: apcu.tgz
|
|
||||||
metadata:
|
metadata:
|
||||||
license-files: [LICENSE]
|
license-files: [LICENSE]
|
||||||
license: PHP-3.01
|
license: PHP-3.01
|
||||||
|
|||||||
6
config/pkg/ext/ext-ast.yml
Normal file
6
config/pkg/ext/ext-ast.yml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
ext-ast:
|
||||||
|
type: php-extension
|
||||||
|
artifact:
|
||||||
|
source:
|
||||||
|
type: pecl
|
||||||
|
name: ast
|
||||||
@ -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
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
ext-mbstring:
|
|
||||||
type: php-extension
|
|
||||||
php-extension:
|
|
||||||
arg-type: custom
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
ext-phar:
|
|
||||||
type: php-extension
|
|
||||||
depends:
|
|
||||||
- zlib
|
|
||||||
@ -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
|
|
||||||
@ -3,7 +3,7 @@ gmp:
|
|||||||
artifact:
|
artifact:
|
||||||
source:
|
source:
|
||||||
type: filelist
|
type: filelist
|
||||||
url: 'https://gmplib.org/download/gmp/'
|
url: 'https://ftp.gnu.org/gnu/gmp/'
|
||||||
regex: '/href="(?<file>gmp-(?<version>[^"]+)\.tar\.xz)"/'
|
regex: '/href="(?<file>gmp-(?<version>[^"]+)\.tar\.xz)"/'
|
||||||
source-mirror:
|
source-mirror:
|
||||||
type: url
|
type: url
|
||||||
|
|||||||
@ -10,9 +10,8 @@ libxml2:
|
|||||||
license: MIT
|
license: MIT
|
||||||
depends@unix:
|
depends@unix:
|
||||||
- libiconv
|
- libiconv
|
||||||
suggests@unix:
|
|
||||||
- xz
|
|
||||||
- zlib
|
- zlib
|
||||||
|
- xz
|
||||||
headers:
|
headers:
|
||||||
- libxml2
|
- libxml2
|
||||||
pkg-configs:
|
pkg-configs:
|
||||||
|
|||||||
@ -6,8 +6,10 @@ namespace Package\Artifact;
|
|||||||
|
|
||||||
use StaticPHP\Artifact\ArtifactDownloader;
|
use StaticPHP\Artifact\ArtifactDownloader;
|
||||||
use StaticPHP\Artifact\Downloader\DownloadResult;
|
use StaticPHP\Artifact\Downloader\DownloadResult;
|
||||||
|
use StaticPHP\Artifact\Downloader\Type\CheckUpdateResult;
|
||||||
use StaticPHP\Attribute\Artifact\AfterBinaryExtract;
|
use StaticPHP\Attribute\Artifact\AfterBinaryExtract;
|
||||||
use StaticPHP\Attribute\Artifact\CustomBinary;
|
use StaticPHP\Attribute\Artifact\CustomBinary;
|
||||||
|
use StaticPHP\Attribute\Artifact\CustomBinaryCheckUpdate;
|
||||||
use StaticPHP\Exception\DownloaderException;
|
use StaticPHP\Exception\DownloaderException;
|
||||||
use StaticPHP\Runtime\SystemTarget;
|
use StaticPHP\Runtime\SystemTarget;
|
||||||
use StaticPHP\Util\GlobalEnvManager;
|
use StaticPHP\Util\GlobalEnvManager;
|
||||||
@ -65,6 +67,25 @@ class go_xcaddy
|
|||||||
return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $version], extract: "{$pkgroot}/go-xcaddy", verified: true, version: $version);
|
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 DownloaderException('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', [
|
#[AfterBinaryExtract('go-xcaddy', [
|
||||||
'linux-x86_64',
|
'linux-x86_64',
|
||||||
'linux-aarch64',
|
'linux-aarch64',
|
||||||
|
|||||||
@ -6,8 +6,10 @@ namespace Package\Artifact;
|
|||||||
|
|
||||||
use StaticPHP\Artifact\ArtifactDownloader;
|
use StaticPHP\Artifact\ArtifactDownloader;
|
||||||
use StaticPHP\Artifact\Downloader\DownloadResult;
|
use StaticPHP\Artifact\Downloader\DownloadResult;
|
||||||
|
use StaticPHP\Artifact\Downloader\Type\CheckUpdateResult;
|
||||||
use StaticPHP\Attribute\Artifact\AfterBinaryExtract;
|
use StaticPHP\Attribute\Artifact\AfterBinaryExtract;
|
||||||
use StaticPHP\Attribute\Artifact\CustomBinary;
|
use StaticPHP\Attribute\Artifact\CustomBinary;
|
||||||
|
use StaticPHP\Attribute\Artifact\CustomBinaryCheckUpdate;
|
||||||
use StaticPHP\Exception\DownloaderException;
|
use StaticPHP\Exception\DownloaderException;
|
||||||
use StaticPHP\Runtime\SystemTarget;
|
use StaticPHP\Runtime\SystemTarget;
|
||||||
|
|
||||||
@ -59,6 +61,36 @@ class zig
|
|||||||
return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $latest_version], extract: PKG_ROOT_PATH . '/zig', verified: true, version: $latest_version);
|
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;
|
||||||
|
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;
|
||||||
|
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 || $latest_version !== $old_version,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[AfterBinaryExtract('zig', [
|
#[AfterBinaryExtract('zig', [
|
||||||
'linux-x86_64',
|
'linux-x86_64',
|
||||||
'linux-aarch64',
|
'linux-aarch64',
|
||||||
|
|||||||
@ -6,19 +6,30 @@ namespace Package\Library;
|
|||||||
|
|
||||||
use StaticPHP\Attribute\Package\BuildFor;
|
use StaticPHP\Attribute\Package\BuildFor;
|
||||||
use StaticPHP\Attribute\Package\Library;
|
use StaticPHP\Attribute\Package\Library;
|
||||||
|
use StaticPHP\Attribute\Package\PatchBeforeBuild;
|
||||||
use StaticPHP\Package\LibraryPackage;
|
use StaticPHP\Package\LibraryPackage;
|
||||||
use StaticPHP\Package\PackageBuilder;
|
use StaticPHP\Package\PackageBuilder;
|
||||||
|
use StaticPHP\Util\FileSystem;
|
||||||
|
|
||||||
#[Library('bzip2')]
|
#[Library('bzip2')]
|
||||||
class bzip2
|
class bzip2
|
||||||
{
|
{
|
||||||
|
#[PatchBeforeBuild]
|
||||||
|
public function patchBeforeBuild(LibraryPackage $lib): void
|
||||||
|
{
|
||||||
|
FileSystem::replaceFileStr($lib->getSourceDir() . '/Makefile', 'CFLAGS=-Wall', 'CFLAGS=-fPIC -Wall');
|
||||||
|
}
|
||||||
|
|
||||||
#[BuildFor('Linux')]
|
#[BuildFor('Linux')]
|
||||||
#[BuildFor('Darwin')]
|
#[BuildFor('Darwin')]
|
||||||
public function build(LibraryPackage $lib, PackageBuilder $builder): void
|
public function build(LibraryPackage $lib, PackageBuilder $builder): void
|
||||||
{
|
{
|
||||||
shell()->cd($lib->getSourceDir())->initializeEnv($lib)
|
$shell = shell()->cd($lib->getSourceDir())->initializeEnv($lib);
|
||||||
->exec("make PREFIX='{$lib->getBuildRootPath()}' clean")
|
$env = $shell->getEnvString();
|
||||||
->exec("make -j{$builder->concurrency} PREFIX='{$lib->getBuildRootPath()}' libbz2.a")
|
$cc_env = 'CC=' . escapeshellarg(getenv('CC') ?: 'cc') . ' AR=' . escapeshellarg(getenv('AR') ?: 'ar');
|
||||||
|
|
||||||
|
$shell->exec("make PREFIX='{$lib->getBuildRootPath()}' clean")
|
||||||
|
->exec("make -j{$builder->concurrency} {$cc_env} {$env} PREFIX='{$lib->getBuildRootPath()}' libbz2.a")
|
||||||
->exec('cp libbz2.a ' . $lib->getLibDir())
|
->exec('cp libbz2.a ' . $lib->getLibDir())
|
||||||
->exec('cp bzlib.h ' . $lib->getIncludeDir());
|
->exec('cp bzlib.h ' . $lib->getIncludeDir());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,17 +17,13 @@ class libxml2
|
|||||||
public function buildForLinux(LibraryPackage $lib): void
|
public function buildForLinux(LibraryPackage $lib): void
|
||||||
{
|
{
|
||||||
UnixCMakeExecutor::create($lib)
|
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(
|
->addConfigureArgs(
|
||||||
'-DLIBXML2_WITH_ICONV=ON',
|
'-DLIBXML2_WITH_ICONV=ON',
|
||||||
'-DIconv_IS_BUILT_IN=OFF',
|
'-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_ICU=OFF', // optional, but discouraged: https://gitlab.gnome.org/GNOME/libxml2/-/blob/master/README.md
|
||||||
'-DLIBXML2_WITH_PYTHON=OFF',
|
'-DLIBXML2_WITH_PYTHON=OFF',
|
||||||
'-DLIBXML2_WITH_PROGRAMS=OFF',
|
'-DLIBXML2_WITH_PROGRAMS=OFF',
|
||||||
|
|||||||
@ -27,9 +27,15 @@ class Artifact
|
|||||||
/** @var null|callable Bind custom source fetcher callback */
|
/** @var null|callable Bind custom source fetcher callback */
|
||||||
protected mixed $custom_source_callback = null;
|
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<string, callable> Bind custom binary fetcher callbacks */
|
/** @var array<string, callable> Bind custom binary fetcher callbacks */
|
||||||
protected mixed $custom_binary_callbacks = [];
|
protected mixed $custom_binary_callbacks = [];
|
||||||
|
|
||||||
|
/** @var array<string, callable> 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) */
|
/** @var null|callable Bind custom source extract callback (completely takes over extraction) */
|
||||||
protected mixed $source_extract_callback = null;
|
protected mixed $source_extract_callback = null;
|
||||||
|
|
||||||
@ -405,6 +411,19 @@ class Artifact
|
|||||||
return $this->custom_source_callback ?? null;
|
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
|
public function getCustomBinaryCallback(): ?callable
|
||||||
{
|
{
|
||||||
$current_platform = SystemTarget::getCurrentPlatformString();
|
$current_platform = SystemTarget::getCurrentPlatformString();
|
||||||
@ -433,6 +452,24 @@ class Artifact
|
|||||||
$this->custom_binary_callbacks[$target_os] = $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 ====================
|
// ==================== Extraction Callbacks ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -18,7 +18,9 @@ class ArtifactCache
|
|||||||
* filename?: string,
|
* filename?: string,
|
||||||
* dirname?: string,
|
* dirname?: string,
|
||||||
* extract: null|'&custom'|string,
|
* extract: null|'&custom'|string,
|
||||||
* hash: null|string
|
* hash: null|string,
|
||||||
|
* time: int,
|
||||||
|
* downloader: null|string
|
||||||
* },
|
* },
|
||||||
* binary: array{
|
* binary: array{
|
||||||
* windows-x86_64?: null|array{
|
* windows-x86_64?: null|array{
|
||||||
@ -28,7 +30,9 @@ class ArtifactCache
|
|||||||
* dirname?: string,
|
* dirname?: string,
|
||||||
* extract: null|'&custom'|string,
|
* extract: null|'&custom'|string,
|
||||||
* hash: null|string,
|
* hash: null|string,
|
||||||
* version?: null|string
|
* time: int,
|
||||||
|
* version?: null|string,
|
||||||
|
* downloader: null|string
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
* }>
|
* }>
|
||||||
@ -106,8 +110,10 @@ class ArtifactCache
|
|||||||
'filename' => $download_result->filename,
|
'filename' => $download_result->filename,
|
||||||
'extract' => $download_result->extract,
|
'extract' => $download_result->extract,
|
||||||
'hash' => sha1_file(DOWNLOAD_PATH . '/' . $download_result->filename),
|
'hash' => sha1_file(DOWNLOAD_PATH . '/' . $download_result->filename),
|
||||||
|
'time' => time(),
|
||||||
'version' => $download_result->version,
|
'version' => $download_result->version,
|
||||||
'config' => $download_result->config,
|
'config' => $download_result->config,
|
||||||
|
'downloader' => $download_result->downloader,
|
||||||
];
|
];
|
||||||
} elseif ($download_result->cache_type === 'file') {
|
} elseif ($download_result->cache_type === 'file') {
|
||||||
$obj = [
|
$obj = [
|
||||||
@ -116,8 +122,10 @@ class ArtifactCache
|
|||||||
'filename' => $download_result->filename,
|
'filename' => $download_result->filename,
|
||||||
'extract' => $download_result->extract,
|
'extract' => $download_result->extract,
|
||||||
'hash' => sha1_file(DOWNLOAD_PATH . '/' . $download_result->filename),
|
'hash' => sha1_file(DOWNLOAD_PATH . '/' . $download_result->filename),
|
||||||
|
'time' => time(),
|
||||||
'version' => $download_result->version,
|
'version' => $download_result->version,
|
||||||
'config' => $download_result->config,
|
'config' => $download_result->config,
|
||||||
|
'downloader' => $download_result->downloader,
|
||||||
];
|
];
|
||||||
} elseif ($download_result->cache_type === 'git') {
|
} elseif ($download_result->cache_type === 'git') {
|
||||||
$obj = [
|
$obj = [
|
||||||
@ -126,8 +134,10 @@ class ArtifactCache
|
|||||||
'dirname' => $download_result->dirname,
|
'dirname' => $download_result->dirname,
|
||||||
'extract' => $download_result->extract,
|
'extract' => $download_result->extract,
|
||||||
'hash' => trim(exec('cd ' . escapeshellarg(DOWNLOAD_PATH . '/' . $download_result->dirname) . ' && ' . SPC_GIT_EXEC . ' rev-parse HEAD')),
|
'hash' => trim(exec('cd ' . escapeshellarg(DOWNLOAD_PATH . '/' . $download_result->dirname) . ' && ' . SPC_GIT_EXEC . ' rev-parse HEAD')),
|
||||||
|
'time' => time(),
|
||||||
'version' => $download_result->version,
|
'version' => $download_result->version,
|
||||||
'config' => $download_result->config,
|
'config' => $download_result->config,
|
||||||
|
'downloader' => $download_result->downloader,
|
||||||
];
|
];
|
||||||
} elseif ($download_result->cache_type === 'local') {
|
} elseif ($download_result->cache_type === 'local') {
|
||||||
$obj = [
|
$obj = [
|
||||||
@ -136,8 +146,10 @@ class ArtifactCache
|
|||||||
'dirname' => $download_result->dirname,
|
'dirname' => $download_result->dirname,
|
||||||
'extract' => $download_result->extract,
|
'extract' => $download_result->extract,
|
||||||
'hash' => null,
|
'hash' => null,
|
||||||
|
'time' => time(),
|
||||||
'version' => $download_result->version,
|
'version' => $download_result->version,
|
||||||
'config' => $download_result->config,
|
'config' => $download_result->config,
|
||||||
|
'downloader' => $download_result->downloader,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
if ($obj === null) {
|
if ($obj === null) {
|
||||||
@ -157,7 +169,7 @@ class ArtifactCache
|
|||||||
throw new SPCInternalException("Invalid lock type '{$lock_type}' for artifact {$artifact_name}");
|
throw new SPCInternalException("Invalid lock type '{$lock_type}' for artifact {$artifact_name}");
|
||||||
}
|
}
|
||||||
// save cache to file
|
// 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));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -270,12 +282,22 @@ class ArtifactCache
|
|||||||
logger()->debug("Removed binary cache entry for [{$artifact_name}] on platform [{$platform}]");
|
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<string> Artifact names
|
||||||
|
*/
|
||||||
|
public function getCachedArtifactNames(): array
|
||||||
|
{
|
||||||
|
return array_keys($this->cache);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save cache to file.
|
* Save cache to file.
|
||||||
*/
|
*/
|
||||||
public function save(): void
|
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
|
private function isObjectDownloaded(?array $object, bool $compare_hash = false): bool
|
||||||
|
|||||||
@ -6,9 +6,19 @@ namespace StaticPHP\Artifact;
|
|||||||
|
|
||||||
use Psr\Log\LogLevel;
|
use Psr\Log\LogLevel;
|
||||||
use StaticPHP\Artifact\Downloader\DownloadResult;
|
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\DownloadTypeInterface;
|
||||||
|
use StaticPHP\Artifact\Downloader\Type\FileList;
|
||||||
use StaticPHP\Artifact\Downloader\Type\Git;
|
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\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\Url;
|
||||||
use StaticPHP\Artifact\Downloader\Type\ValidatorInterface;
|
use StaticPHP\Artifact\Downloader\Type\ValidatorInterface;
|
||||||
use StaticPHP\DI\ApplicationContext;
|
use StaticPHP\DI\ApplicationContext;
|
||||||
@ -29,6 +39,21 @@ use ZM\Logger\ConsoleColor;
|
|||||||
*/
|
*/
|
||||||
class ArtifactDownloader
|
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<string, class-string<DownloadTypeInterface>> */
|
/** @var array<string, class-string<DownloadTypeInterface>> */
|
||||||
protected array $downloaders = [];
|
protected array $downloaders = [];
|
||||||
|
|
||||||
@ -196,7 +221,7 @@ class ArtifactDownloader
|
|||||||
$this->_before_files = FileSystem::scanDirFiles(DOWNLOAD_PATH, false, true, true) ?: [];
|
$this->_before_files = FileSystem::scanDirFiles(DOWNLOAD_PATH, false, true, true) ?: [];
|
||||||
|
|
||||||
// load downloaders
|
// load downloaders
|
||||||
$this->downloaders = require ROOT_DIR . '/config/downloader.php';
|
$this->downloaders = self::DOWNLOADERS;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -323,6 +348,81 @@ 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) {
|
||||||
|
[$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;
|
||||||
|
}
|
||||||
|
// 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) {
|
||||||
|
$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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($info['lock_type'] ?? null) === 'source' && ($callback = $artifact->getCustomSourceCheckUpdateCallback()) !== null) {
|
||||||
|
return ApplicationContext::invoke($callback, [
|
||||||
|
ArtifactDownloader::class => $this,
|
||||||
|
'old_version' => $info['version'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($callback = $artifact->getCustomBinaryCheckUpdateCallback()) !== null) {
|
||||||
|
return ApplicationContext::invoke($callback, [
|
||||||
|
ArtifactDownloader::class => $this,
|
||||||
|
'old_version' => $info['version'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
// 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<string> $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<string, CheckUpdateResult> 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
|
public function getRetry(): int
|
||||||
{
|
{
|
||||||
return $this->retry;
|
return $this->retry;
|
||||||
@ -338,6 +438,105 @@ class ArtifactDownloader
|
|||||||
return $this->options[$name] ?? $default;
|
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) {
|
||||||
|
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
|
private function downloadWithType(Artifact $artifact, int $current, int $total, bool $parallel = false, bool $interactive = true): int
|
||||||
{
|
{
|
||||||
$queue = $this->generateQueue($artifact);
|
$queue = $this->generateQueue($artifact);
|
||||||
|
|||||||
@ -17,6 +17,7 @@ class DownloadResult
|
|||||||
* @param bool $verified Whether the download has been verified (hash check)
|
* @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 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 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(
|
private function __construct(
|
||||||
public readonly string $cache_type,
|
public readonly string $cache_type,
|
||||||
@ -27,6 +28,7 @@ class DownloadResult
|
|||||||
public bool $verified = false,
|
public bool $verified = false,
|
||||||
public readonly ?string $version = null,
|
public readonly ?string $version = null,
|
||||||
public readonly array $metadata = [],
|
public readonly array $metadata = [],
|
||||||
|
public readonly ?string $downloader = null,
|
||||||
) {
|
) {
|
||||||
switch ($this->cache_type) {
|
switch ($this->cache_type) {
|
||||||
case 'archive':
|
case 'archive':
|
||||||
@ -59,11 +61,12 @@ class DownloadResult
|
|||||||
mixed $extract = null,
|
mixed $extract = null,
|
||||||
bool $verified = false,
|
bool $verified = false,
|
||||||
?string $version = null,
|
?string $version = null,
|
||||||
array $metadata = []
|
array $metadata = [],
|
||||||
|
?string $downloader = null,
|
||||||
): DownloadResult {
|
): DownloadResult {
|
||||||
// judge if it is archive or just a pure file
|
// judge if it is archive or just a pure file
|
||||||
$cache_type = self::isArchiveFile($filename) ? 'archive' : '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(
|
public static function file(
|
||||||
@ -71,10 +74,11 @@ class DownloadResult
|
|||||||
array $config,
|
array $config,
|
||||||
bool $verified = false,
|
bool $verified = false,
|
||||||
?string $version = null,
|
?string $version = null,
|
||||||
array $metadata = []
|
array $metadata = [],
|
||||||
|
?string $downloader = null,
|
||||||
): DownloadResult {
|
): DownloadResult {
|
||||||
$cache_type = self::isArchiveFile($filename) ? 'archive' : 'file';
|
$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 null|string $version Version string (tag, branch, or commit)
|
||||||
* @param array $metadata Additional metadata (e.g., commit hash)
|
* @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 null|string $version Version string if known
|
||||||
* @param array $metadata Additional metadata
|
* @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->extract,
|
||||||
$this->verified,
|
$this->verified,
|
||||||
$version,
|
$version,
|
||||||
$this->metadata
|
$this->metadata,
|
||||||
|
$this->downloader,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,7 +159,8 @@ class DownloadResult
|
|||||||
$this->extract,
|
$this->extract,
|
||||||
$this->verified,
|
$this->verified,
|
||||||
$this->version,
|
$this->version,
|
||||||
array_merge($this->metadata, [$key => $value])
|
array_merge($this->metadata, [$key => $value]),
|
||||||
|
$this->downloader,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -36,6 +36,6 @@ class BitBucketTag implements DownloadTypeInterface
|
|||||||
$path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename;
|
$path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename;
|
||||||
logger()->debug("Downloading {$name} version {$ver} from BitBucket: {$download_url}");
|
logger()->debug("Downloading {$name} version {$ver} from BitBucket: {$download_url}");
|
||||||
default_shell()->executeCurlDownload($download_url, $path, retries: $downloader->getRetry());
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace StaticPHP\Artifact\Downloader\Type;
|
||||||
|
|
||||||
|
use StaticPHP\Artifact\ArtifactDownloader;
|
||||||
|
|
||||||
|
interface CheckUpdateInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Check if an update is available for the given artifact.
|
||||||
|
*
|
||||||
|
* @param string $name the name of the artifact
|
||||||
|
* @param array $config the configuration for the artifact
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
15
src/StaticPHP/Artifact/Downloader/Type/CheckUpdateResult.php
Normal file
15
src/StaticPHP/Artifact/Downloader/Type/CheckUpdateResult.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace StaticPHP\Artifact\Downloader\Type;
|
||||||
|
|
||||||
|
readonly class CheckUpdateResult
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public ?string $old,
|
||||||
|
public ?string $new,
|
||||||
|
public bool $needUpdate,
|
||||||
|
public bool $unsupported = false,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@ -9,9 +9,34 @@ use StaticPHP\Artifact\Downloader\DownloadResult;
|
|||||||
use StaticPHP\Exception\DownloaderException;
|
use StaticPHP\Exception\DownloaderException;
|
||||||
|
|
||||||
/** filelist */
|
/** filelist */
|
||||||
class FileList implements DownloadTypeInterface
|
class FileList implements DownloadTypeInterface, CheckUpdateInterface
|
||||||
{
|
{
|
||||||
public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult
|
public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult
|
||||||
|
{
|
||||||
|
[$filename, $version, $versions] = $this->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 !== $old_version,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function fetchFileList(string $name, array $config, ArtifactDownloader $downloader): array
|
||||||
{
|
{
|
||||||
logger()->debug("Fetching file list from {$config['url']}");
|
logger()->debug("Fetching file list from {$config['url']}");
|
||||||
$page = default_shell()->executeCurl($config['url'], retries: $downloader->getRetry());
|
$page = default_shell()->executeCurl($config['url'], retries: $downloader->getRetry());
|
||||||
@ -33,15 +58,6 @@ class FileList implements DownloadTypeInterface
|
|||||||
uksort($versions, 'version_compare');
|
uksort($versions, 'version_compare');
|
||||||
$filename = end($versions);
|
$filename = end($versions);
|
||||||
$version = array_key_last($versions);
|
$version = array_key_last($versions);
|
||||||
if (isset($config['download-url'])) {
|
return [$filename, $version, $versions];
|
||||||
$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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ use StaticPHP\Exception\DownloaderException;
|
|||||||
use StaticPHP\Util\FileSystem;
|
use StaticPHP\Util\FileSystem;
|
||||||
|
|
||||||
/** git */
|
/** git */
|
||||||
class Git implements DownloadTypeInterface
|
class Git implements DownloadTypeInterface, CheckUpdateInterface
|
||||||
{
|
{
|
||||||
public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult
|
public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult
|
||||||
{
|
{
|
||||||
@ -21,8 +21,11 @@ class Git implements DownloadTypeInterface
|
|||||||
// direct branch clone
|
// direct branch clone
|
||||||
if (isset($config['rev'])) {
|
if (isset($config['rev'])) {
|
||||||
default_shell()->executeGitClone($config['url'], $config['rev'], $path, $shallow, $config['submodules'] ?? null);
|
default_shell()->executeGitClone($config['url'], $config['rev'], $path, $shallow, $config['submodules'] ?? null);
|
||||||
$version = "dev-{$config['rev']}";
|
$shell = PHP_OS_FAMILY === 'Windows' ? cmd(false) : shell(false);
|
||||||
return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version);
|
$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);
|
||||||
}
|
}
|
||||||
if (!isset($config['regex'])) {
|
if (!isset($config['regex'])) {
|
||||||
throw new DownloaderException('Either "rev" or "regex" must be specified for git download type.');
|
throw new DownloaderException('Either "rev" or "regex" must be specified for git download type.');
|
||||||
@ -64,8 +67,62 @@ class Git implements DownloadTypeInterface
|
|||||||
$branch = $matched_version_branch[$version];
|
$branch = $matched_version_branch[$version];
|
||||||
logger()->info("Matched version {$version} from branch {$branch} for {$name}");
|
logger()->info("Matched version {$version} from branch {$branch} for {$name}");
|
||||||
default_shell()->executeGitClone($config['url'], $branch, $path, $shallow, $config['submodules'] ?? null);
|
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).");
|
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, 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;
|
||||||
|
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 !== $old_version,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new DownloaderException("No matching branch found for regex {$config['regex']}.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ use StaticPHP\Artifact\Downloader\DownloadResult;
|
|||||||
use StaticPHP\Exception\DownloaderException;
|
use StaticPHP\Exception\DownloaderException;
|
||||||
|
|
||||||
/** ghrel */
|
/** ghrel */
|
||||||
class GitHubRelease implements DownloadTypeInterface, ValidatorInterface
|
class GitHubRelease implements DownloadTypeInterface, ValidatorInterface, CheckUpdateInterface
|
||||||
{
|
{
|
||||||
use GitHubTokenSetupTrait;
|
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
|
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 = str_replace('{repo}', $repo, self::API_URL);
|
||||||
$url .= ($query ?? '');
|
$url .= ($query ?? '');
|
||||||
$headers = $this->getGitHubTokenHeaders();
|
$headers = $this->getGitHubTokenHeaders();
|
||||||
@ -95,7 +96,7 @@ class GitHubRelease implements DownloadTypeInterface, ValidatorInterface
|
|||||||
$path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename;
|
$path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename;
|
||||||
logger()->debug("Downloading {$name} asset from URL: {$asset_url}");
|
logger()->debug("Downloading {$name} asset from URL: {$asset_url}");
|
||||||
default_shell()->executeCurlDownload($asset_url, $path, headers: $headers, retries: $downloader->getRetry());
|
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
|
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");
|
logger()->debug("No sha256 digest found for GitHub release asset of {$name}, skipping hash validation");
|
||||||
return true;
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ use StaticPHP\Exception\DownloaderException;
|
|||||||
|
|
||||||
/** ghtar */
|
/** ghtar */
|
||||||
/** ghtagtar */
|
/** ghtagtar */
|
||||||
class GitHubTarball implements DownloadTypeInterface
|
class GitHubTarball implements DownloadTypeInterface, CheckUpdateInterface
|
||||||
{
|
{
|
||||||
use GitHubTokenSetupTrait;
|
use GitHubTokenSetupTrait;
|
||||||
|
|
||||||
@ -42,12 +42,12 @@ class GitHubTarball implements DownloadTypeInterface
|
|||||||
}
|
}
|
||||||
if ($match_url === null) {
|
if ($match_url === null) {
|
||||||
$url = $rel['tarball_url'] ?? null;
|
$url = $rel['tarball_url'] ?? null;
|
||||||
$version = $rel['tag_name'] ?? null;
|
$version = $rel['tag_name'] ?? $rel['name'] ?? null;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (preg_match("|{$match_url}|", $rel['tarball_url'] ?? '')) {
|
if (preg_match("|{$match_url}|", $rel['tarball_url'] ?? '')) {
|
||||||
$url = $rel['tarball_url'];
|
$url = $rel['tarball_url'];
|
||||||
$version = $rel['tag_name'] ?? null;
|
$version = $rel['tag_name'] ?? $rel['name'] ?? null;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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);
|
[$url, $filename] = $this->getGitHubTarballInfo($name, $config['repo'], $rel_type, $config['prefer-stable'] ?? true, $config['match'] ?? null, $name, $config['query'] ?? null);
|
||||||
$path = DOWNLOAD_PATH . "/{$filename}";
|
$path = DOWNLOAD_PATH . "/{$filename}";
|
||||||
default_shell()->executeCurlDownload($url, $path, headers: $this->getGitHubTokenHeaders());
|
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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,10 +16,12 @@ trait GitHubTokenSetupTrait
|
|||||||
// GITHUB_TOKEN support
|
// GITHUB_TOKEN support
|
||||||
if (($token = getenv('GITHUB_TOKEN')) !== false && ($user = getenv('GITHUB_USER')) !== false) {
|
if (($token = getenv('GITHUB_TOKEN')) !== false && ($user = getenv('GITHUB_USER')) !== false) {
|
||||||
logger()->debug("Using 'GITHUB_TOKEN' with user {$user} for authentication");
|
logger()->debug("Using 'GITHUB_TOKEN' with user {$user} for authentication");
|
||||||
|
spc_add_log_filter([$user, $token]);
|
||||||
return ['Authorization: Basic ' . base64_encode("{$user}:{$token}")];
|
return ['Authorization: Basic ' . base64_encode("{$user}:{$token}")];
|
||||||
}
|
}
|
||||||
if (($token = getenv('GITHUB_TOKEN')) !== false) {
|
if (($token = getenv('GITHUB_TOKEN')) !== false) {
|
||||||
logger()->debug("Using 'GITHUB_TOKEN' for authentication");
|
logger()->debug("Using 'GITHUB_TOKEN' for authentication");
|
||||||
|
spc_add_log_filter($token);
|
||||||
return ["Authorization: Bearer {$token}"];
|
return ["Authorization: Bearer {$token}"];
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@ -26,7 +26,7 @@ class HostedPackageBin implements DownloadTypeInterface
|
|||||||
public static function getReleaseInfo(): array
|
public static function getReleaseInfo(): array
|
||||||
{
|
{
|
||||||
if (empty(self::$release_info)) {
|
if (empty(self::$release_info)) {
|
||||||
$rel = (new GitHubRelease())->getGitHubReleases('hosted', self::BASE_REPO);
|
$rel = new GitHubRelease()->getGitHubReleases('hosted', self::BASE_REPO);
|
||||||
if (empty($rel)) {
|
if (empty($rel)) {
|
||||||
throw new DownloaderException('No releases found for hosted package-bin');
|
throw new DownloaderException('No releases found for hosted package-bin');
|
||||||
}
|
}
|
||||||
@ -55,7 +55,7 @@ class HostedPackageBin implements DownloadTypeInterface
|
|||||||
$path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename;
|
$path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename;
|
||||||
$headers = $this->getGitHubTokenHeaders();
|
$headers = $this->getGitHubTokenHeaders();
|
||||||
default_shell()->executeCurlDownload($download_url, $path, headers: $headers, retries: $downloader->getRetry());
|
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}");
|
throw new DownloaderException("No matching asset found for hosted package-bin {$name}: {$find_str}");
|
||||||
|
|||||||
@ -13,6 +13,6 @@ class LocalDir implements DownloadTypeInterface
|
|||||||
public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult
|
public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult
|
||||||
{
|
{
|
||||||
logger()->debug("Using local source directory for {$name} from {$config['dirname']}");
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
79
src/StaticPHP/Artifact/Downloader/Type/PECL.php
Normal file
79
src/StaticPHP/Artifact/Downloader/Type/PECL.php
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace StaticPHP\Artifact\Downloader\Type;
|
||||||
|
|
||||||
|
use StaticPHP\Artifact\ArtifactDownloader;
|
||||||
|
use StaticPHP\Artifact\Downloader\DownloadResult;
|
||||||
|
use StaticPHP\Exception\DownloaderException;
|
||||||
|
|
||||||
|
/* pecl */
|
||||||
|
class PECL implements DownloadTypeInterface, CheckUpdateInterface
|
||||||
|
{
|
||||||
|
private const string PECL_BASE_URL = 'https://pecl.php.net';
|
||||||
|
|
||||||
|
/** REST API: returns XML with <r><v>VERSION</v><s>STATE</s></r> 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 !== $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 <r><v>VERSION</v><s>STATE</s></r>
|
||||||
|
preg_match_all('/<r><v>(?P<version>[^<]+)<\/v><s>(?P<state>[^<]+)<\/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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,28 +9,13 @@ use StaticPHP\Artifact\Downloader\DownloadResult;
|
|||||||
use StaticPHP\Exception\DownloaderException;
|
use StaticPHP\Exception\DownloaderException;
|
||||||
|
|
||||||
/** pie */
|
/** pie */
|
||||||
class PIE implements DownloadTypeInterface
|
class PIE implements DownloadTypeInterface, CheckUpdateInterface
|
||||||
{
|
{
|
||||||
public const string PACKAGIST_URL = 'https://repo.packagist.org/p2/';
|
public const string PACKAGIST_URL = 'https://repo.packagist.org/p2/';
|
||||||
|
|
||||||
public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult
|
public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult
|
||||||
{
|
{
|
||||||
$packagist_url = self::PACKAGIST_URL . "{$config['repo']}.json";
|
$first = $this->fetchPackagistInfo($name, $config, $downloader);
|
||||||
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");
|
|
||||||
}
|
|
||||||
// get download link from dist
|
// get download link from dist
|
||||||
$dist_url = $first['dist']['url'] ?? null;
|
$dist_url = $first['dist']['url'] ?? null;
|
||||||
$dist_type = $first['dist']['type'] ?? null;
|
$dist_type = $first['dist']['type'] ?? null;
|
||||||
@ -42,6 +27,39 @@ class PIE implements DownloadTypeInterface
|
|||||||
$filename = "{$name}-{$version}." . ($dist_type === 'zip' ? 'zip' : 'tar.gz');
|
$filename = "{$name}-{$version}." . ($dist_type === 'zip' ? 'zip' : 'tar.gz');
|
||||||
$path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename;
|
$path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename;
|
||||||
default_shell()->executeCurlDownload($dist_url, $path, retries: $downloader->getRetry());
|
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, version: $version, 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 || $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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,11 +8,17 @@ use StaticPHP\Artifact\ArtifactDownloader;
|
|||||||
use StaticPHP\Artifact\Downloader\DownloadResult;
|
use StaticPHP\Artifact\Downloader\DownloadResult;
|
||||||
use StaticPHP\Exception\DownloaderException;
|
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}';
|
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 = '';
|
private ?string $sha256 = '';
|
||||||
|
|
||||||
@ -22,18 +28,9 @@ class PhpRelease implements DownloadTypeInterface, ValidatorInterface
|
|||||||
// Handle 'git' version to clone from php-src repository
|
// Handle 'git' version to clone from php-src repository
|
||||||
if ($phpver === 'git') {
|
if ($phpver === 'git') {
|
||||||
$this->sha256 = null;
|
$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);
|
||||||
}
|
|
||||||
|
|
||||||
// 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, $config, $downloader);
|
||||||
$version = $info['version'];
|
$version = $info['version'];
|
||||||
foreach ($info['source'] as $source) {
|
foreach ($info['source'] as $source) {
|
||||||
if (str_ends_with($source['filename'], '.tar.xz')) {
|
if (str_ends_with($source['filename'], '.tar.xz')) {
|
||||||
@ -45,11 +42,12 @@ class PhpRelease implements DownloadTypeInterface, ValidatorInterface
|
|||||||
if (!isset($filename)) {
|
if (!isset($filename)) {
|
||||||
throw new DownloaderException("No suitable source tarball found for PHP version {$version}");
|
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}");
|
logger()->debug("Downloading PHP release {$version} from {$url}");
|
||||||
$path = DOWNLOAD_PATH . "/{$filename}";
|
$path = DOWNLOAD_PATH . "/{$filename}";
|
||||||
default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry());
|
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
|
public function validate(string $name, array $config, ArtifactDownloader $downloader, DownloadResult $result): bool
|
||||||
@ -73,4 +71,46 @@ class PhpRelease implements DownloadTypeInterface, ValidatorInterface
|
|||||||
logger()->debug("SHA256 checksum validated successfully for {$name}.");
|
logger()->debug("SHA256 checksum validated successfully for {$name}.");
|
||||||
return true;
|
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, $config, $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, array $config, 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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$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($url, 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,6 @@ class Url implements DownloadTypeInterface
|
|||||||
logger()->debug("Downloading {$name} from URL: {$url}");
|
logger()->debug("Downloading {$name} from URL: {$url}");
|
||||||
$version = $config['version'] ?? null;
|
$version = $config['version'] ?? null;
|
||||||
default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry());
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/StaticPHP/Attribute/Artifact/CustomBinaryCheckUpdate.php
Normal file
11
src/StaticPHP/Attribute/Artifact/CustomBinaryCheckUpdate.php
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace StaticPHP\Attribute\Artifact;
|
||||||
|
|
||||||
|
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
|
||||||
|
class CustomBinaryCheckUpdate
|
||||||
|
{
|
||||||
|
public function __construct(public string $artifact_name, public array $support_os) {}
|
||||||
|
}
|
||||||
11
src/StaticPHP/Attribute/Artifact/CustomSourceCheckUpdate.php
Normal file
11
src/StaticPHP/Attribute/Artifact/CustomSourceCheckUpdate.php
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace StaticPHP\Attribute\Artifact;
|
||||||
|
|
||||||
|
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
|
||||||
|
class CustomSourceCheckUpdate
|
||||||
|
{
|
||||||
|
public function __construct(public string $artifact_name) {}
|
||||||
|
}
|
||||||
79
src/StaticPHP/Command/CheckUpdateCommand.php
Normal file
79
src/StaticPHP/Command/CheckUpdateCommand.php
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
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;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
|
||||||
|
#[AsCommand('check-update', description: 'Check for updates for a specific artifact')]
|
||||||
|
class CheckUpdateCommand extends BaseCommand
|
||||||
|
{
|
||||||
|
public bool $no_motd = true;
|
||||||
|
|
||||||
|
public function configure(): void
|
||||||
|
{
|
||||||
|
$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');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$artifact_arg = $this->input->getArgument('artifact');
|
||||||
|
if ($artifact_arg === null) {
|
||||||
|
$artifacts = ApplicationContext::get(ArtifactCache::class)->getCachedArtifactNames();
|
||||||
|
if (empty($artifacts)) {
|
||||||
|
$this->output->writeln('<comment>No downloaded artifacts found.</comment>');
|
||||||
|
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 ($results as $artifact => $result) {
|
||||||
|
$outputs[$artifact] = [
|
||||||
|
'need-update' => $result->needUpdate,
|
||||||
|
'unsupported' => $result->unsupported,
|
||||||
|
'old' => $result->old,
|
||||||
|
'new' => $result->new,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$this->output->writeln(json_encode($outputs, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
|
||||||
|
return static::OK;
|
||||||
|
}
|
||||||
|
$downloader->checkUpdates($artifacts, bare: $bare, onResult: function (string $artifact, CheckUpdateResult $result) {
|
||||||
|
if ($result->unsupported) {
|
||||||
|
$this->output->writeln("Artifact <info>{$artifact}</info> does not support update checking, <comment>skipped</comment>");
|
||||||
|
} elseif (!$result->needUpdate) {
|
||||||
|
$ver = $result->new ? "(<comment>{$result->new}</comment>)" : '';
|
||||||
|
$this->output->writeln("Artifact <info>{$artifact}</info> is already up to date {$ver}");
|
||||||
|
} else {
|
||||||
|
[$old, $new] = [$result->old ?? 'unavailable', $result->new ?? 'unknown'];
|
||||||
|
$this->output->writeln("Update available for <info>{$artifact}</info>: <comment>{$old}</comment> -> <comment>{$new}</comment>");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return static::OK;
|
||||||
|
} catch (SPCException $e) {
|
||||||
|
$e->setSimpleOutput();
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,26 +17,28 @@ trait ReturnCode
|
|||||||
{
|
{
|
||||||
public const int OK = 0;
|
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 */
|
// 64-69: reserved for standard errors
|
||||||
public const FAILURE = 1;
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -89,7 +89,8 @@ class ConfigValidator
|
|||||||
'bitbuckettag' => [['repo'], ['extract']],
|
'bitbuckettag' => [['repo'], ['extract']],
|
||||||
'local' => [['dirname'], ['extract']],
|
'local' => [['dirname'], ['extract']],
|
||||||
'pie' => [['repo'], ['extract']],
|
'pie' => [['repo'], ['extract']],
|
||||||
'php-release' => [[], ['extract']],
|
'pecl' => [['name'], ['extract']],
|
||||||
|
'php-release' => [['domain'], ['extract']],
|
||||||
'custom' => [[], ['func']],
|
'custom' => [[], ['func']],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ namespace StaticPHP;
|
|||||||
|
|
||||||
use StaticPHP\Command\BuildLibsCommand;
|
use StaticPHP\Command\BuildLibsCommand;
|
||||||
use StaticPHP\Command\BuildTargetCommand;
|
use StaticPHP\Command\BuildTargetCommand;
|
||||||
|
use StaticPHP\Command\CheckUpdateCommand;
|
||||||
use StaticPHP\Command\Dev\DumpCapabilitiesCommand;
|
use StaticPHP\Command\Dev\DumpCapabilitiesCommand;
|
||||||
use StaticPHP\Command\Dev\DumpStagesCommand;
|
use StaticPHP\Command\Dev\DumpStagesCommand;
|
||||||
use StaticPHP\Command\Dev\EnvCommand;
|
use StaticPHP\Command\Dev\EnvCommand;
|
||||||
@ -63,6 +64,7 @@ class ConsoleApplication extends Application
|
|||||||
new SPCConfigCommand(),
|
new SPCConfigCommand(),
|
||||||
new DumpLicenseCommand(),
|
new DumpLicenseCommand(),
|
||||||
new ResetCommand(),
|
new ResetCommand(),
|
||||||
|
new CheckUpdateCommand(),
|
||||||
|
|
||||||
// dev commands
|
// dev commands
|
||||||
new ShellCommand(),
|
new ShellCommand(),
|
||||||
|
|||||||
@ -29,12 +29,6 @@ class ExceptionHandler
|
|||||||
RegistryException::class,
|
RegistryException::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
public const array MINOR_LOG_EXCEPTIONS = [
|
|
||||||
InterruptException::class,
|
|
||||||
WrongUsageException::class,
|
|
||||||
RegistryException::class,
|
|
||||||
];
|
|
||||||
|
|
||||||
/** @var array<string, mixed> Build PHP extra info binding */
|
/** @var array<string, mixed> Build PHP extra info binding */
|
||||||
private static array $build_php_extra_info = [];
|
private static array $build_php_extra_info = [];
|
||||||
|
|
||||||
@ -57,10 +51,7 @@ class ExceptionHandler
|
|||||||
};
|
};
|
||||||
self::logError($head_msg);
|
self::logError($head_msg);
|
||||||
|
|
||||||
// ----------------------------------------
|
if ($e->isSimpleOutput()) {
|
||||||
$minor_logs = in_array($class, self::MINOR_LOG_EXCEPTIONS, true);
|
|
||||||
|
|
||||||
if ($minor_logs) {
|
|
||||||
return self::getReturnCode($e);
|
return self::getReturnCode($e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,7 +115,7 @@ class ExceptionHandler
|
|||||||
$msg = explode("\n", (string) $message);
|
$msg = explode("\n", (string) $message);
|
||||||
foreach ($msg as $v) {
|
foreach ($msg as $v) {
|
||||||
$line = str_pad($v, strlen($v) + $indent_space, ' ', STR_PAD_LEFT);
|
$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) {
|
if ($output_log) {
|
||||||
InteractiveTerm::plain(ConsoleColor::$color($line) . '', 'error');
|
InteractiveTerm::plain(ConsoleColor::$color($line) . '', 'error');
|
||||||
}
|
}
|
||||||
@ -283,6 +274,6 @@ class ExceptionHandler
|
|||||||
self::printArrayInfo($info);
|
self::printArrayInfo($info);
|
||||||
}
|
}
|
||||||
|
|
||||||
self::logError("---------------------------------------------------------\n", color: 'none');
|
self::logError("-----------------------------------------------------------\n", color: 'none');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,4 +7,7 @@ namespace StaticPHP\Exception;
|
|||||||
/**
|
/**
|
||||||
* Exception caused by manual intervention.
|
* Exception caused by manual intervention.
|
||||||
*/
|
*/
|
||||||
class InterruptException extends SPCException {}
|
class InterruptException extends SPCException
|
||||||
|
{
|
||||||
|
protected bool $simple_output = true;
|
||||||
|
}
|
||||||
|
|||||||
@ -4,4 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace StaticPHP\Exception;
|
namespace StaticPHP\Exception;
|
||||||
|
|
||||||
class RegistryException extends SPCException {}
|
class RegistryException extends SPCException
|
||||||
|
{
|
||||||
|
protected bool $simple_output = true;
|
||||||
|
}
|
||||||
|
|||||||
@ -20,6 +20,8 @@ use StaticPHP\Package\TargetPackage;
|
|||||||
*/
|
*/
|
||||||
abstract class SPCException extends \Exception
|
abstract class SPCException extends \Exception
|
||||||
{
|
{
|
||||||
|
protected bool $simple_output = false;
|
||||||
|
|
||||||
/** @var null|array Package information */
|
/** @var null|array Package information */
|
||||||
private ?array $package_info = null;
|
private ?array $package_info = null;
|
||||||
|
|
||||||
@ -155,6 +157,16 @@ abstract class SPCException extends \Exception
|
|||||||
return $this->extra_log_files;
|
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.
|
* Load stack trace information to detect Package, Builder, and Installer context.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -10,4 +10,7 @@ namespace StaticPHP\Exception;
|
|||||||
* This exception is used to indicate that the SPC is being used incorrectly.
|
* 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.
|
* 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;
|
||||||
|
}
|
||||||
|
|||||||
@ -80,7 +80,7 @@ class PhpExtensionPackage extends Package
|
|||||||
}
|
}
|
||||||
$escapedPath = str_replace("'", '', escapeshellarg(BUILD_ROOT_PATH)) !== BUILD_ROOT_PATH || str_contains(BUILD_ROOT_PATH, ' ') ? escapeshellarg(BUILD_ROOT_PATH) : BUILD_ROOT_PATH;
|
$escapedPath = str_replace("'", '', escapeshellarg(BUILD_ROOT_PATH)) !== BUILD_ROOT_PATH || str_contains(BUILD_ROOT_PATH, ' ') ? escapeshellarg(BUILD_ROOT_PATH) : BUILD_ROOT_PATH;
|
||||||
$name = str_replace('_', '-', $this->getExtensionName());
|
$name = str_replace('_', '-', $this->getExtensionName());
|
||||||
$ext_config = PackageConfig::get($name, 'php-extension', []);
|
$ext_config = PackageConfig::get($this->getName(), 'php-extension', []);
|
||||||
|
|
||||||
$arg_type = match (SystemTarget::getTargetOS()) {
|
$arg_type = match (SystemTarget::getTargetOS()) {
|
||||||
'Windows' => $ext_config['arg-type@windows'] ?? $ext_config['arg-type'] ?? 'enable',
|
'Windows' => $ext_config['arg-type@windows'] ?? $ext_config['arg-type'] ?? 'enable',
|
||||||
|
|||||||
@ -9,7 +9,9 @@ use StaticPHP\Attribute\Artifact\AfterBinaryExtract;
|
|||||||
use StaticPHP\Attribute\Artifact\AfterSourceExtract;
|
use StaticPHP\Attribute\Artifact\AfterSourceExtract;
|
||||||
use StaticPHP\Attribute\Artifact\BinaryExtract;
|
use StaticPHP\Attribute\Artifact\BinaryExtract;
|
||||||
use StaticPHP\Attribute\Artifact\CustomBinary;
|
use StaticPHP\Attribute\Artifact\CustomBinary;
|
||||||
|
use StaticPHP\Attribute\Artifact\CustomBinaryCheckUpdate;
|
||||||
use StaticPHP\Attribute\Artifact\CustomSource;
|
use StaticPHP\Attribute\Artifact\CustomSource;
|
||||||
|
use StaticPHP\Attribute\Artifact\CustomSourceCheckUpdate;
|
||||||
use StaticPHP\Attribute\Artifact\SourceExtract;
|
use StaticPHP\Attribute\Artifact\SourceExtract;
|
||||||
use StaticPHP\Config\ArtifactConfig;
|
use StaticPHP\Config\ArtifactConfig;
|
||||||
use StaticPHP\Exception\ValidationException;
|
use StaticPHP\Exception\ValidationException;
|
||||||
@ -61,7 +63,9 @@ class ArtifactLoader
|
|||||||
|
|
||||||
foreach ($ref->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
|
foreach ($ref->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
|
||||||
self::processCustomSourceAttribute($ref, $method, $class_instance);
|
self::processCustomSourceAttribute($ref, $method, $class_instance);
|
||||||
|
self::processCustomSourceCheckUpdateAttribute($ref, $method, $class_instance);
|
||||||
self::processCustomBinaryAttribute($ref, $method, $class_instance);
|
self::processCustomBinaryAttribute($ref, $method, $class_instance);
|
||||||
|
self::processCustomBinaryCheckUpdateAttribute($ref, $method, $class_instance);
|
||||||
self::processSourceExtractAttribute($ref, $method, $class_instance);
|
self::processSourceExtractAttribute($ref, $method, $class_instance);
|
||||||
self::processBinaryExtractAttribute($ref, $method, $class_instance);
|
self::processBinaryExtractAttribute($ref, $method, $class_instance);
|
||||||
self::processAfterSourceExtractAttribute($ref, $method, $class_instance);
|
self::processAfterSourceExtractAttribute($ref, $method, $class_instance);
|
||||||
@ -98,6 +102,24 @@ class ArtifactLoader
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
* Process #[CustomBinary] attribute.
|
||||||
*/
|
*/
|
||||||
@ -118,6 +140,26 @@ class ArtifactLoader
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
* Process #[SourceExtract] attribute.
|
||||||
* This attribute allows completely taking over the source extraction process.
|
* This attribute allows completely taking over the source extraction process.
|
||||||
|
|||||||
@ -25,7 +25,7 @@ class DefaultShell extends Shell
|
|||||||
/**
|
/**
|
||||||
* Execute a cURL command to fetch data from a URL.
|
* 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) {
|
foreach ($hooks as $hook) {
|
||||||
$hook($method, $url, $headers);
|
$hook($method, $url, $headers);
|
||||||
@ -39,7 +39,8 @@ class DefaultShell extends Shell
|
|||||||
};
|
};
|
||||||
$header_arg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $headers));
|
$header_arg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $headers));
|
||||||
$retry_arg = $retries > 0 ? "--retry {$retries}" : '';
|
$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);
|
$this->logCommandInfo($cmd);
|
||||||
$result = $this->passthru($cmd, capture_output: true, throw_on_error: false);
|
$result = $this->passthru($cmd, capture_output: true, throw_on_error: false);
|
||||||
@ -72,7 +73,7 @@ class DefaultShell extends Shell
|
|||||||
$header_arg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $headers));
|
$header_arg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $headers));
|
||||||
$retry_arg = $retries > 0 ? "--retry {$retries}" : '';
|
$retry_arg = $retries > 0 ? "--retry {$retries}" : '';
|
||||||
$check = $this->console_putput ? '#' : 's';
|
$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);
|
$this->logCommandInfo($cmd);
|
||||||
logger()->debug('[CURL DOWNLOAD] ' . $cmd);
|
logger()->debug('[CURL DOWNLOAD] ' . $cmd);
|
||||||
$this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true);
|
$this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true);
|
||||||
@ -93,7 +94,7 @@ class DefaultShell extends Shell
|
|||||||
$path_arg = escapeshellarg($path);
|
$path_arg = escapeshellarg($path);
|
||||||
$shallow_arg = $shallow ? '--depth 1 --single-branch' : '';
|
$shallow_arg = $shallow ? '--depth 1 --single-branch' : '';
|
||||||
$submodules_arg = ($submodules === null && $shallow) ? '--recursive --shallow-submodules' : ($submodules === null ? '--recursive' : '');
|
$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);
|
$this->logCommandInfo($cmd);
|
||||||
logger()->debug("[GIT CLONE] {$cmd}");
|
logger()->debug("[GIT CLONE] {$cmd}");
|
||||||
$this->passthru($cmd, $this->console_putput);
|
$this->passthru($cmd, $this->console_putput);
|
||||||
|
|||||||
@ -114,22 +114,22 @@ abstract class Shell
|
|||||||
if (!$this->enable_log_file) {
|
if (!$this->enable_log_file) {
|
||||||
return;
|
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');
|
$log_file = fopen(SPC_SHELL_LOG, 'a');
|
||||||
fwrite($log_file, "\n>>>>>>>>>>>>>>>>>>>>>>>>>> [" . date('Y-m-d H:i:s') . "]\n");
|
spc_write_log($log_file, "\n>>>>>>>>>>>>>>>>>>>>>>>>>> [" . date('Y-m-d H:i:s') . "]\n");
|
||||||
fwrite($log_file, "> Executing command: {$cmd}\n");
|
spc_write_log($log_file, "> Executing command: {$cmd}\n");
|
||||||
// get the backtrace to find the file and line number
|
// get the backtrace to find the file and line number
|
||||||
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
|
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
|
||||||
if (isset($backtrace[1]['file'], $backtrace[1]['line'])) {
|
if (isset($backtrace[1]['file'], $backtrace[1]['line'])) {
|
||||||
$file = $backtrace[1]['file'];
|
$file = $backtrace[1]['file'];
|
||||||
$line = $backtrace[1]['line'];
|
$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) {
|
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 @@ abstract class Shell
|
|||||||
): array {
|
): array {
|
||||||
$file_res = null;
|
$file_res = null;
|
||||||
if ($this->enable_log_file) {
|
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');
|
$file_res = fopen(SPC_SHELL_LOG, 'a');
|
||||||
}
|
}
|
||||||
if ($console_output) {
|
if ($console_output) {
|
||||||
@ -194,10 +194,10 @@ abstract class Shell
|
|||||||
foreach ([$pipes[1], $pipes[2]] as $pipe) {
|
foreach ([$pipes[1], $pipes[2]] as $pipe) {
|
||||||
while (($chunk = fread($pipe, 8192)) !== false && $chunk !== '') {
|
while (($chunk = fread($pipe, 8192)) !== false && $chunk !== '') {
|
||||||
if ($console_output) {
|
if ($console_output) {
|
||||||
fwrite($console_res, $chunk);
|
spc_write_log($console_res, $chunk);
|
||||||
}
|
}
|
||||||
if ($file_res !== null) {
|
if ($file_res !== null) {
|
||||||
fwrite($file_res, $chunk);
|
spc_write_log($file_res, $chunk);
|
||||||
}
|
}
|
||||||
if ($capture_output) {
|
if ($capture_output) {
|
||||||
$output_value .= $chunk;
|
$output_value .= $chunk;
|
||||||
@ -207,7 +207,7 @@ abstract class Shell
|
|||||||
// check exit code
|
// check exit code
|
||||||
if ($throw_on_error && $status['exitcode'] !== 0) {
|
if ($throw_on_error && $status['exitcode'] !== 0) {
|
||||||
if ($file_res !== null) {
|
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(
|
throw new ExecutionException(
|
||||||
cmd: $original_command ?? $cmd,
|
cmd: $original_command ?? $cmd,
|
||||||
@ -238,10 +238,10 @@ abstract class Shell
|
|||||||
foreach ($read as $pipe) {
|
foreach ($read as $pipe) {
|
||||||
while (($chunk = fread($pipe, 8192)) !== false && $chunk !== '') {
|
while (($chunk = fread($pipe, 8192)) !== false && $chunk !== '') {
|
||||||
if ($console_output) {
|
if ($console_output) {
|
||||||
fwrite($console_res, $chunk);
|
spc_write_log($console_res, $chunk);
|
||||||
}
|
}
|
||||||
if ($file_res !== null) {
|
if ($file_res !== null) {
|
||||||
fwrite($file_res, $chunk);
|
spc_write_log($file_res, $chunk);
|
||||||
}
|
}
|
||||||
if ($capture_output) {
|
if ($capture_output) {
|
||||||
$output_value .= $chunk;
|
$output_value .= $chunk;
|
||||||
|
|||||||
@ -52,7 +52,7 @@ if (filter_var(getenv('SPC_ENABLE_LOG_FILE'), FILTER_VALIDATE_BOOLEAN)) {
|
|||||||
$log_file_fd = fopen(SPC_OUTPUT_LOG, 'a');
|
$log_file_fd = fopen(SPC_OUTPUT_LOG, 'a');
|
||||||
$ob_logger->addLogCallback(function ($level, $output) use ($log_file_fd) {
|
$ob_logger->addLogCallback(function ($level, $output) use ($log_file_fd) {
|
||||||
if ($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;
|
return true;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -104,7 +104,7 @@ const SPC_DOWNLOAD_TYPE_DISPLAY_NAME = [
|
|||||||
'local' => 'local dir',
|
'local' => 'local dir',
|
||||||
'pie' => 'PHP Installer for Extensions (PIE)',
|
'pie' => 'PHP Installer for Extensions (PIE)',
|
||||||
'url' => 'url',
|
'url' => 'url',
|
||||||
'php-release' => 'php.net',
|
'php-release' => 'PHP website release',
|
||||||
'custom' => 'custom downloader',
|
'custom' => 'custom downloader',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -132,6 +132,32 @@ function patch_point(): string
|
|||||||
return '';
|
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
|
function patch_point_interrupt(int $retcode, string $msg = ''): InterruptException
|
||||||
{
|
{
|
||||||
return new InterruptException(message: $msg, code: $retcode);
|
return new InterruptException(message: $msg, code: $retcode);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user