diff --git a/config/downloader.php b/config/downloader.php
index 48710a88..2d81a57e 100644
--- a/config/downloader.php
+++ b/config/downloader.php
@@ -10,6 +10,7 @@ use StaticPHP\Artifact\Downloader\Type\GitHubRelease;
use StaticPHP\Artifact\Downloader\Type\GitHubTarball;
use StaticPHP\Artifact\Downloader\Type\HostedPackageBin;
use StaticPHP\Artifact\Downloader\Type\LocalDir;
+use StaticPHP\Artifact\Downloader\Type\PECL;
use StaticPHP\Artifact\Downloader\Type\PhpRelease;
use StaticPHP\Artifact\Downloader\Type\PIE;
use StaticPHP\Artifact\Downloader\Type\Url;
@@ -24,6 +25,7 @@ return [
'ghtagtar' => GitHubTarball::class,
'local' => LocalDir::class,
'pie' => PIE::class,
+ 'pecl' => PECL::class,
'url' => Url::class,
'php-release' => PhpRelease::class,
'hosted' => HostedPackageBin::class,
diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml
index d12cd219..0149fe97 100644
--- a/config/pkg/ext/builtin-extensions.yml
+++ b/config/pkg/ext/builtin-extensions.yml
@@ -1,5 +1,19 @@
ext-bcmath:
type: php-extension
+ext-mbregex:
+ type: php-extension
+ depends:
+ - onig
+ - ext-mbstring
+ php-extension:
+ arg-type: custom
+ build-shared: false
+ build-static: true
+ display-name: mbstring
+ext-mbstring:
+ type: php-extension
+ php-extension:
+ arg-type: custom
ext-openssl:
type: php-extension
depends:
@@ -10,6 +24,21 @@ ext-openssl:
arg-type: custom
arg-type@windows: with
build-with-php: true
+ext-phar:
+ type: php-extension
+ depends:
+ - zlib
+ext-readline:
+ type: php-extension
+ depends:
+ - libedit
+ php-extension:
+ support:
+ Windows: wip
+ BSD: wip
+ arg-type: with-path
+ build-shared: false
+ build-static: true
ext-zlib:
type: php-extension
depends:
diff --git a/config/pkg/ext/ext-amqp.yml b/config/pkg/ext/ext-amqp.yml
index 93756914..1c802360 100644
--- a/config/pkg/ext/ext-amqp.yml
+++ b/config/pkg/ext/ext-amqp.yml
@@ -2,10 +2,8 @@ ext-amqp:
type: php-extension
artifact:
source:
- type: url
- url: 'https://pecl.php.net/get/amqp'
- extract: php-src/ext/amqp
- filename: amqp.tgz
+ type: pecl
+ name: amqp
metadata:
license-files: [LICENSE]
license: PHP-3.01
diff --git a/config/pkg/ext/ext-apcu.yml b/config/pkg/ext/ext-apcu.yml
index 289de301..331b04f7 100644
--- a/config/pkg/ext/ext-apcu.yml
+++ b/config/pkg/ext/ext-apcu.yml
@@ -2,10 +2,8 @@ ext-apcu:
type: php-extension
artifact:
source:
- type: url
- url: 'https://pecl.php.net/get/APCu'
- extract: php-src/ext/apcu
- filename: apcu.tgz
+ type: pecl
+ name: APCu
metadata:
license-files: [LICENSE]
license: PHP-3.01
diff --git a/config/pkg/ext/ext-ast.yml b/config/pkg/ext/ext-ast.yml
new file mode 100644
index 00000000..0684959d
--- /dev/null
+++ b/config/pkg/ext/ext-ast.yml
@@ -0,0 +1,6 @@
+ext-ast:
+ type: php-extension
+ artifact:
+ source:
+ type: pecl
+ name: ast
diff --git a/config/pkg/ext/ext-mbregex.yml b/config/pkg/ext/ext-mbregex.yml
deleted file mode 100644
index ae59f023..00000000
--- a/config/pkg/ext/ext-mbregex.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-ext-mbregex:
- type: php-extension
- depends:
- - onig
- - ext-mbstring
- php-extension:
- arg-type: custom
- build-shared: false
- build-static: true
- display-name: mbstring
diff --git a/config/pkg/ext/ext-mbstring.yml b/config/pkg/ext/ext-mbstring.yml
deleted file mode 100644
index 6583ca61..00000000
--- a/config/pkg/ext/ext-mbstring.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-ext-mbstring:
- type: php-extension
- php-extension:
- arg-type: custom
diff --git a/config/pkg/ext/ext-phar.yml b/config/pkg/ext/ext-phar.yml
deleted file mode 100644
index 3625d2c0..00000000
--- a/config/pkg/ext/ext-phar.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-ext-phar:
- type: php-extension
- depends:
- - zlib
diff --git a/config/pkg/ext/ext-readline.yml b/config/pkg/ext/ext-readline.yml
deleted file mode 100644
index 19b1886c..00000000
--- a/config/pkg/ext/ext-readline.yml
+++ /dev/null
@@ -1,11 +0,0 @@
-ext-readline:
- type: php-extension
- depends:
- - libedit
- php-extension:
- support:
- Windows: wip
- BSD: wip
- arg-type: with-path
- build-shared: false
- build-static: true
diff --git a/src/StaticPHP/Artifact/ArtifactCache.php b/src/StaticPHP/Artifact/ArtifactCache.php
index dcd75ef7..ea3bde41 100644
--- a/src/StaticPHP/Artifact/ArtifactCache.php
+++ b/src/StaticPHP/Artifact/ArtifactCache.php
@@ -163,7 +163,7 @@ class ArtifactCache
throw new SPCInternalException("Invalid lock type '{$lock_type}' for artifact {$artifact_name}");
}
// save cache to file
- file_put_contents($this->cache_file, json_encode($this->cache, JSON_PRETTY_PRINT));
+ file_put_contents($this->cache_file, json_encode($this->cache, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
/**
@@ -281,7 +281,7 @@ class ArtifactCache
*/
public function save(): void
{
- file_put_contents($this->cache_file, json_encode($this->cache, JSON_PRETTY_PRINT));
+ file_put_contents($this->cache_file, json_encode($this->cache, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
private function isObjectDownloaded(?array $object, bool $compare_hash = false): bool
diff --git a/src/StaticPHP/Artifact/Downloader/Type/Git.php b/src/StaticPHP/Artifact/Downloader/Type/Git.php
index 4a712005..1ee9da4d 100644
--- a/src/StaticPHP/Artifact/Downloader/Type/Git.php
+++ b/src/StaticPHP/Artifact/Downloader/Type/Git.php
@@ -22,7 +22,7 @@ class Git implements DownloadTypeInterface, CheckUpdateInterface
if (isset($config['rev'])) {
default_shell()->executeGitClone($config['url'], $config['rev'], $path, $shallow, $config['submodules'] ?? null);
$shell = PHP_OS_FAMILY === 'Windows' ? cmd(false) : shell(false);
- $hash_result = $shell->execWithResult(SPC_GIT_EXEC . ' -C ' . escapeshellarg($path) . ' rev-parse HEAD');
+ $hash_result = $shell->execWithResult(SPC_GIT_EXEC . ' -C ' . escapeshellarg($path) . ' rev-parse --short HEAD');
$hash = ($hash_result[0] === 0 && !empty($hash_result[1])) ? trim($hash_result[1][0]) : '';
$version = $hash !== '' ? "dev-{$config['rev']}+{$hash}" : "dev-{$config['rev']}";
return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version, downloader: static::class);
@@ -80,7 +80,7 @@ class Git implements DownloadTypeInterface, CheckUpdateInterface
if ($result[0] !== 0 || empty($result[1])) {
throw new DownloaderException("Failed to ls-remote from {$config['url']}");
}
- $new_hash = substr($result[1][0], 0, 40);
+ $new_hash = substr($result[1][0], 0, 7);
$new_version = "dev-{$config['rev']}+{$new_hash}";
// Extract stored hash from "dev-{rev}+{hash}", null if bare mode or old format without hash
$old_hash = ($old_version !== null && str_contains($old_version, '+')) ? substr(strrchr($old_version, '+'), 1) : null;
diff --git a/src/StaticPHP/Artifact/Downloader/Type/PECL.php b/src/StaticPHP/Artifact/Downloader/Type/PECL.php
new file mode 100644
index 00000000..0b14b05d
--- /dev/null
+++ b/src/StaticPHP/Artifact/Downloader/Type/PECL.php
@@ -0,0 +1,79 @@
+VERSIONSTATE per release */
+ private const string PECL_REST_URL = 'https://pecl.php.net/rest/r/%s/allreleases.xml';
+
+ public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult
+ {
+ [, $version] = $this->fetchPECLInfo($name, $config, $downloader);
+ return new CheckUpdateResult(
+ old: $old_version,
+ new: $version,
+ needUpdate: $old_version === null || version_compare($version, $old_version, '>'),
+ );
+ }
+
+ public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult
+ {
+ [$filename, $version] = $this->fetchPECLInfo($name, $config, $downloader);
+ $url = self::PECL_BASE_URL . '/get/' . $filename;
+ $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename;
+ logger()->debug("Downloading {$name} from URL: {$url}");
+ default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry());
+ $extract = $config['extract'] ?? ('php-src/ext/' . $this->getExtractName($name));
+ return DownloadResult::archive($filename, $config, $extract, version: $version, downloader: static::class);
+ }
+
+ protected function fetchPECLInfo(string $name, array $config, ArtifactDownloader $downloader): array
+ {
+ $peclName = strtolower($config['name'] ?? $this->getExtractName($name));
+ $url = sprintf(self::PECL_REST_URL, $peclName);
+ logger()->debug("Fetching PECL release list for {$name} from REST API");
+ $xml = default_shell()->executeCurl($url, retries: $downloader->getRetry());
+ if ($xml === false) {
+ throw new DownloaderException("Failed to fetch PECL release list for {$name}");
+ }
+ // Match VERSIONSTATE
+ preg_match_all('/(?P[^<]+)<\/v>(?P[^<]+)<\/s><\/r>/', $xml, $matches);
+ if (empty($matches['version'])) {
+ throw new DownloaderException("Failed to parse PECL release list for {$name}");
+ }
+ $versions = [];
+ logger()->debug('Matched ' . count($matches['version']) . " releases for {$name} from PECL");
+ foreach ($matches['version'] as $i => $version) {
+ if ($matches['state'][$i] !== 'stable') {
+ continue;
+ }
+ $versions[$version] = $peclName . '-' . $version . '.tgz';
+ }
+ if (empty($versions)) {
+ throw new DownloaderException("No stable releases found for {$name} on PECL");
+ }
+ uksort($versions, 'version_compare');
+ $filename = end($versions);
+ $version = array_key_last($versions);
+ return [$filename, $version, $versions];
+ }
+
+ /**
+ * Derive the lowercase PECL package / extract name from the artifact name.
+ * e.g. "ext-apcu" -> "apcu", "ext-ast" -> "ast"
+ */
+ private function getExtractName(string $name): string
+ {
+ return strtolower(preg_replace('/^ext-/i', '', $name));
+ }
+}
diff --git a/src/StaticPHP/Config/ConfigValidator.php b/src/StaticPHP/Config/ConfigValidator.php
index fbf88321..83f9ca41 100644
--- a/src/StaticPHP/Config/ConfigValidator.php
+++ b/src/StaticPHP/Config/ConfigValidator.php
@@ -89,6 +89,7 @@ class ConfigValidator
'bitbuckettag' => [['repo'], ['extract']],
'local' => [['dirname'], ['extract']],
'pie' => [['repo'], ['extract']],
+ 'pecl' => [['name'], ['extract']],
'php-release' => [[], ['extract']],
'custom' => [[], ['func']],
];
diff --git a/src/StaticPHP/Runtime/Shell/DefaultShell.php b/src/StaticPHP/Runtime/Shell/DefaultShell.php
index 5b50d152..66dfb7ab 100644
--- a/src/StaticPHP/Runtime/Shell/DefaultShell.php
+++ b/src/StaticPHP/Runtime/Shell/DefaultShell.php
@@ -25,7 +25,7 @@ class DefaultShell extends Shell
/**
* Execute a cURL command to fetch data from a URL.
*/
- public function executeCurl(string $url, string $method = 'GET', array $headers = [], array $hooks = [], int $retries = 0): false|string
+ public function executeCurl(string $url, string $method = 'GET', array $headers = [], array $hooks = [], int $retries = 0, bool $compressed = false): false|string
{
foreach ($hooks as $hook) {
$hook($method, $url, $headers);
@@ -39,7 +39,8 @@ class DefaultShell extends Shell
};
$header_arg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $headers));
$retry_arg = $retries > 0 ? "--retry {$retries}" : '';
- $cmd = SPC_CURL_EXEC . " -sfSL {$retry_arg} {$method_arg} {$header_arg} {$url_arg}";
+ $compressed_arg = $compressed ? '--compressed' : '';
+ $cmd = SPC_CURL_EXEC . " -sfSL --max-time 3600 {$retry_arg} {$compressed_arg} {$method_arg} {$header_arg} {$url_arg}";
$this->logCommandInfo($cmd);
$result = $this->passthru($cmd, capture_output: true, throw_on_error: false);
@@ -72,7 +73,7 @@ class DefaultShell extends Shell
$header_arg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $headers));
$retry_arg = $retries > 0 ? "--retry {$retries}" : '';
$check = $this->console_putput ? '#' : 's';
- $cmd = clean_spaces(SPC_CURL_EXEC . " -{$check}fSL {$retry_arg} {$header_arg} -o {$path_arg} {$url_arg}");
+ $cmd = clean_spaces(SPC_CURL_EXEC . " -{$check}fSL --max-time 3600 {$retry_arg} {$header_arg} -o {$path_arg} {$url_arg}");
$this->logCommandInfo($cmd);
logger()->debug('[CURL DOWNLOAD] ' . $cmd);
$this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true);
@@ -93,7 +94,7 @@ class DefaultShell extends Shell
$path_arg = escapeshellarg($path);
$shallow_arg = $shallow ? '--depth 1 --single-branch' : '';
$submodules_arg = ($submodules === null && $shallow) ? '--recursive --shallow-submodules' : ($submodules === null ? '--recursive' : '');
- $cmd = clean_spaces("{$git} clone --config core.autocrlf=false --branch {$branch_arg} {$shallow_arg} {$submodules_arg} {$url_arg} {$path_arg}");
+ $cmd = clean_spaces("{$git} clone -c http.lowSpeedLimit=1 -c http.lowSpeedTime=3600 --config core.autocrlf=false --branch {$branch_arg} {$shallow_arg} {$submodules_arg} {$url_arg} {$path_arg}");
$this->logCommandInfo($cmd);
logger()->debug("[GIT CLONE] {$cmd}");
$this->passthru($cmd, $this->console_putput);