diff --git a/README-zh.md b/README-zh.md index 156077cf6..e46ff07ad 100755 --- a/README-zh.md +++ b/README-zh.md @@ -3,16 +3,16 @@ [![Chinese readme](https://img.shields.io/badge/README-%E4%B8%AD%E6%96%87%20%F0%9F%87%A8%F0%9F%87%B3-moccasin?style=flat-square)](README-zh.md) [![English readme](https://img.shields.io/badge/README-English%20%F0%9F%87%AC%F0%9F%87%A7-moccasin?style=flat-square)](README.md) [![Releases](https://img.shields.io/packagist/v/crazywhalecc/static-php-cli?include_prereleases&label=Release&style=flat-square)](https://github.com/crazywhalecc/static-php-cli/releases) -[![CI](https://img.shields.io/github/actions/workflow/status/crazywhalecc/static-php-cli/tests.yml?branch=main&label=Build%20Test&style=flat-square)](https://github.com/crazywhalecc/static-php-cli/actions/workflows/tests.yml) -[![License](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](https://github.com/crazywhalecc/static-php-cli/blob/main/LICENSE) +[![CI](https://img.shields.io/github/actions/workflow/status/crazywhalecc/static-php-cli/tests.yml?branch=v3&label=Build%20Test&style=flat-square)](https://github.com/crazywhalecc/static-php-cli/actions/workflows/tests.yml) +[![License](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](https://github.com/crazywhalecc/static-php-cli/blob/v3/LICENSE) [![Discord](https://img.shields.io/discord/nrSRbpMJ?label=Discord&logo=discord&style=flat-square)](https://discord.gg/xf6Rd4pEAk) **StaticPHP** 是一个强大的工具,用于构建可移植的可执行文件,包括 PHP、扩展等。 > [!IMPORTANT] -> 我们正准备发布 **v3** 版本,届时项目名称将从 **static-php-cli** 更改为 **StaticPHP**。 -> 此分支对应 v3 版本。v2 版本请查看 [v2 分支](https://github.com/crazywhalecc/static-php-cli/tree/main)。 -> 请更新您的参考资料,并关注正式版发布。 +> **v3** 是当前默认分支。项目名称现为 **StaticPHP**,仓库名保持 `static-php-cli` 不变。 +> v2 版本请查看 [v2 分支](https://github.com/crazywhalecc/static-php-cli/tree/main)。 +> 敬请关注正式版发布。 ## 特性 @@ -40,15 +40,15 @@ ### 1. 下载 spc 二进制 ```bash -# For Linux x86_64 +# Linux x86_64 curl -fsSL -o spc https://dl.static-php.dev/v3/spc-bin/nightly/spc-linux-x86_64 -# For Linux aarch64 +# Linux aarch64 curl -fsSL -o spc https://dl.static-php.dev/v3/spc-bin/nightly/spc-linux-aarch64 -# macOS x86_64 (Intel) +# macOS x86_64(Intel) curl -fsSL -o spc https://dl.static-php.dev/v3/spc-bin/nightly/spc-macos-x86_64 -# macOS aarch64 (Apple) +# macOS aarch64(Apple) curl -fsSL -o spc https://dl.static-php.dev/v3/spc-bin/nightly/spc-macos-aarch64 -# Windows (x86_64, win10 build 17063 or later, please install VS2022 first) +# Windows(x86_64,需 Windows 10 build 17063 或更高版本,请先安装带 C++ 工具的 Visual Studio 2022) curl.exe -fsSL -o spc.exe https://dl.static-php.dev/v3/spc-bin/nightly/spc-windows-x64.exe ``` @@ -58,14 +58,17 @@ curl.exe -fsSL -o spc.exe https://dl.static-php.dev/v3/spc-bin/nightly/spc-windo chmod +x ./spc ``` +> [!TIP] +> 建议在首次构建前运行 `./spc doctor --auto-fix`,检查并安装所需的构建依赖(必要时 spc 也会自动提醒你)。 + ### 2. 构建静态 PHP 首先,创建 `craft.yml` 文件,并从 [扩展列表](https://static-php.dev/en/guide/extensions.html) 或 [命令生成器](https://static-php.dev/en/guide/cli-generator.html) 指定要包含的扩展: ```yml -# PHP version support: 8.1, 8.2, 8.3, 8.4, 8.5, 8.6 (pre-release) +# PHP 版本支持:8.1、8.2、8.3、8.4、8.5、8.6(预发布) php-version: 8.5 -# Put your extension list here +# 在此填写你的扩展列表 extensions: "apcu,bcmath,calendar,ctype,curl,dba,dom,exif,fileinfo,filter,gd,iconv,mbregex,mbstring,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_sqlite,phar,posix,readline,redis,session,simplexml,sockets,sodium,sqlite3,tokenizer,xml,xmlreader,xmlwriter,xsl,zip,zlib" sapi: - cli @@ -102,16 +105,26 @@ echo ' a.php 当前 README 包含基础用法。有关 StaticPHP 的完整功能集, 请访问 。 +如果你从 v2 迁移,请参阅 [迁移指南](https://static-php.dev/zh/guide/migrate-from-v2.html)。 + ## 直接下载 -如果你暂时不想构建,或只想先测试,可以从 [Actions](https://github.com/static-php/hosted/actions/workflows/build-php-bulk.yml) 下载示例预编译产物,或从自托管服务器下载。 +如果你暂时不想构建,或只想先测试,可以从 [Actions](https://github.com/static-php/hosted/actions/workflows/v3-php-bin-unix.yml)([Windows](https://github.com/static-php/hosted/actions/workflows/v3-php-bin-windows.yml))下载示例预编译产物,或从自托管服务器下载。 -我们为每个 PHP 版本提供 2 种扩展集合: +以下是几个具有不同扩展组合的预编译静态 PHP 二进制文件, +你可以根据需要直接下载。 -- **gigantic**:尽可能包含更多扩展,二进制大小约 100-150MB。 -- **base**:仅包含 StaticPHP 自身使用的少量扩展,二进制大小约 10MB。 +| 组合名称 | 扩展数量 | 系统 | 备注 | +|-----------------------------------------------------------|-------------------------------------------------------------|----------------|------------------------------| +| [common](https://dl.static-php.dev/v3/php-bin/common/) | [37](https://dl.static-php.dev/v3/php-bin/common/README.txt) | Linux, macOS | 二进制文件大小约为 13MB | +| [bulk](https://dl.static-php.dev/v3/php-bin/bulk/) | [56](https://dl.static-php.dev/v3/php-bin/bulk/README.txt) | Linux, macOS | 二进制文件大小约为 30MB | +| [gnu-bulk](https://dl.static-php.dev/v3/php-bin/gnu-bulk/) | [56](https://dl.static-php.dev/v3/php-bin/gnu-bulk/README.txt) | Linux | 使用共享 glibc | +| [minimal](https://dl.static-php.dev/v3/php-bin/minimal/) | [7](https://dl.static-php.dev/v3/php-bin/minimal/README.txt) | Linux, macOS | 二进制文件大小约为 3MB | +| [spc-min](https://dl.static-php.dev/v3/php-bin/spc-min/) | [7](https://dl.static-php.dev/v3/php-bin/spc-min/README.txt) | Windows | 二进制文件大小约为 3MB | +| [spc-max](https://dl.static-php.dev/v3/php-bin/spc-max/) | [50](https://dl.static-php.dev/v3/php-bin/spc-max/README.txt) | Windows | 二进制文件大小约为 9MB | -> WIP +> Linux 和 Windows 二进制文件均经过 UPX 压缩,可以将二进制文件大小减少 30% 到 50%。 +> macOS 不支持 UPX 压缩,因此 mac 的预构建二进制文件大小较大。 ### 在线构建(使用 GitHub Actions) @@ -120,8 +133,8 @@ echo ' a.php 并同时自定义要编译的扩展列表。 1. Fork 此仓库。 -2. 进入项目的 Actions 并选择 `CI`。 -3. 选择 `Run workflow`,填写你要编译的 PHP 版本、目标类型和扩展列表。(扩展用逗号分隔,例如 `bcmath,curl,mbstring`) +2. 进入项目的 Actions 并选择 `CI on Unix`(Windows 构建请选择 `CI on x86_64 Windows`)。 +3. 选择 `Run workflow`,填写目标系统(`os`)、PHP 版本(`php-version`)和扩展列表(`extensions`,扩展用逗号分隔,例如 `bcmath,curl,mbstring`)。 4. 等待工作流执行完成后,进入对应运行记录并下载 `Artifacts`。 如果你启用 `debug`,构建时将输出所有日志,包括编译日志,便于排查问题。 @@ -146,6 +159,10 @@ echo ' a.php NativePHP Logo +JetBrains Logo + +**[JetBrains](https://github.com/JetBrains)** 为核心贡献者提供 开源开发许可证 + ## 开源许可证 本项目本身采用 MIT 许可证。 diff --git a/README.md b/README.md index 566954a16..1a4c6560a 100755 --- a/README.md +++ b/README.md @@ -3,16 +3,16 @@ [![Chinese readme](https://img.shields.io/badge/README-%E4%B8%AD%E6%96%87%20%F0%9F%87%A8%F0%9F%87%B3-moccasin?style=flat-square)](README-zh.md) [![English readme](https://img.shields.io/badge/README-English%20%F0%9F%87%AC%F0%9F%87%A7-moccasin?style=flat-square)](README.md) [![Releases](https://img.shields.io/packagist/v/crazywhalecc/static-php-cli?include_prereleases&label=Release&style=flat-square)](https://github.com/crazywhalecc/static-php-cli/releases) -[![CI](https://img.shields.io/github/actions/workflow/status/crazywhalecc/static-php-cli/tests.yml?branch=main&label=Build%20Test&style=flat-square)](https://github.com/crazywhalecc/static-php-cli/actions/workflows/tests.yml) -[![License](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](https://github.com/crazywhalecc/static-php-cli/blob/main/LICENSE) +[![CI](https://img.shields.io/github/actions/workflow/status/crazywhalecc/static-php-cli/tests.yml?branch=v3&label=Build%20Test&style=flat-square)](https://github.com/crazywhalecc/static-php-cli/actions/workflows/tests.yml) +[![License](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](https://github.com/crazywhalecc/static-php-cli/blob/v3/LICENSE) [![Discord](https://img.shields.io/discord/nrSRbpMJ?label=Discord&logo=discord&style=flat-square)](https://discord.gg/xf6Rd4pEAk) **StaticPHP** is a powerful tool designed for building portable executables including PHP, extensions, and more. > [!IMPORTANT] -> We are preparing to release **v3**, which will include a project rename from **static-php-cli** to **StaticPHP**. -> And this branch is for v3. For v2, please check the [v2 branch](https://github.com/crazywhalecc/static-php-cli/tree/main). -> Please update your references and stay tuned for the official release. +> **v3** is the current default branch. The project is now called **StaticPHP**, while the repository keeps its `static-php-cli` name. +> For v2, please check the [v2 branch](https://github.com/crazywhalecc/static-php-cli/tree/main). +> Stay tuned for the official release. ## Features @@ -48,7 +48,7 @@ curl -fsSL -o spc https://dl.static-php.dev/v3/spc-bin/nightly/spc-linux-aarch64 curl -fsSL -o spc https://dl.static-php.dev/v3/spc-bin/nightly/spc-macos-x86_64 # macOS aarch64 (Apple) curl -fsSL -o spc https://dl.static-php.dev/v3/spc-bin/nightly/spc-macos-aarch64 -# Windows (x86_64, win10 build 17063 or later, please install VS2022 first) +# Windows (x86_64, Windows 10 build 17063 or later, install Visual Studio 2022 with C++ tools first) curl.exe -fsSL -o spc.exe https://dl.static-php.dev/v3/spc-bin/nightly/spc-windows-x64.exe ``` @@ -58,6 +58,9 @@ For macOS and Linux, add execute permission first: chmod +x ./spc ``` +> [!TIP] +> Before the first build, it is recommended to run `./spc doctor --auto-fix` to check and install the required build dependencies (spc will also remind you automatically when needed). + ### 2. Build Static PHP First, create a `craft.yml` file and specify which extensions you want to include from [extension list](https://static-php.dev/en/guide/extensions.html) or [command generator](https://static-php.dev/en/guide/cli-generator.html): @@ -102,16 +105,26 @@ echo ' a.php The current README contains basic usage. For the complete feature set of StaticPHP, see . +If you are migrating from v2, check out the [migration guide](https://static-php.dev/en/guide/migrate-from-v2.html). + ## Direct Download -If you do not want to build yet or just want to test first, you can download example pre-compiled artifacts from [Actions](https://github.com/static-php/hosted/actions/workflows/build-php-bulk.yml) or from a self-hosted server. +If you do not want to build yet or just want to test first, you can download example pre-compiled artifacts from [Actions](https://github.com/static-php/hosted/actions/workflows/v3-php-bin-unix.yml) ([Windows](https://github.com/static-php/hosted/actions/workflows/v3-php-bin-windows.yml)) or from a self-hosted server. -We offer 2 types of extension sets for each PHP version: +Below are several precompiled static-php binaries with different extension combinations, +which can be downloaded directly according to your needs. -- **gigantic**: Includes as many extensions as possible, the binary size is about 100-150MB. -- **base**: Only includes a few extensions used by StaticPHP itself, the binary size is about 10MB. +| Combination | Extension Count | OS | Comment | +|------------------------------------------------------------|------------------------------------------------------------------|--------------|---------------------------------| +| [common](https://dl.static-php.dev/v3/php-bin/common/) | [37](https://dl.static-php.dev/v3/php-bin/common/README.txt) | Linux, macOS | The binary size is about 13MB | +| [bulk](https://dl.static-php.dev/v3/php-bin/bulk/) | [56](https://dl.static-php.dev/v3/php-bin/bulk/README.txt) | Linux, macOS | The binary size is about 30MB | +| [gnu-bulk](https://dl.static-php.dev/v3/php-bin/gnu-bulk/) | [56](https://dl.static-php.dev/v3/php-bin/gnu-bulk/README.txt) | Linux | Using shared glibc | +| [minimal](https://dl.static-php.dev/v3/php-bin/minimal/) | [7](https://dl.static-php.dev/v3/php-bin/minimal/README.txt) | Linux, macOS | The binary size is about 3MB | +| [spc-min](https://dl.static-php.dev/v3/php-bin/spc-min/) | [7](https://dl.static-php.dev/v3/php-bin/spc-min/README.txt) | Windows | The binary size is about 3MB | +| [spc-max](https://dl.static-php.dev/v3/php-bin/spc-max/) | [50](https://dl.static-php.dev/v3/php-bin/spc-max/README.txt) | Windows | The binary size is about 9MB | -> WIP +> Linux and Windows binaries are compressed with UPX, which can reduce the size of the binary by 30% to 50%. +> macOS does not support UPX compression, so the size of the pre-built binaries for macOS is larger. ### Build Online (using GitHub Actions) @@ -120,8 +133,8 @@ you can use GitHub Actions to easily build a statically compiled PHP while defining your own extension list. 1. Fork this repository. -2. Go to the Actions of the project and select `CI`. -3. Select `Run workflow`, fill in the PHP version you want to compile, the target type, and the list of extensions. (extensions comma separated, e.g. `bcmath,curl,mbstring`) +2. Go to the Actions of the project and select `CI on Unix` (or `CI on x86_64 Windows` for Windows builds). +3. Select `Run workflow`, fill in the target OS (`os`), the PHP version (`php-version`), and the list of extensions (`extensions`, comma separated, e.g. `bcmath,curl,mbstring`). 4. After waiting for the workflow to finish, open the corresponding run and download `Artifacts`. If you enable `debug`, all logs will be output at build time, including compiled logs, for troubleshooting. diff --git a/docs/.vitepress/components/Contributors.vue b/docs/.vitepress/components/Contributors.vue index c1f474b0c..9736f9fbc 100644 --- a/docs/.vitepress/components/Contributors.vue +++ b/docs/.vitepress/components/Contributors.vue @@ -59,7 +59,7 @@ const fetchContributors = async () => { error.value = ''; const response = await fetch( - 'https://api.github.com/repos/crazywhalecc/static-php-cli/contributors?per_page=24' + 'https://api.github.com/repos/crazywhalecc/static-php-cli/contributors?per_page=50' ); if (!response.ok) { diff --git a/src/StaticPHP/Artifact/Artifact.php b/src/StaticPHP/Artifact/Artifact.php index d37d350b8..ac0c33115 100644 --- a/src/StaticPHP/Artifact/Artifact.php +++ b/src/StaticPHP/Artifact/Artifact.php @@ -27,12 +27,18 @@ class Artifact /** @var null|callable Bind custom source fetcher callback */ protected mixed $custom_source_callback = null; + /** @var null|string Display label describing where the custom source callback came from */ + protected ?string $custom_source_callback_origin = null; + /** @var null|callable Bind custom source check-update callback */ protected mixed $custom_source_check_update_callback = null; /** @var array Bind custom binary fetcher callbacks */ protected mixed $custom_binary_callbacks = []; + /** @var array Display label per platform describing where the custom binary callback came from */ + protected array $custom_binary_callback_origins = []; + /** @var array Bind custom binary check-update callbacks */ protected array $custom_binary_check_update_callbacks = []; @@ -285,15 +291,19 @@ public function getDownloadConfig(string $type): mixed * Get source extraction directory. * * Rules: - * 1. If extract is not specified: SOURCE_PATH/{artifact_name} - * 2. If extract is relative path: SOURCE_PATH/{value} - * 3. If extract is absolute path: {value} - * 4. If extract is array (dict): handled by extractor (selective extraction) + * 1. If cache_type is 'local': use the absolute dirname recorded at download time (no symlink/copy). + * 2. If extract is not specified: SOURCE_PATH/{artifact_name} + * 3. If extract is relative path: SOURCE_PATH/{value} + * 4. If extract is absolute path: {value} + * 5. If extract is array (dict): handled by extractor (selective extraction) */ public function getSourceDir(): string { // Prefer cache extract path, fall back to config $cache_info = ApplicationContext::get(ArtifactCache::class)->getSourceInfo($this->name); + if (($cache_info['cache_type'] ?? null) === 'local' && isset($cache_info['dirname'])) { + return FileSystem::convertPath($cache_info['dirname']); + } $extract = is_string($cache_info['extract'] ?? null) ? $cache_info['extract'] : ($this->config['source']['extract'] ?? null); @@ -406,10 +416,13 @@ public function getBinaryDir(): ?string /** * Set custom source fetcher callback. + * + * @param string $origin Short label shown in progress output (e.g. 'package downloader', 'custom url') */ - public function setCustomSourceCallback(callable $callback): void + public function setCustomSourceCallback(callable $callback, string $origin = 'package downloader'): void { $this->custom_source_callback = $callback; + $this->custom_source_callback_origin = $origin; } public function getCustomSourceCallback(): ?callable @@ -417,6 +430,11 @@ public function getCustomSourceCallback(): ?callable return $this->custom_source_callback ?? null; } + public function getCustomSourceCallbackOrigin(): ?string + { + return $this->custom_source_callback_origin; + } + /** * Set custom source check-update callback. */ @@ -451,11 +469,19 @@ public function emitCustomBinary(): void * * @param string $target_os Target OS platform string (e.g. linux-x86_64) * @param callable $callback Custom binary fetcher callback + * @param string $origin Short label shown in progress output (e.g. 'package downloader') */ - public function setCustomBinaryCallback(string $target_os, callable $callback): void + public function setCustomBinaryCallback(string $target_os, callable $callback, string $origin = 'package downloader'): void { ConfigValidator::validatePlatformString($target_os); $this->custom_binary_callbacks[$target_os] = $callback; + $this->custom_binary_callback_origins[$target_os] = $origin; + } + + public function getCustomBinaryCallbackOrigin(): ?string + { + $current_platform = SystemTarget::getCurrentPlatformString(); + return $this->custom_binary_callback_origins[$current_platform] ?? null; } /** diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index bfe7a21f5..db5df2080 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -7,6 +7,7 @@ use Psr\Log\LogLevel; use StaticPHP\Artifact\Downloader\DownloadResult; use StaticPHP\Artifact\Downloader\Type\BitBucketTag; +use StaticPHP\Artifact\Downloader\Type\CacheMatchInterface; use StaticPHP\Artifact\Downloader\Type\CheckUpdateInterface; use StaticPHP\Artifact\Downloader\Type\CheckUpdateResult; use StaticPHP\Artifact\Downloader\Type\DownloadTypeInterface; @@ -89,6 +90,9 @@ class ArtifactDownloader private array $_before_files; + /** @var array Memoized generateQueue() results, valid for one download() run (queues only depend on each artifact's own cache files) */ + private array $queue_memo = []; + /** * @param array{ * parallel?: int, @@ -318,7 +322,12 @@ public function download(): void if (!is_dir(DOWNLOAD_PATH)) { FileSystem::createDir(DOWNLOAD_PATH); } - logger()->info('Downloading' . implode(', ', array_map(fn ($x) => " '{$x->getName()}'", $this->artifacts)) . " with concurrency {$this->parallel} ..."); + // fresh memo for this run: queues reflect pre-download cache state + $this->queue_memo = []; + $pending = array_values(array_filter($this->artifacts, fn ($a) => $this->generateQueue($a) !== [])); + if ($pending !== []) { + logger()->info('Downloading' . implode(', ', array_map(fn ($x) => " '{$x->getName()}'", $pending)) . " with concurrency {$this->parallel} ..."); + } // Download artifacts parallelly if ($this->parallel > 1) { $this->downloadWithConcurrency(); @@ -573,8 +582,8 @@ private function downloadWithType(Artifact $artifact, int $current, int $total, $instance = null; $call = $this->downloaders[$item['config']['type']] ?? null; $type_display_name = match (true) { - $item['lock'] === 'source' && ($callback = $artifact->getCustomSourceCallback()) !== null => 'user defined source downloader', - $item['lock'] === 'binary' && ($callback = $artifact->getCustomBinaryCallback()) !== null => 'user defined binary downloader', + $item['lock'] === 'source' && $artifact->getCustomSourceCallback() !== null => $artifact->getCustomSourceCallbackOrigin() ?? 'source package downloader', + $item['lock'] === 'binary' && $artifact->getCustomBinaryCallback() !== null => $artifact->getCustomBinaryCallbackOrigin() ?? 'binary package downloader', default => SPC_DOWNLOAD_TYPE_DISPLAY_NAME[$item['config']['type']] ?? $item['config']['type'], }; $try_h = $try ? 'Try downloading' : 'Downloading'; @@ -748,11 +757,29 @@ private function downloadWithConcurrency(): void */ private function generateQueue(Artifact $artifact): array { + $memo_key = $artifact->getName(); + if (isset($this->queue_memo[$memo_key])) { + return $this->queue_memo[$memo_key]; + } /** @var array $queue */ $queue = []; $binary_downloaded = $artifact->isBinaryDownloaded(compare_hash: true); $source_downloaded = $artifact->isSourceDownloaded(compare_hash: true); + // Some download types fetch content depending on request options rather than config alone + // (e.g. php-release varies with --with-php): let them veto a stale cache entry. + // Custom source callbacks carry their own semantics, they bypass type-based checks. + if ($source_downloaded && $artifact->getCustomSourceCallback() === null) { + $source_config = $artifact->getDownloadConfig('source'); + $dl_cls = is_array($source_config) ? ($this->downloaders[$source_config['type']] ?? null) : null; + if ($dl_cls !== null && is_a($dl_cls, CacheMatchInterface::class, true)) { + $source_lock = ApplicationContext::get(ArtifactCache::class)->getSourceInfo($artifact->getName()) ?? []; + if (!(new $dl_cls())->cacheMatches($artifact->getName(), $source_config, $source_lock, $this)) { + $source_downloaded = false; + } + } + } + $item_source = ['display' => 'source', 'lock' => 'source', 'config' => $artifact->getDownloadConfig('source')]; $item_source_mirror = ['display' => 'source (mirror)', 'lock' => 'source', 'config' => $artifact->getDownloadConfig('source-mirror')]; @@ -802,7 +829,7 @@ private function generateQueue(Artifact $artifact): array if (empty($queue)) { throw new ValidationException("Artifact '{$artifact->getName()}' does not provide any download source for current platform (" . SystemTarget::getCurrentPlatformString() . ').'); } - return $queue; + return $this->queue_memo[$memo_key] = $queue; } // check if already downloaded @@ -823,7 +850,7 @@ private function generateQueue(Artifact $artifact): array // if already downloaded, skip if ($has_usable_download) { - return []; + return $this->queue_memo[$memo_key] = []; } // validate: ensure at least one download source is available @@ -838,7 +865,7 @@ private function generateQueue(Artifact $artifact): array throw new ValidationException("Validation failed: Artifact '{$artifact->getName()}' does not provide any download source for current platform (" . SystemTarget::getCurrentPlatformString() . ').'); } - return $queue; + return $this->queue_memo[$memo_key] = $queue; } private function applyCustomDownloads(): void @@ -847,21 +874,21 @@ private function applyCustomDownloads(): void if (isset($this->artifacts[$artifact_name])) { $this->artifacts[$artifact_name]->setCustomSourceCallback(function (ArtifactDownloader $downloader) use ($artifact_name, $custom_url) { return (new Url())->download($artifact_name, ['url' => $custom_url], $downloader); - }); + }, 'custom url'); } } foreach ($this->custom_gits as $artifact_name => [$branch, $git_url]) { if (isset($this->artifacts[$artifact_name])) { $this->artifacts[$artifact_name]->setCustomSourceCallback(function (ArtifactDownloader $downloader) use ($artifact_name, $branch, $git_url) { return (new Git())->download($artifact_name, ['rev' => $branch, 'url' => $git_url], $downloader); - }); + }, 'custom git'); } } foreach ($this->custom_locals as $artifact_name => $local_path) { if (isset($this->artifacts[$artifact_name])) { $this->artifacts[$artifact_name]->setCustomSourceCallback(function (ArtifactDownloader $downloader) use ($artifact_name, $local_path) { return (new LocalDir())->download($artifact_name, ['dirname' => $local_path], $downloader); - }); + }, 'custom local dir'); } } } diff --git a/src/StaticPHP/Artifact/ArtifactExtractor.php b/src/StaticPHP/Artifact/ArtifactExtractor.php index 086e02455..7edbf75ee 100644 --- a/src/StaticPHP/Artifact/ArtifactExtractor.php +++ b/src/StaticPHP/Artifact/ArtifactExtractor.php @@ -136,6 +136,12 @@ protected function extractSource(Artifact $artifact): int throw new WrongUsageException("Artifact source [{$name}] not downloaded, please download it first!"); } + // Local (--custom-local): source lives in place at $cache_info['dirname']. + if (($cache_info['cache_type'] ?? null) === 'local') { + $artifact->emitAfterSourceExtract($artifact->getSourceDir()); + return SPC_STATUS_ALREADY_EXTRACTED; + } + $source_file = $this->cache->getCacheFullPath($cache_info); $target_path = $artifact->getSourceDir(); diff --git a/src/StaticPHP/Artifact/Downloader/Type/CacheMatchInterface.php b/src/StaticPHP/Artifact/Downloader/Type/CacheMatchInterface.php new file mode 100644 index 000000000..fc75d1c26 --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/CacheMatchInterface.php @@ -0,0 +1,31 @@ +executeCurl($latest_url, headers: $headers, retries: $retries) ?: '', true); + if (is_array($latest) && isset($latest['assets'])) { + array_unshift($data, $latest); + } + } foreach ($data as $release) { if ($prefer_stable && $release['prerelease'] === true) { continue; diff --git a/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php b/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php index f20866f5e..fa03cbe32 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php +++ b/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php @@ -8,7 +8,7 @@ use StaticPHP\Artifact\Downloader\DownloadResult; use StaticPHP\Exception\DownloaderException; -class PhpRelease implements DownloadTypeInterface, ValidatorInterface, CheckUpdateInterface +class PhpRelease implements DownloadTypeInterface, ValidatorInterface, CheckUpdateInterface, CacheMatchInterface { public const string DEFAULT_PHP_DOMAIN = 'https://www.php.net'; @@ -83,6 +83,25 @@ public function checkUpdate(string $name, array $config, ?string $old_version, A ); } + public function cacheMatches(string $name, array $config, array $lock_entry, ArtifactDownloader $downloader): bool + { + $requested = $downloader->getOption('with-php'); + // No explicit version request (option left at its null default): accept whatever + // is cached. The '8.5' default only applies when actually fetching, so a sticky + // cache is never invalidated by a version the user did not ask about. + if ($requested === null || $requested === '' || $requested === false) { + return true; + } + $cached_version = $lock_entry['version'] ?? null; + $cache_type = $lock_entry['cache_type'] ?? null; + if ($requested === 'git') { + return $cache_type === 'git'; + } + return $cached_version !== null + && $cache_type !== 'git' + && ($cached_version === $requested || str_starts_with($cached_version, $requested . '.')); + } + /** @return array{version: string, url: string, filename: string, sha256: string} */ protected function resolveRelease(array $info, array $config): array { diff --git a/src/StaticPHP/Artifact/Downloader/Type/Url.php b/src/StaticPHP/Artifact/Downloader/Type/Url.php index 02425fe5d..d082c4826 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/Url.php +++ b/src/StaticPHP/Artifact/Downloader/Type/Url.php @@ -8,7 +8,7 @@ use StaticPHP\Artifact\Downloader\DownloadResult; /** url */ -class Url implements DownloadTypeInterface +class Url implements DownloadTypeInterface, CacheMatchInterface { public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult { @@ -20,4 +20,11 @@ public function download(string $name, array $config, ArtifactDownloader $downlo default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); return DownloadResult::archive($filename, config: $config, extract: $config['extract'] ?? null, version: $version, downloader: static::class); } + + public function cacheMatches(string $name, array $config, array $lock_entry, ArtifactDownloader $downloader): bool + { + // A changed filename already invalidates via the file-exists check; a changed url + // with an unchanged filename (mirror switch, fixed-name tarball) does not. + return ($lock_entry['config']['url'] ?? null) === ($config['url'] ?? null); + } } diff --git a/src/StaticPHP/Artifact/DownloaderOptions.php b/src/StaticPHP/Artifact/DownloaderOptions.php index 47c960ef5..5b35bfef9 100644 --- a/src/StaticPHP/Artifact/DownloaderOptions.php +++ b/src/StaticPHP/Artifact/DownloaderOptions.php @@ -51,8 +51,9 @@ public static function getConsoleOptions(string $prefix = ''): array $shortI = $prefix ? null : 'i'; return [ - // php version option - new InputOption("{$p}with-php", null, InputOption::VALUE_REQUIRED, 'PHP version in major.minor format (default 8.5)', '8.5'), + // php version option (null default: only enforced against the download cache when + // explicitly given; PhpRelease falls back to the latest 8.5.x when actually fetching) + new InputOption("{$p}with-php", null, InputOption::VALUE_REQUIRED, 'PHP version in major.minor format (default 8.5)'), // download preference options new InputOption("{$p}prefer-source", null, InputOption::VALUE_OPTIONAL, 'Prefer source downloads when both source and binary are available', false), diff --git a/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php b/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php index 6d5e63437..25fef98d1 100644 --- a/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php +++ b/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php @@ -229,8 +229,15 @@ private function getDefaultCMakeArgs(): array "-DCMAKE_TOOLCHAIN_FILE={$this->makeCmakeToolchainFile()}", ]; - // EXE linker flags: base system libs + framework flags for target packages - $exeLinkerFlags = SystemTarget::getRuntimeLibs(); + // EXE linker flags: framework flags for target packages (Darwin). On Linux the + // runtime libs (-ldl & co.) are NOT passed here: CMake puts EXE_LINKER_FLAGS before + // the objects/archives, where -Wl,--as-needed (Debian gcc default, and our own + // LDFLAGS) discards them and breaks try_compile probes linking static archives that + // need them (curl's SSL_set_quic_tls_cbs check vs libcrypto.a needing dlopen on + // glibc < 2.34). They go into the toolchain file as CMAKE__STANDARD_LIBRARIES + // instead, which cmake appends AFTER the objects — correct order, as-needed intact, + // and the toolchain file is re-evaluated inside every try_compile. + $exeLinkerFlags = SystemTarget::getTargetOS() === 'Linux' ? '' : SystemTarget::getRuntimeLibs(); if ($this->package instanceof TargetPackage && SystemTarget::getTargetOS() === 'Darwin') { $resolvedNames = array_keys($this->installer->getResolvedPackages()); $resolvedNames[] = $this->package->getName(); @@ -309,6 +316,14 @@ private function makeCmakeToolchainFile(): string $ranlib = getenv('SPC_DEFAULT_RANLIB') ?: (getenv('RANLIB') ?: 'ranlib'); $toolchain .= "\nSET(CMAKE_AR \"{$ar}\")"; $toolchain .= "\nSET(CMAKE_RANLIB \"{$ranlib}\")"; + // Runtime libs as standard libraries: appended after objects/archives on every + // link line (incl. try_compile probes), so -Wl,--as-needed keeps the ones that + // are actually referenced. + $runtimeLibs = SystemTarget::getRuntimeLibs(); + if ($runtimeLibs !== '') { + $toolchain .= "\nset(CMAKE_C_STANDARD_LIBRARIES_INIT \"{$runtimeLibs}\")"; + $toolchain .= "\nset(CMAKE_CXX_STANDARD_LIBRARIES_INIT \"{$runtimeLibs}\")"; + } } FileSystem::writeFile(SOURCE_PATH . '/toolchain.cmake', $toolchain); return $created = realpath(SOURCE_PATH . '/toolchain.cmake'); diff --git a/src/StaticPHP/Util/PkgConfigUtil.php b/src/StaticPHP/Util/PkgConfigUtil.php index d5b03757f..7de8230fd 100644 --- a/src/StaticPHP/Util/PkgConfigUtil.php +++ b/src/StaticPHP/Util/PkgConfigUtil.php @@ -57,17 +57,22 @@ public static function getModuleVersion(string $pkg_config_str): string /** * Get CFLAGS from pkg-config * - * Returns --cflags-only-other output from pkg-config. + * Returns full --cflags output (not --cflags-only-other) because: + * 1. Libraries like ImageMagick and libxml2 install headers into include + * subdirectories whose -I paths are dropped by --cflags-only-other. + * 2. Extensions that can only be linked dynamically (glfw, webview, ...) + * intentionally resolve system .pc files via a user-set PKG_CONFIG_PATH; + * their -I paths are required. pkg-config itself already filters -I/usr/include, + * so only genuinely needed subdirectory includes appear here. * The reason we return the string is we cannot use array_unique() on cflags, * some cflags may contains spaces. * * @param string $pkg_config_str .pc file string, accepts multiple files - * @return string CFLAGS string, e.g. "-Wno-implicit-int-float-conversion ..." + * @return string CFLAGS string, e.g. "-I/path/to/include -Wno-implicit-int-float-conversion ..." */ public static function getCflags(string $pkg_config_str): string { - // get other things - $result = self::execWithResult("pkg-config --static --cflags-only-other {$pkg_config_str}"); + $result = self::execWithResult("pkg-config --static --cflags {$pkg_config_str}"); return trim($result); }