diff --git a/.github/workflows/build-unix.yml b/.github/workflows/build-unix.yml index 16ad76e0..0166bfa0 100644 --- a/.github/workflows/build-unix.yml +++ b/.github/workflows/build-unix.yml @@ -136,12 +136,12 @@ jobs: macos-x86_64) DOWN_CMD="composer update --no-dev --classmap-authoritative && ./bin/spc doctor --auto-fix && ./bin/spc download" BUILD_CMD="./bin/spc build" - RUNS_ON="macos-13" + RUNS_ON="macos-15-intel" ;; macos-aarch64) DOWN_CMD="composer update --no-dev --classmap-authoritative && ./bin/spc doctor --auto-fix && ./bin/spc download" BUILD_CMD="./bin/spc build" - RUNS_ON="macos-14" + RUNS_ON="macos-15" ;; esac DOWN_CMD="$DOWN_CMD --with-php=${{ inputs.php-version }} --for-extensions=${{ inputs.extensions }} --ignore-cache-sources=php-src" diff --git a/.github/workflows/ext-matrix-tests.yml b/.github/workflows/ext-matrix-tests.yml index 84d3b518..e93e9145 100644 --- a/.github/workflows/ext-matrix-tests.yml +++ b/.github/workflows/ext-matrix-tests.yml @@ -85,9 +85,9 @@ jobs: - "8.4" operating-system: - "ubuntu-latest" - #- "macos-13" + #- "macos-15-intel" #- "debian-arm64-self-hosted" - - "macos-14" + - "macos-15" steps: - name: "Checkout" @@ -99,11 +99,11 @@ jobs: OS="" if [ "${{ matrix.operating-system }}" = "ubuntu-latest" ]; then OS="linux-x86_64" - elif [ "${{ matrix.operating-system }}" = "macos-13" ]; then + elif [ "${{ matrix.operating-system }}" = "macos-15-intel" ]; then OS="macos-x86_64" elif [ "${{ matrix.operating-system }}" = "debian-arm64-self-hosted" ]; then OS="linux-aarch64" - elif [ "${{ matrix.operating-system }}" = "macos-14" ]; then + elif [ "${{ matrix.operating-system }}" = "macos-15" ]; then OS="macos-aarch64" fi echo "OS=$OS" >> $GITHUB_ENV diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 61004bd3..aaeee1ff 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -27,7 +27,7 @@ jobs: os: "ubuntu-latest" filename: "spc-linux-x86_64.tar.gz" - name: "macos-x86_64" - os: "macos-13" + os: "macos-15-intel" filename: "spc-macos-x86_64.tar.gz" - name: "linux-aarch64" os: "ubuntu-latest" @@ -147,11 +147,11 @@ jobs: - name: "linux-x86_64" os: "ubuntu-latest" - name: "macos-x86_64" - os: "macos-13" + os: "macos-15-intel" - name: "linux-aarch64" os: "ubuntu-24.04-arm" - name: "macos-aarch64" - os: "macos-latest" + os: "macos-15" - name: "windows-x64" os: "windows-latest" steps: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8accd62f..fa9e646f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,13 +1,9 @@ name: Tests on: - push: - branches: - - main - paths: - - 'src/globals/test-extensions.php' pull_request: branches: [ "main" ] + types: [ opened, synchronize, reopened ] paths: - 'src/**' - 'config/**' diff --git a/.gitignore b/.gitignore index 4bf41b55..0d2bd554 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,9 @@ docker/source/ # default package root directory /pkgroot/** +# Windows PHP SDK binary tools +/php-sdk-binary-tools/** + # default pack:lib and release directory /dist/** packlib_files.txt diff --git a/README-zh.md b/README-zh.md index b159a5c3..d8d1b396 100755 --- a/README-zh.md +++ b/README-zh.md @@ -5,310 +5,168 @@ [![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) -[![Extensions](https://img.shields.io/badge/Extension%20Counter-75+-yellow.svg?style=flat-square)](https://static-php.dev/zh/guide/extensions.html) -**static-php-cli**是一个用于静态编译、构建 PHP 解释器的工具,支持众多流行扩展。 - -目前 static-php-cli 支持 `cli`、`fpm`、`embed`、`micro` 和 `frankenphp` SAPI。 - -**static-php-cli**也支持将 PHP 代码和 PHP 运行时打包为一个文件并运行。 +**static-php-cli** 是一个用于构建静态、独立 PHP 运行时的强大工具,支持众多流行扩展。 ## 特性 -static-php-cli(简称 `spc`)有许多特性: +- :elephant: **支持多 PHP 版本** - 支持 PHP 8.1, 8.2, 8.3, 8.4, 8.5 +- :handbag: **单文件 PHP 可执行文件** - 构建零依赖的独立 PHP +- :hamburger: **phpmicro 集成** - 构建 **[phpmicro](https://github.com/dixyes/phpmicro)** 自解压可执行文件(将 PHP 二进制文件和源代码合并为一个文件) +- :pill: **智能环境检查器** - 自动构建环境检查器,具备自动修复功能 +- :zap: **跨平台支持** - 支持 Linux、macOS、FreeBSD 和 Windows +- :wrench: **可配置补丁** - 可自定义的源代码补丁系统 +- :books: **智能依赖管理** - 自动处理构建依赖 +- 📦 **自包含工具** - 提供使用 [box](https://github.com/box-project/box) 构建的 `spc` 可执行文件 +- :fire: **广泛的扩展支持** - 支持 75+ 流行 [扩展](https://static-php.dev/zh/guide/extensions.html) +- :floppy_disk: **UPX 压缩** - 减小二进制文件大小 30-50%(仅 Linux/Windows) -- :handbag: 构建独立的单文件 PHP 解释器,无需任何依赖 -- :hamburger: 构建 **[phpmicro](https://github.com/dixyes/phpmicro)** 自执行二进制(将 PHP 代码和 PHP 解释器打包为一个文件) -- :pill: 提供一键检查和修复编译环境的 Doctor 模块 -- :zap: 支持多个系统:`Linux`、`macOS`、`FreeBSD`、`Windows` -- :wrench: 高度自定义的代码 patch 功能 -- :books: 自带编译依赖管理 -- 📦 提供由自身编译的独立 `spc` 二进制(使用 spc 和 [box](https://github.com/box-project/box) 构建) -- :fire: 支持大量 [扩展](https://static-php.dev/zh/guide/extensions.html) -- :floppy_disk: 整合 UPX 工具(减小二进制文件体积) - -**静态 php-cli:** +**单文件独立 php-cli:** out1 -**使用 phpmicro 打包 PHP 代码:** +**使用 phpmicro 将 PHP 代码与 PHP 解释器结合:** out2 -## 文档 +## 快速开始 -目前 README 编写了基本用法。有关 static-php-cli 所有的功能,请点击这里查看文档:。 - -## 直接下载 - -如果你不想自行编译 PHP,可以从本项目现有的示例 Action 下载 Artifact,也可以从自托管的服务器下载。 - -| 组合名称 | 组合扩展数 | 系统 | 备注 | -|---------------------------------------------------------------------|----------------------------------------------------------------------------|-------------|--------------| -| [common](https://dl.static-php.dev/static-php-cli/common/) | [30+](https://dl.static-php.dev/static-php-cli/common/README.txt) | Linux/macOS | 体积为 7.5MB 左右 | -| [bulk](https://dl.static-php.dev/static-php-cli/bulk/) | [50+](https://dl.static-php.dev/static-php-cli/bulk/README.txt) | Linux/macOS | 体积为 25MB 左右 | -| [minimal](https://dl.static-php.dev/static-php-cli/minimal/) | [5](https://dl.static-php.dev/static-php-cli/minimal/README.txt) | Linux/macOS | 体积为 3MB 左右 | -| [spc-min](https://dl.static-php.dev/static-php-cli/windows/spc-min) | [5](https://dl.static-php.dev/static-php-cli/windows/spc-min/README.txt) | Windows | 体积为 3MB 左右 | -| [spc-max](https://dl.static-php.dev/static-php-cli/windows/spc-max) | [40+](https://dl.static-php.dev/static-php-cli/windows/spc-max/README.txt) | Windows | 体积为 8.5MB 左右 | - -> Linux 和 Windows 默认启用了 UPX 压缩,可减小 30~50% 的 PHP 二进制体积。 -> macOS 当前不支持 UPX,所以上述预编译的 macOS 版本体积可能较大。 - -## 使用 static-php-cli 构建 PHP - -### 编译环境需求 - -- PHP >= 8.4(这是 spc 自身需要的版本,不是支持的构建版本) -- 扩展:`mbstring,tokenizer,phar` -- 系统安装了 `curl` 和 `git` - -是的,本项目采用 PHP 编写,编译前需要一个 PHP 环境,比较滑稽。 -但本项目默认可通过自身构建的 micro 和 static-php 二进制运行,其他只需要包含上面提到的扩展和 PHP 版本大于等于 8.1 即可。 - -下面是架构支持情况,:octocat: 代表支持 GitHub Action 构建,:computer: 代表支持本地构建,空 代表暂不支持。 - -| | x86_64 | aarch64 | -|---------|----------------------|----------------------| -| macOS | :octocat: :computer: | :octocat: :computer: | -| Linux | :octocat: :computer: | :octocat: :computer: | -| Windows | :octocat: :computer: | | -| FreeBSD | :computer: | :computer: | - -当前支持编译的 PHP 版本: - -> :warning: 部分支持,对于新的测试版和旧版本可能存在问题。 -> -> :heavy_check_mark: 支持 -> -> :x: 不支持 - -| PHP Version | Status | Comment | -|-------------|--------------------|---------------------------------------------------------| -| 7.2 | :x: | | -| 7.3 | :x: | phpmicro 和许多扩展不支持 7.3、7.4 版本 | -| 7.4 | :x: | phpmicro 和许多扩展不支持 7.3、7.4 版本 | -| 8.0 | :warning: | PHP 官方已停止 8.0 的维护,我们不再处理 8.0 相关的 backport 支持 | -| 8.1 | :heavy_check_mark: | PHP 官方仅对 8.1 提供安全更新,在 8.5 发布后我们不再处理 8.1 相关的 backport 支持 | -| 8.2 | :heavy_check_mark: | | -| 8.3 | :heavy_check_mark: | | -| 8.4 | :heavy_check_mark: | | -| 8.5 (alpha) | :warning: | PHP 8.5 目前处于 alpha 阶段 | - -> 这个表格的支持状态是 static-php-cli 对构建对应版本的支持情况,不是 PHP 官方对该版本的支持情况。 - -### 支持的扩展 - -请先根据下方扩展列表选择你要编译的扩展。 - -- [扩展支持列表](https://static-php.dev/zh/guide/extensions.html) -- [编译命令生成器](https://static-php.dev/zh/guide/cli-generator.html) - -> 如果这里没有你需要的扩展,可以提交 Issue。 - -### 在线构建(使用 GitHub Actions) - -使用 GitHub Action 可以方便地构建一个静态编译的 PHP,同时可以自行定义要编译的扩展。 - -1. Fork 本项目。 -2. 进入项目的 Actions,选择 CI。 -3. 选择 `Run workflow`,填入你要编译的 PHP 版本、目标类型、扩展列表。(扩展列表使用英文逗号分割,例如 `bcmath,curl,mbstring`) -4. 等待大约一段时间后,进入对应的任务中,获取 `Artifacts`。 - -如果你选择了 `debug`,则会在构建时输出所有日志,包括编译的日志,以供排查错误。 - -### 本地构建(使用 spc 二进制,推荐) - -该项目提供了 static-php-cli 的二进制文件:`spc`。 -您可以使用 `spc` 二进制文件,无需安装任何运行时(用起来就像 golang 程序)。 -目前,`spc` 二进制文件提供的平台有 Linux 和 macOS。 - -使用以下命令从自托管服务器下载: +### 1. 下载 spc 二进制文件 ```bash -# Download from self-hosted nightly builds (sync with main branch) -# For Linux x86_64 +# Linux x86_64 curl -fsSL -o spc https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-linux-x86_64 -# For Linux aarch64 +# Linux aarch64 curl -fsSL -o spc https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-linux-aarch64 # macOS x86_64 (Intel) curl -fsSL -o spc https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-macos-x86_64 # macOS aarch64 (Apple) curl -fsSL -o spc https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-macos-aarch64 -# Windows (x86_64, win10 build 17063 or later) +# Windows (x86_64, win10 build 17063 或更高版本,请先安装 VS2022) curl.exe -fsSL -o spc.exe https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-windows-x64.exe +``` -# Add execute perm (Linux and macOS only) +对于 macOS 和 Linux,请先添加执行权限: + +```bash chmod +x ./spc - -# Run (Linux and macOS) -./spc --version -# Run (Windows powershell) -.\spc.exe --version ``` -自托管 `spc` 由 GitHub Actions 构建,你也可以从 Actions 直接下载:[此处](https://github.com/crazywhalecc/static-php-cli/actions/workflows/release-build.yml)。 +### 2. 构建静态 PHP -### 本地构建(使用 git 源码) +首先,创建一个 `craft.yml` 文件,并从 [扩展列表](https://static-php.dev/zh/guide/extensions.html) 或 [命令生成器](https://static-php.dev/zh/guide/cli-generator.html) 中指定要包含的扩展: -如果你需要修改 static-php-cli 源码,或者使用 spc 二进制构建有问题,你可以使用 git 源码下载 static-php-cli。 +```yml +# PHP 版本支持:8.1, 8.2, 8.3, 8.4, 8.5 +php-version: 8.4 +# 在此处放置您的扩展列表 +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 + - micro + - fpm +download-options: + prefer-pre-built: true +``` + +运行命令: ```bash -# clone 仓库即可 -git clone https://github.com/crazywhalecc/static-php-cli.git +./spc craft + +# 输出完整控制台日志 +./spc craft --debug ``` -如果您的系统上尚未安装 php,我们建议你使用内置的 setup-runtime 自动安装 PHP 和 Composer。 +### 3. 静态 PHP 使用 -```bash -cd static-php-cli -chmod +x bin/setup-runtime -# it will download static php (from self-hosted server) and composer (from getcomposer) -bin/setup-runtime -# initialize composer deps -bin/composer install -# chmod -chmod +x bin/spc -bin/spc --version +现在您可以将 static-php-cli 构建的二进制文件复制到另一台机器上,无需依赖即可运行: + +``` +# php-cli +buildroot/bin/php -v + +# phpmicro +echo ' a.php +./spc micro:combine a.php -O my-app +./my-app + +# php-fpm +buildroot/bin/php-fpm -v ``` -### 开始构建 PHP +## 文档 -下面是使用 static-php-cli 的基础用法: +当前 README 包含基本用法。有关 static-php-cli 的所有功能, +请访问 。 -> 如果你使用的是打包好的 `spc` 二进制,你需要将下列命令的 `./bin/spc` 替换为 `./spc`。 +## 直接下载 -```bash -# 检查环境依赖,并根据尝试自动安装缺失的编译工具 -./bin/spc doctor --auto-fix +如果您不想构建或想先测试,可以从 [Actions](https://github.com/static-php/static-php-cli-hosted/actions/workflows/build-php-bulk.yml) 下载示例预编译工件,或从自托管服务器下载。 -# 输出目标项目依赖的扩展列表 -./bin/spc dump-extensions /path/to/your/project --format=text +以下是几个具有不同扩展组合的预编译静态 PHP 二进制文件, +您可以根据需要直接下载。 -# 拉取所有依赖库 -./bin/spc download --all -# 只拉取编译指定扩展需要的所有依赖(推荐) -./bin/spc download --for-extensions="openssl,pcntl,mbstring,pdo_sqlite" -# 下载依赖时,优先下载有预编译的库(节省编译依赖的时间) -./bin/spc download --for-extensions="openssl,curl,mbstring,mbregex" --prefer-pre-built -# 下载编译不同版本的 PHP (--with-php=x.y 或 --with-php=x.y.z,推荐 8.1 ~ 8.3) -./bin/spc download --for-extensions="openssl,curl,mbstring" --with-php=8.1 +| 组合名称 | 扩展数量 | 系统 | 备注 | +|----------------------------------------------------------------------|----------------------------------------------------------------------------|--------------|--------------------| +| [common](https://dl.static-php.dev/static-php-cli/common/) | [30+](https://dl.static-php.dev/static-php-cli/common/README.txt) | Linux, macOS | 二进制文件大小约为 7.5MB | +| [bulk](https://dl.static-php.dev/static-php-cli/bulk/) | [50+](https://dl.static-php.dev/static-php-cli/bulk/README.txt) | Linux, macOS | 二进制文件大小约为 25MB | +| [gnu-bulk](https://dl.static-php.dev/static-php-cli/gnu-bulk/) | [50+](https://dl.static-php.dev/static-php-cli/bulk/README.txt) | Linux, macOS | 使用 glibc 的 bulk 组合 | +| [minimal](https://dl.static-php.dev/static-php-cli/minimal/) | [5](https://dl.static-php.dev/static-php-cli/minimal/README.txt) | Linux, macOS | 二进制文件大小约为 3MB | +| [spc-min](https://dl.static-php.dev/static-php-cli/windows/spc-min/) | [5](https://dl.static-php.dev/static-php-cli/windows/spc-min/README.txt) | Windows | 二进制文件大小约为 3MB | +| [spc-max](https://dl.static-php.dev/static-php-cli/windows/spc-max/) | [40+](https://dl.static-php.dev/static-php-cli/windows/spc-max/README.txt) | Windows | 二进制文件大小约为 8.5MB | -# 构建包含 bcmath,openssl,tokenizer,sqlite3,pdo_sqlite,ftp,curl 扩展的 php-cli 和 micro.sfx -./bin/spc build "bcmath,openssl,tokenizer,sqlite3,pdo_sqlite,ftp,curl" --build-cli --build-micro -# 编译线程安全版本 (--enable-zts) -./bin/spc build "curl,phar" --enable-zts --build-cli -# 编译后使用 UPX 减小可执行文件体积 (仅 Linux、Windows 可用) (至少压缩至原来的 30~50%) -./bin/spc build "curl,phar" --enable-zts --build-cli --with-upx-pack -``` +> Linux 和 Windows 支持对二进制文件进行 UPX 压缩,可以将二进制文件大小减少 30% 到 50%。 +> macOS 不支持 UPX 压缩,因此 mac 的预构建二进制文件大小较大。 -其中,目前支持构建 cli,micro,fpm 和 embed,使用以下参数的一个或多个来指定编译的 SAPI: +### 在线构建(使用 GitHub Actions) -- `--build-cli`:构建 cli 二进制 -- `--build-micro`:构建 phpmicro 自执行二进制 -- `--build-fpm`:构建 fpm -- `--build-embed`:构建 embed(libphp) -- `--build-cgi`: 构建 cgi(不推荐) -- `--build-all`:构建所有 +上方直接下载的二进制不能满足需求时,可使用 GitHub Action 可以轻松构建静态编译的 PHP, +同时自行定义要编译的扩展。 -如果出现了任何错误,可以使用 `--debug` 参数来展示完整的输出日志,以供排查错误: +1. Fork 本项目。 +2. 进入项目的 Actions 并选择 `CI`。 +3. 选择 `Run workflow`,填入您要编译的 PHP 版本、目标类型和扩展列表。(扩展用逗号分隔,例如 `bcmath,curl,mbstring`) +4. 等待一段时间后,进入相应的任务并获取 `Artifacts`。 -```bash -./bin/spc build "openssl,pcntl,mbstring" --debug --build-all -./bin/spc download --all --debug -``` - -## 不同 SAPI 的使用 - -### 使用 cli - -> php-cli 是一个静态的二进制文件,类似 Go、Rust 语言编译后的单个可移植的二进制文件。 - -采用参数 `--build-cli` 或`--build-all` 参数时,最后编译结果会输出一个 `./php` 的二进制文件,此文件可分发、可直接使用。 -该文件编译后会存放在 `buildroot/bin/` 目录中,名称为 `php`,拷贝出来即可。 - -```bash -cd buildroot/bin/ -./php -v # 检查版本 -./php -m # 检查编译的扩展 -./php your_code.php # 运行代码 -./php your_project.phar # 运行打包为 phar 单文件的项目 -``` - -### 使用 micro - -> phpmicro 是一个提供自执行二进制 PHP 的项目,本项目依赖 phpmicro 进行编译自执行二进制。详见 [dixyes/phpmicro](https://github.com/dixyes/phpmicro)。 - -采用项目参数 `--build-micro` 或 `--build-all` 时,最后编译结果会输出一个 `./micro.sfx` 的文件,此文件需要配合你的 PHP 源码使用。 -该文件编译后会存放在 `buildroot/bin/` 目录中,拷贝出来即可。 - -使用时应准备好你的项目源码文件,可以是单个 PHP 文件,也可以是 Phar 文件。 - -```bash -echo " code.php -cat micro.sfx code.php > single-app && chmod +x single-app -./single-app -``` - -如果打包 PHAR 文件,仅需把 code.php 更换为 phar 文件路径即可。 -你可以使用 [box-project/box](https://github.com/box-project/box) 将你的 CLI 项目打包为 Phar, -然后将它与 phpmicro 结合,生成独立可执行的二进制文件。 - -```bash -# 使用 static-php-cli 生成的 micro.sfx 结合,也可以直接使用 cat 命令结合它们 -bin/spc micro:combine my-app.phar -cat buildroot/bin/micro.sfx my-app.phar > my-app && chmod +x my-app - -# 使用 micro:combine 结合可以将 INI 选项注入到二进制中 -bin/spc micro:combine my-app.phar -I "memory_limit=4G" -I "disable_functions=system" --output my-app-2 -``` - -> 有些情况下的 phar 文件或 PHP 项目可能无法在 micro 环境下运行。 - -### 使用 fpm - -采用项目参数 `--build-fpm` 或 `--build-all` 时,最后编译结果会输出一个 `./php-fpm` 的文件。 -该文件存放在 `buildroot/bin/` 目录,拷贝出来即可使用。 - -在正常的 Linux 发行版和 macOS 系统中,安装 php-fpm 后包管理会自动生成默认的 fpm 配置文件。 -因为 php-fpm 必须指定配置文件才可启动,本项目编译的 php-fpm 不会带任何配置文件,所以需自行编写 `php-fpm.conf` 和 `pool.conf` 配置文件。 - -指定 `php-fpm.conf` 可以使用命令参数 `-y`,例如:`./php-fpm -y php-fpm.conf`。 - -### 使用 embed - -采用项目参数 `--build-embed` 或 `--build-all` 时,最后编译结果会输出一个 `libphp.a`、`php-config` 以及一系列头文件,存放在 `buildroot/`,你可以在你的其他代码中引入它们。 - -如果你知道 [embed SAPI](https://github.com/php/php-src/tree/master/sapi/embed),你应该知道如何使用它。对于有可能编译用到引入其他库的问题,你可以使用 `buildroot/bin/php-config` 来获取编译时的配置。 - -另外,有关如何使用此功能的高级示例,请查看[如何使用它构建 FrankenPHP 的静态版本](https://github.com/php/frankenphp/blob/main/docs/static.md)。 +如果您启用 `debug`,构建时将输出所有日志,包括编译日志,以便故障排除。 ## 贡献 -如果缺少你需要的扩展,可发起 Issue。如果你对本项目较熟悉,也欢迎为本项目发起 Pull Request。 +如果您需要的扩展缺失,可以创建 issue。 +如果您熟悉本项目,也欢迎发起 pull request。 -另外,添加新扩展的贡献方式,可以参考下方 `进阶`。 +如果您想贡献文档,请直接编辑 `docs/` 目录。 -如果你想贡献文档内容,请直接修改 `docs/` 目录。 +现在有一个 [static-php](https://github.com/static-php) 组织,用于存储与项目相关的仓库。 ## 赞助本项目 -你可以在 [我的个人赞助页](https://github.com/crazywhalecc/crazywhalecc/blob/master/FUNDING.md) 支持我和我的项目。你捐赠的一部分将会被用于维护 **static-php.dev** 服务器。 +您可以从 [GitHub Sponsor](https://github.com/crazywhalecc) 赞助我或我的项目。您捐赠的一部分将用于维护 **static-php.dev** 服务器。 -**特别赞助商**: +**特别感谢以下赞助商**: Beyond Code Logo NativePHP Logo -## 开源协议 +## 开源许可证 -本项目采用 MIT License 许可开源,下面是类似的项目: +本项目本身基于 MIT 许可证, +一些新添加的扩展和依赖可能来自其他项目, +这些代码文件的头部也会给出额外的许可证和作者说明。 + +这些是类似的项目: - [dixyes/lwmbs](https://github.com/dixyes/lwmbs) - [swoole/swoole-cli](https://github.com/swoole/swoole-cli) -该项目使用了 [dixyes/lwmbs](https://github.com/dixyes/lwmbs) 中的一些代码,例如 Windows 静态构建目标和 libiconv 库支持。 -lwmbs 使用 [Mulan PSL 2](http://license.coscl.org.cn/MulanPSL2) 许可进行分发。对应文件有关于作者和许可的特殊说明,除此之外,均使用 MIT 授权许可。 +本项目使用了 [dixyes/lwmbs](https://github.com/dixyes/lwmbs) 的一些代码,例如 Windows 静态构建目标和 libiconv 支持。 +lwmbs 基于 [Mulan PSL 2](http://license.coscl.org.cn/MulanPSL2) 许可证。 -因本项目的特殊性,使用项目编译过程中会使用很多其他开源项目,例如 curl、protobuf 等,它们都有各自的开源协议。 -请在编译完成后,使用命令 `bin/spc dump-license` 导出项目使用项目的开源协议,并遵守对应项目的 LICENSE。 +由于本项目的特殊性, +项目编译过程中会使用许多其他开源项目,如 curl 和 protobuf, +它们都有自己的开源许可证。 + +请在编译后使用 `bin/spc dump-license` 命令导出项目中使用的开源许可证, +并遵守相应项目的 LICENSE。 diff --git a/README.md b/README.md index e405edf4..3f3bfbf1 100755 --- a/README.md +++ b/README.md @@ -5,29 +5,22 @@ [![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) -[![Extensions](https://img.shields.io/badge/Extension%20Counter-75+-yellow.svg?style=flat-square)](https://static-php.dev/en/guide/extensions.html) **static-php-cli** is a powerful tool designed for building static, standalone PHP runtime with popular extensions. -Static PHP built by **static-php-cli** supports `cli`, `fpm`, `embed`, `micro` and `frankenphp` SAPI. - -**static-php-cli** also has the ability to package PHP projects -along with the PHP interpreter into one single executable file. - ## Features -static-php-cli (you can call it `spc`) has a lot of features: - -- :handbag: Build single-file php executable, without any dependencies -- :hamburger: Build **[phpmicro](https://github.com/dixyes/phpmicro)** self-extracted executable (glue php binary and php source code into one file) -- :pill: Automatic build environment checker (Doctor module) +- :elephant: Support multiple PHP versions - PHP 8.1, 8.2, 8.3, 8.4, 8.5 +- :handbag: Build single-file PHP executable with zero dependencies +- :hamburger:Build **[phpmicro](https://github.com/dixyes/phpmicro)** self-extracting executables (combines PHP binary and source code into one file) +- :pill: Automatic build environment checker with auto-fix capabilities - :zap: `Linux`, `macOS`, `FreeBSD`, `Windows` support -- :wrench: Configurable source code patches -- :books: Build dependency management -- 📦 Provide `spc` own standalone executable (built by spc and [box](https://github.com/box-project/box)) -- :fire: Support many popular [extensions](https://static-php.dev/en/guide/extensions.html) -- :floppy_disk: UPX integration (significantly reduces binary size) +- :wrench: Configurable source code patching +- :books: Intelligent dependency management +- 📦 Self-contained `spc` executable (built with [box](https://github.com/box-project/box)) +- :fire: Support 100+ popular [extensions](https://static-php.dev/en/guide/extensions.html) +- :floppy_disk: UPX compression support (reduces binary size by 30-50%) **Single-file standalone php-cli:** @@ -37,6 +30,72 @@ static-php-cli (you can call it `spc`) has a lot of features: out2 +## Quickstart + +### 1. Download spc binary + +```bash +# For Linux x86_64 +curl -fsSL -o spc https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-linux-x86_64 +# For Linux aarch64 +curl -fsSL -o spc https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-linux-aarch64 +# macOS x86_64 (Intel) +curl -fsSL -o spc https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-macos-x86_64 +# macOS aarch64 (Apple) +curl -fsSL -o spc https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-macos-aarch64 +# Windows (x86_64, win10 build 17063 or later, please install VS2022 first) +curl.exe -fsSL -o spc.exe https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-windows-x64.exe +``` + +For macOS and Linux, add execute permission first: + +```bash +chmod +x ./spc +``` + +### 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): + +```yml +# PHP version support: 8.1, 8.2, 8.3, 8.4, 8.5 +php-version: 8.4 +# 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 + - micro + - fpm +download-options: + prefer-pre-built: true +``` + +Run command: + +```bash +./spc craft + +# Output full console log +./spc craft --debug +``` + +### 3. Static PHP usage + +Now you can copy binaries built by static-php-cli to another machine and run with no dependencies: + +``` +# php-cli +buildroot/bin/php -v + +# phpmicro +echo ' a.php +./spc micro:combine a.php -O my-app +./my-app + +# php-fpm +buildroot/bin/php-fpm -v +``` + ## Documentation The current README contains basic usage. For all the features of static-php-cli, @@ -53,6 +112,7 @@ which can be downloaded directly according to your needs. |----------------------------------------------------------------------|----------------------------------------------------------------------------|--------------|--------------------------------| | [common](https://dl.static-php.dev/static-php-cli/common/) | [30+](https://dl.static-php.dev/static-php-cli/common/README.txt) | Linux, macOS | The binary size is about 7.5MB | | [bulk](https://dl.static-php.dev/static-php-cli/bulk/) | [50+](https://dl.static-php.dev/static-php-cli/bulk/README.txt) | Linux, macOS | The binary size is about 25MB | +| [gnu-bulk](https://dl.static-php.dev/static-php-cli/gnu-bulk/) | [50+](https://dl.static-php.dev/static-php-cli/bulk/README.txt) | Linux, macOS | Using shared glibc | | [minimal](https://dl.static-php.dev/static-php-cli/minimal/) | [5](https://dl.static-php.dev/static-php-cli/minimal/README.txt) | Linux, macOS | The binary size is about 3MB | | [spc-min](https://dl.static-php.dev/static-php-cli/windows/spc-min/) | [5](https://dl.static-php.dev/static-php-cli/windows/spc-min/README.txt) | Windows | The binary size is about 3MB | | [spc-max](https://dl.static-php.dev/static-php-cli/windows/spc-max/) | [40+](https://dl.static-php.dev/static-php-cli/windows/spc-max/README.txt) | Windows | The binary size is about 8.5MB | @@ -60,64 +120,10 @@ which can be downloaded directly according to your needs. > Linux and Windows supports UPX compression for binaries, 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 mac is larger. -## Build - -### Compilation Requirements - -You can say I made a PHP builder written in PHP, pretty funny. -But static-php-cli runtime only requires an environment above PHP 8.1 and extensions mentioned below. - -- PHP >= 8.4 (This is the version required by spc itself, not the build version) -- Extension: `mbstring,tokenizer,phar` -- Supported OS with `curl` and `git` installed - -Here is the supported OS and arch, where :octocat: represents support for GitHub Action builds, -:computer: represents support for local manual builds, and blank represents not currently supported. - -| | x86_64 | aarch64 | -|---------|----------------------|----------------------| -| macOS | :octocat: :computer: | :octocat: :computer: | -| Linux | :octocat: :computer: | :octocat: :computer: | -| Windows | :octocat: :computer: | | -| FreeBSD | :computer: | :computer: | - -Currently supported PHP versions for compilation: - -> :warning: Partial support, there may be issues with newer test versions or older versions. -> -> :heavy_check_mark: supported -> -> :x: not supported - -| PHP Version | Status | Comment | -|-------------|--------------------|----------------------------------------------------------------------------------------------------| -| 7.2 | :x: | | -| 7.3 | :x: | phpmicro and some extensions not supported on 7.x | -| 7.4 | :x: | phpmicro and some extensions not supported on 7.x | -| 8.0 | :warning: | PHP official has stopped maintenance of 8.0, we no longer provide backport support for version 8.0 | -| 8.1 | :heavy_check_mark: | PHP official has security fixes only, we no longer provide backport support when 8.5 released | -| 8.2 | :heavy_check_mark: | | -| 8.3 | :heavy_check_mark: | | -| 8.4 | :heavy_check_mark: | | -| 8.5 (alpha) | :warning: | PHP 8.5 is in alpha | - -> This table shows the support status for static-php-cli in building the corresponding version, -> not the official PHP support status for that version. - -### Supported Extensions - -Please first select the extension you want to compile based on the extension list below. - -- [Supported Extension List](https://static-php.dev/en/guide/extensions.html) -- [Command Generator](https://static-php.dev/en/guide/cli-generator.html) - -> If an extension you need is missing, you can submit an issue. - -Here is the current planned roadmap for extension support: [#152](https://github.com/crazywhalecc/static-php-cli/issues/152) . - ### Build Online (using GitHub Actions) -Use GitHub Action to easily build a statically compiled PHP, +When the above direct download binaries cannot meet your needs, +you can use GitHub Action to easily build a statically compiled PHP, and at the same time define the extensions to be compiled by yourself. 1. Fork me. @@ -127,185 +133,6 @@ and at the same time define the extensions to be compiled by yourself. If you enable `debug`, all logs will be output at build time, including compiled logs, for troubleshooting. -### Build Locally (using SPC binary, recommended) - -This project provides a binary file of static-php-cli: `spc`. -You can use `spc` binary instead of installing any runtime like golang app. -Currently, the platforms supported by `spc` binary are Linux and macOS. - -Download from self-hosted nightly builds using commands below: - -```bash -# Download from self-hosted nightly builds (sync with main branch) -# For Linux x86_64 -curl -fsSL -o spc https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-linux-x86_64 -# For Linux aarch64 -curl -fsSL -o spc https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-linux-aarch64 -# macOS x86_64 (Intel) -curl -fsSL -o spc https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-macos-x86_64 -# macOS aarch64 (Apple) -curl -fsSL -o spc https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-macos-aarch64 -# Windows (x86_64, win10 build 17063 or later) -curl.exe -fsSL -o spc.exe https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-windows-x64.exe - -# Add execute perm (Linux and macOS only) -chmod +x ./spc - -# Run (Linux and macOS) -./spc --version -# Run (Windows powershell) -.\spc.exe --version -``` - -Self-hosted `spc` is built by GitHub Actions, you can also download from Actions artifacts [here](https://github.com/crazywhalecc/static-php-cli/actions/workflows/release-build.yml). - -### Build Locally (using git source) - -If you need to modify the static-php-cli source code, or have problems using the spc binary build, -you can download static-php-cli using the git source code. - -```bash -# just clone me! -git clone https://github.com/crazywhalecc/static-php-cli.git -``` - -If you have not installed php on your system, we recommend that you use the built-in setup-runtime to install PHP and Composer automatically. - -```bash -cd static-php-cli -chmod +x bin/setup-runtime -# it will download static php (from self-hosted server) and composer (from getcomposer) -bin/setup-runtime -# initialize composer deps -bin/composer install -# chmod -chmod +x bin/spc -bin/spc --version -``` - -### Start Building PHP - -Basic usage for building php with some extensions: - -> If you are using the packaged standalone `spc` binary, you need to replace `bin/spc` with `./spc` or `.\spc.exe` in the following commands. - -```bash -# Check system tool dependencies, auto-fix them if possible -./bin/spc doctor --auto-fix - -# fetch all libraries -./bin/spc download --all -# dump a list of extensions required by your project -./bin/spc dump-extensions /path/to/your/project --format=text -# only fetch necessary sources by needed extensions (recommended) -./bin/spc download --for-extensions="openssl,pcntl,mbstring,pdo_sqlite" -# download pre-built libraries first (save time for compiling dependencies) -./bin/spc download --for-extensions="openssl,curl,mbstring,mbregex" --prefer-pre-built -# download different PHP version (--with-php=x.y or --with-php=x.y.z, recommend 8.3 ~ 8.4) -./bin/spc download --for-extensions="openssl,curl,mbstring" --with-php=8.1 - -# with bcmath,openssl,tokenizer,sqlite3,pdo_sqlite,ftp,curl extension, build both CLI and phpmicro SAPI -./bin/spc build "bcmath,openssl,tokenizer,sqlite3,pdo_sqlite,ftp,curl" --build-cli --build-micro -# build thread-safe (ZTS) version (--enable-zts) -./bin/spc build "curl,phar" --enable-zts --build-cli -# build, pack executable with UPX (linux and windows only) (reduce binary size for 30~50%) -./bin/spc build "curl,phar" --enable-zts --build-cli --with-upx-pack -``` - -Now we support `cli`, `micro`, `fpm` and `embed` SAPI. You can use one or more of the following parameters to specify the compiled SAPI: - -- `--build-cli`: build static cli executable -- `--build-micro`: build static phpmicro self-extracted executable -- `--build-fpm`: build static fpm binary -- `--build-embed`: build embed (libphp) -- `--build-cgi`: build cgi binary (not recommended) -- `--build-all`: build all - -If anything goes wrong, use `--debug` option to display full terminal output: - -```bash -./bin/spc build "openssl,pcntl,mbstring" --debug --build-all -./bin/spc download --all --debug -``` - -## Different SAPI Usage - -### Use cli - -> php-cli is a single static binary, you can use it like normal php installed on your system. - -When using the parameter `--build-cli` or `--build-all`, -the final compilation result will output a binary file named `./php`, -which can be distributed and used directly. -This file will be located in the directory `buildroot/bin/`, copy it out for use. - -```bash -cd buildroot/bin/ -./php -v # check version -./php -m # check extensions -./php your_code.php # run your php code -./php your_project.phar # run your phar (project archive) -``` - -### Use micro - -> phpmicro is a SelF-extracted eXecutable SAPI module, -> provided by [phpmicro](https://github.com/dixyes/phpmicro) project. -> But this project is using a [fork](https://github.com/static-php/phpmicro) of phpmicro, because we need to add some features to it. -> It can put php runtime and your source code together. - -When using the parameter `--build-all` or `--build-micro`, -the final compilation result will output a file named `./micro.sfx`, -which needs to be used with your PHP source code like `code.php`. -This file will be located in the path `buildroot/bin/micro.sfx`, simply copy it out for use. - -Prepare your project source code, which can be a single PHP file or a Phar file, for use. - -```bash -echo " code.php -cat micro.sfx code.php > single-app && chmod +x single-app -./single-app -``` - -If you package a PHAR file, just replace `code.php` with the phar file path. -You can use [box-project/box](https://github.com/box-project/box) to package your CLI project as Phar, -It is then combined with phpmicro to produce a standalone executable binary. - -```bash -# Use the micro.sfx generated by static-php-cli to combine, -bin/spc micro:combine my-app.phar -# or you can directly use the cat command to combine them. -cat buildroot/bin/micro.sfx my-app.phar > my-app && chmod +x my-app - -# Use micro:combine combination to inject INI options into the binary. -bin/spc micro:combine my-app.phar -I "memory_limit=4G" -I "disable_functions=system" --output my-app-2 -``` - -> In some cases, PHAR files may not run in a micro environment. Overall, micro is not production ready. - -### Use fpm - -When using the parameter `--build-all` or `--build-fpm`, -the final compilation result will output a file named `./php-fpm`, -This file will be located in the path `buildroot/bin/`, simply copy it out for use. - -In common Linux distributions and macOS systems, the package manager will automatically generate a default fpm configuration file after installing php-fpm. -Because php-fpm must specify a configuration file before running, the php-fpm compiled by this project will not have any configuration files, so you need to write `php-fpm.conf` and `pool.conf` configuration files yourself. - -Specifying `php-fpm.conf` can use the command parameter `-y`, for example: `./php-fpm -y php-fpm.conf`. - -### Use embed - -When using the project parameters `--build-embed` or `--build-all`, -the final compilation result will output a `libphp.a`, `php-config` and a series of header files, -stored in `buildroot/`. You can introduce them in your other projects. - -If you know [embed SAPI](https://github.com/php/php-src/tree/master/sapi/embed), you should know how to use it. -You may require the introduction of other libraries during compilation, -you can use `buildroot/bin/php-config` to obtain the compile-time configuration. - -For an advanced example of how to use this feature, take a look at [how to use it to build a static version of FrankenPHP](https://github.com/php/frankenphp/blob/main/docs/static.md). - ## Contribution If the extension you need is missing, you can create an issue. diff --git a/bin/spc-alpine-docker b/bin/spc-alpine-docker index 3a58547f..6365233c 100755 --- a/bin/spc-alpine-docker +++ b/bin/spc-alpine-docker @@ -150,6 +150,7 @@ MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/source:/app/source" MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/dist:/app/dist" MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/downloads:/app/downloads" MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/pkgroot:/app/pkgroot" +MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/log:/app/log" if [ -f "$(pwd)/craft.yml" ]; then MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/craft.yml:/app/craft.yml" fi diff --git a/bin/spc-gnu-docker b/bin/spc-gnu-docker index 97c9a089..7fcf5d41 100755 --- a/bin/spc-gnu-docker +++ b/bin/spc-gnu-docker @@ -158,6 +158,7 @@ MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/source:/app/source" MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/dist:/app/dist" MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/downloads:/app/downloads" MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/pkgroot:/app/pkgroot" +MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/log:/app/log" if [ -f "$(pwd)/craft.yml" ]; then MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/craft.yml:/app/craft.yml" fi diff --git a/composer.lock b/composer.lock index 069a340f..4b28fc61 100644 --- a/composer.lock +++ b/composer.lock @@ -8,7 +8,7 @@ "packages": [ { "name": "illuminate/collections", - "version": "v11.45.3", + "version": "v11.46.1", "source": { "type": "git", "url": "https://github.com/illuminate/collections.git", @@ -64,7 +64,7 @@ }, { "name": "illuminate/conditionable", - "version": "v11.45.3", + "version": "v11.46.1", "source": { "type": "git", "url": "https://github.com/illuminate/conditionable.git", @@ -110,7 +110,7 @@ }, { "name": "illuminate/contracts", - "version": "v11.45.3", + "version": "v11.46.1", "source": { "type": "git", "url": "https://github.com/illuminate/contracts.git", @@ -158,7 +158,7 @@ }, { "name": "illuminate/macroable", - "version": "v11.45.3", + "version": "v11.46.1", "source": { "type": "git", "url": "https://github.com/illuminate/macroable.git", @@ -416,16 +416,16 @@ }, { "name": "symfony/console", - "version": "v7.3.3", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7" + "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7", - "reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7", + "url": "https://api.github.com/repos/symfony/console/zipball/2b9c5fafbac0399a20a2e82429e2bd735dcfb7db", + "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db", "shasum": "" }, "require": { @@ -490,7 +490,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.3" + "source": "https://github.com/symfony/console/tree/v7.3.4" }, "funding": [ { @@ -510,7 +510,7 @@ "type": "tidelift" } ], - "time": "2025-08-25T06:35:40+00:00" + "time": "2025-09-22T15:31:00+00:00" }, { "name": "symfony/deprecation-contracts", @@ -916,16 +916,16 @@ }, { "name": "symfony/process", - "version": "v7.3.3", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "32241012d521e2e8a9d713adb0812bb773b907f1" + "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/32241012d521e2e8a9d713adb0812bb773b907f1", - "reference": "32241012d521e2e8a9d713adb0812bb773b907f1", + "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b", + "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b", "shasum": "" }, "require": { @@ -957,7 +957,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.3.3" + "source": "https://github.com/symfony/process/tree/v7.3.4" }, "funding": [ { @@ -977,7 +977,7 @@ "type": "tidelift" } ], - "time": "2025-08-18T09:42:54+00:00" + "time": "2025-09-11T10:12:26+00:00" }, { "name": "symfony/service-contracts", @@ -1064,16 +1064,16 @@ }, { "name": "symfony/string", - "version": "v7.3.3", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c" + "reference": "f96476035142921000338bad71e5247fbc138872" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/17a426cce5fd1f0901fefa9b2a490d0038fd3c9c", - "reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c", + "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", + "reference": "f96476035142921000338bad71e5247fbc138872", "shasum": "" }, "require": { @@ -1088,7 +1088,6 @@ }, "require-dev": { "symfony/emoji": "^7.1", - "symfony/error-handler": "^6.4|^7.0", "symfony/http-client": "^6.4|^7.0", "symfony/intl": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3.0", @@ -1131,7 +1130,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.3" + "source": "https://github.com/symfony/string/tree/v7.3.4" }, "funding": [ { @@ -1151,7 +1150,7 @@ "type": "tidelift" } ], - "time": "2025-08-25T06:35:40+00:00" + "time": "2025-09-11T14:36:48+00:00" }, { "name": "symfony/yaml", @@ -2861,16 +2860,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.87.1", + "version": "v3.89.0", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "2f5170365e2a422d0c5421f9c8818b2c078105f6" + "reference": "4dd6768cb7558440d27d18f54909eee417317ce9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/2f5170365e2a422d0c5421f9c8818b2c078105f6", - "reference": "2f5170365e2a422d0c5421f9c8818b2c078105f6", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/4dd6768cb7558440d27d18f54909eee417317ce9", + "reference": "4dd6768cb7558440d27d18f54909eee417317ce9", "shasum": "" }, "require": { @@ -2885,7 +2884,6 @@ "php": "^7.4 || ^8.0", "react/child-process": "^0.6.6", "react/event-loop": "^1.5", - "react/promise": "^3.3", "react/socket": "^1.16", "react/stream": "^1.4", "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0", @@ -2897,12 +2895,13 @@ "symfony/polyfill-mbstring": "^1.33", "symfony/polyfill-php80": "^1.33", "symfony/polyfill-php81": "^1.33", + "symfony/polyfill-php84": "^1.33", "symfony/process": "^5.4.47 || ^6.4.24 || ^7.2", "symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0" }, "require-dev": { "facile-it/paraunit": "^1.3.1 || ^2.7", - "infection/infection": "^0.29.14", + "infection/infection": "^0.31.0", "justinrainbow/json-schema": "^6.5", "keradus/cli-executor": "^2.2", "mikey179/vfsstream": "^1.6.12", @@ -2910,7 +2909,6 @@ "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6", "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6", "phpunit/phpunit": "^9.6.25 || ^10.5.53 || ^11.5.34", - "symfony/polyfill-php84": "^1.33", "symfony/var-dumper": "^5.4.48 || ^6.4.24 || ^7.3.2", "symfony/yaml": "^5.4.45 || ^6.4.24 || ^7.3.2" }, @@ -2953,7 +2951,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.87.1" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.89.0" }, "funding": [ { @@ -2961,20 +2959,20 @@ "type": "github" } ], - "time": "2025-09-02T15:27:36+00:00" + "time": "2025-10-18T19:30:16+00:00" }, { "name": "humbug/box", - "version": "4.6.6", + "version": "4.6.8", "source": { "type": "git", "url": "https://github.com/box-project/box.git", - "reference": "09646041cb2e0963ab6ca109e1b366337617a0f2" + "reference": "05d205d99ddb72f3729658a0115db02cfc08912e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/box-project/box/zipball/09646041cb2e0963ab6ca109e1b366337617a0f2", - "reference": "09646041cb2e0963ab6ca109e1b366337617a0f2", + "url": "https://api.github.com/repos/box-project/box/zipball/05d205d99ddb72f3729658a0115db02cfc08912e", + "reference": "05d205d99ddb72f3729658a0115db02cfc08912e", "shasum": "" }, "require": { @@ -2988,13 +2986,13 @@ "fidry/console": "^0.6.0", "fidry/filesystem": "^1.2.1", "humbug/php-scoper": "^0.18.14", - "justinrainbow/json-schema": "^5.2.12", + "justinrainbow/json-schema": "^6.2.0", "nikic/iter": "^2.2", "php": "^8.2", "phpdocumentor/reflection-docblock": "^5.4", "phpdocumentor/type-resolver": "^1.7", "psr/log": "^3.0", - "sebastian/diff": "^5.0", + "sebastian/diff": "^5.0 || ^6.0 || ^7.0", "seld/jsonlint": "^1.10.2", "seld/phar-utils": "^1.2", "symfony/finder": "^6.4.0 || ^7.0.0", @@ -3005,6 +3003,9 @@ "thecodingmachine/safe": "^2.5 || ^3.0", "webmozart/assert": "^1.11" }, + "conflict": { + "marc-mabe/php-enum": "<4.4" + }, "replace": { "symfony/polyfill-php80": "*", "symfony/polyfill-php81": "*", @@ -3070,22 +3071,22 @@ ], "support": { "issues": "https://github.com/box-project/box/issues", - "source": "https://github.com/box-project/box/tree/4.6.6" + "source": "https://github.com/box-project/box/tree/4.6.8" }, - "time": "2025-03-02T18:20:45+00:00" + "time": "2025-10-13T17:13:17+00:00" }, { "name": "humbug/php-scoper", - "version": "0.18.17", + "version": "0.18.18", "source": { "type": "git", "url": "https://github.com/humbug/php-scoper.git", - "reference": "0a2556c7c23776a61cf22689e2f24298ba00e33a" + "reference": "dd55d01a937602c9473cfbe0ecab9521cb9740aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/humbug/php-scoper/zipball/0a2556c7c23776a61cf22689e2f24298ba00e33a", - "reference": "0a2556c7c23776a61cf22689e2f24298ba00e33a", + "url": "https://api.github.com/repos/humbug/php-scoper/zipball/dd55d01a937602c9473cfbe0ecab9521cb9740aa", + "reference": "dd55d01a937602c9473cfbe0ecab9521cb9740aa", "shasum": "" }, "require": { @@ -3154,9 +3155,9 @@ "description": "Prefixes all PHP namespaces in a file or directory.", "support": { "issues": "https://github.com/humbug/php-scoper/issues", - "source": "https://github.com/humbug/php-scoper/tree/0.18.17" + "source": "https://github.com/humbug/php-scoper/tree/0.18.18" }, - "time": "2025-02-19T22:50:39+00:00" + "time": "2025-10-15T15:29:47+00:00" }, { "name": "jetbrains/phpstorm-stubs", @@ -3207,30 +3208,40 @@ }, { "name": "justinrainbow/json-schema", - "version": "5.3.0", + "version": "6.6.0", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8" + "reference": "68ba7677532803cc0c5900dd5a4d730537f2b2f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", - "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/68ba7677532803cc0c5900dd5a4d730537f2b2f3", + "reference": "68ba7677532803cc0c5900dd5a4d730537f2b2f3", "shasum": "" }, "require": { - "php": ">=7.1" + "ext-json": "*", + "marc-mabe/php-enum": "^4.0", + "php": "^7.2 || ^8.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", - "json-schema/json-schema-test-suite": "1.2.0", - "phpunit/phpunit": "^4.8.35" + "friendsofphp/php-cs-fixer": "3.3.0", + "json-schema/json-schema-test-suite": "^23.2", + "marc-mabe/php-enum-phpstan": "^2.0", + "phpspec/prophecy": "^1.19", + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^8.5" }, "bin": [ "bin/validate-json" ], "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, "autoload": { "psr-4": { "JsonSchema\\": "src/JsonSchema/" @@ -3259,16 +3270,16 @@ } ], "description": "A library to validate a json schema.", - "homepage": "https://github.com/justinrainbow/json-schema", + "homepage": "https://github.com/jsonrainbow/json-schema", "keywords": [ "json", "schema" ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/5.3.0" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.6.0" }, - "time": "2024-07-06T21:00:26+00:00" + "time": "2025-10-10T11:34:09+00:00" }, { "name": "kelunik/certificate", @@ -3502,6 +3513,79 @@ ], "time": "2024-12-08T08:18:47+00:00" }, + { + "name": "marc-mabe/php-enum", + "version": "v4.7.2", + "source": { + "type": "git", + "url": "https://github.com/marc-mabe/php-enum.git", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/bb426fcdd65c60fb3638ef741e8782508fda7eef", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef", + "shasum": "" + }, + "require": { + "ext-reflection": "*", + "php": "^7.1 | ^8.0" + }, + "require-dev": { + "phpbench/phpbench": "^0.16.10 || ^1.0.4", + "phpstan/phpstan": "^1.3.1", + "phpunit/phpunit": "^7.5.20 | ^8.5.22 | ^9.5.11", + "vimeo/psalm": "^4.17.0 | ^5.26.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-3.x": "3.2-dev", + "dev-master": "4.7-dev" + } + }, + "autoload": { + "psr-4": { + "MabeEnum\\": "src/" + }, + "classmap": [ + "stubs/Stringable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Marc Bennewitz", + "email": "dev@mabe.berlin", + "homepage": "https://mabe.berlin/", + "role": "Lead" + } + ], + "description": "Simple and fast implementation of enumerations with native PHP", + "homepage": "https://github.com/marc-mabe/php-enum", + "keywords": [ + "enum", + "enum-map", + "enum-set", + "enumeration", + "enumerator", + "enummap", + "enumset", + "map", + "set", + "type", + "type-hint", + "typehint" + ], + "support": { + "issues": "https://github.com/marc-mabe/php-enum/issues", + "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.2" + }, + "time": "2025-09-14T11:18:39+00:00" + }, { "name": "myclabs/deep-copy", "version": "1.13.4", @@ -4228,16 +4312,11 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.28", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9" - }, + "version": "1.12.32", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/fcf8b71aeab4e1a1131d1783cef97b23a51b87a9", - "reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/2770dcdf5078d0b0d53f94317e06affe88419aa8", + "reference": "2770dcdf5078d0b0d53f94317e06affe88419aa8", "shasum": "" }, "require": { @@ -4282,7 +4361,7 @@ "type": "github" } ], - "time": "2025-07-17T17:15:39+00:00" + "time": "2025-09-30T10:16:31+00:00" }, { "name": "phpunit/php-code-coverage", @@ -4607,16 +4686,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.53", + "version": "10.5.58", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "32768472ebfb6969e6c7399f1c7b09009723f653" + "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/32768472ebfb6969e6c7399f1c7b09009723f653", - "reference": "32768472ebfb6969e6c7399f1c7b09009723f653", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e24fb46da450d8e6a5788670513c1af1424f16ca", + "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca", "shasum": "" }, "require": { @@ -4637,10 +4716,10 @@ "phpunit/php-timer": "^6.0.0", "sebastian/cli-parser": "^2.0.1", "sebastian/code-unit": "^2.0.0", - "sebastian/comparator": "^5.0.3", + "sebastian/comparator": "^5.0.4", "sebastian/diff": "^5.1.1", "sebastian/environment": "^6.1.0", - "sebastian/exporter": "^5.1.2", + "sebastian/exporter": "^5.1.4", "sebastian/global-state": "^6.0.2", "sebastian/object-enumerator": "^5.0.0", "sebastian/recursion-context": "^5.0.1", @@ -4688,7 +4767,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.53" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.58" }, "funding": [ { @@ -4712,7 +4791,7 @@ "type": "tidelift" } ], - "time": "2025-08-20T14:40:06+00:00" + "time": "2025-09-28T12:04:46+00:00" }, { "name": "psr/event-dispatcher", @@ -5640,16 +5719,16 @@ }, { "name": "sebastian/comparator", - "version": "5.0.3", + "version": "5.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e" + "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", - "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e8e53097718d2b53cfb2aa859b06a41abf58c62e", + "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e", "shasum": "" }, "require": { @@ -5705,15 +5784,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.3" + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.4" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" } ], - "time": "2024-10-18T14:56:07+00:00" + "time": "2025-09-07T05:25:07+00:00" }, { "name": "sebastian/complexity", @@ -5906,16 +5997,16 @@ }, { "name": "sebastian/exporter", - "version": "5.1.2", + "version": "5.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "955288482d97c19a372d3f31006ab3f37da47adf" + "reference": "0735b90f4da94969541dac1da743446e276defa6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/955288482d97c19a372d3f31006ab3f37da47adf", - "reference": "955288482d97c19a372d3f31006ab3f37da47adf", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6", + "reference": "0735b90f4da94969541dac1da743446e276defa6", "shasum": "" }, "require": { @@ -5924,7 +6015,7 @@ "sebastian/recursion-context": "^5.0" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { @@ -5972,15 +6063,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.2" + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.4" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-03-02T07:17:12+00:00" + "time": "2025-09-24T06:09:11+00:00" }, { "name": "sebastian/global-state", @@ -7108,16 +7211,16 @@ }, { "name": "symfony/var-dumper", - "version": "v7.3.3", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "34d8d4c4b9597347306d1ec8eb4e1319b1e6986f" + "reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/34d8d4c4b9597347306d1ec8eb4e1319b1e6986f", - "reference": "34d8d4c4b9597347306d1ec8eb4e1319b1e6986f", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb", + "reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb", "shasum": "" }, "require": { @@ -7171,7 +7274,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.3.3" + "source": "https://github.com/symfony/var-dumper/tree/v7.3.4" }, "funding": [ { @@ -7191,7 +7294,7 @@ "type": "tidelift" } ], - "time": "2025-08-13T11:49:31+00:00" + "time": "2025-09-11T10:12:26+00:00" }, { "name": "thecodingmachine/safe", diff --git a/config/env.ini b/config/env.ini index 8b5a58d4..7d770426 100644 --- a/config/env.ini +++ b/config/env.ini @@ -34,6 +34,7 @@ ; SPC_LINUX_DEFAULT_CC: the default compiler for linux. (For alpine linux: `gcc`, default: `$GNU_ARCH-linux-musl-gcc`) ; SPC_LINUX_DEFAULT_CXX: the default c++ compiler for linux. (For alpine linux: `g++`, default: `$GNU_ARCH-linux-musl-g++`) ; SPC_LINUX_DEFAULT_AR: the default archiver for linux. (For alpine linux: `ar`, default: `$GNU_ARCH-linux-musl-ar`) +; SPC_EXTRA_PHP_VARS: the extra vars for building php, used in `configure` and `make` command. [global] ; Build concurrency for make -jN, default is CPU_COUNT, this value are used in every libs. @@ -104,8 +105,6 @@ SPC_MICRO_PATCHES=cli_checks,disable_huge_page SPC_CMD_PREFIX_PHP_BUILDCONF="./buildconf --force" ; configure command SPC_CMD_PREFIX_PHP_CONFIGURE="./configure --prefix= --with-valgrind=no --disable-shared --enable-static --disable-all --disable-phpdbg --with-pic" -; make command -SPC_CMD_PREFIX_PHP_MAKE="make -j${SPC_CONCURRENCY}" ; *** default build vars for building php *** ; embed type for php, static (libphp.a) or shared (libphp.so) @@ -138,8 +137,6 @@ SPC_MICRO_PATCHES=cli_checks,macos_iconv SPC_CMD_PREFIX_PHP_BUILDCONF="./buildconf --force" ; configure command SPC_CMD_PREFIX_PHP_CONFIGURE="./configure --prefix= --with-valgrind=no --enable-shared=no --enable-static=yes --disable-all --disable-phpdbg" -; make command -SPC_CMD_PREFIX_PHP_MAKE="make -j${SPC_CONCURRENCY}" ; *** default build vars for building php *** ; embed type for php, static (libphp.a) or shared (libphp.dylib) diff --git a/config/ext.json b/config/ext.json index 8e02b118..e5aa0ce3 100644 --- a/config/ext.json +++ b/config/ext.json @@ -341,6 +341,7 @@ "ext-depends": [ "xml" ], + "build-with-php": true, "target": [ "static" ] @@ -414,9 +415,17 @@ "libmemcached", "fastlz" ], + "lib-suggests": [ + "zstd" + ], "ext-depends": [ "session", "zlib" + ], + "ext-suggests": [ + "igbinary", + "msgpack", + "session" ] }, "mongodb": { @@ -445,7 +454,7 @@ "type": "external", "source": "msgpack", "arg-type-unix": "with", - "arg-type-win": "enable", + "arg-type-windows": "enable", "ext-depends": [ "session" ] @@ -453,6 +462,7 @@ "mysqli": { "type": "builtin", "arg-type": "with", + "build-with-php": true, "ext-depends": [ "mysqlnd" ] @@ -460,6 +470,7 @@ "mysqlnd": { "type": "builtin", "arg-type-windows": "with", + "build-with-php": true, "lib-depends": [ "zlib" ] @@ -679,7 +690,7 @@ "type": "builtin", "arg-type": "with-path", "lib-depends": [ - "readline" + "libedit" ], "target": [ "static" @@ -751,11 +762,9 @@ }, "type": "builtin", "arg-type": "custom", - "lib-depends": [ - "libxml2" - ], - "ext-depends-windows": [ - "xml" + "ext-depends": [ + "libxml", + "session" ] }, "sockets": { @@ -791,6 +800,7 @@ "type": "builtin", "arg-type": "with-path", "arg-type-windows": "with", + "build-with-php": true, "lib-depends": [ "sqlite" ] @@ -879,6 +889,22 @@ "mysqli" ] }, + "swoole-hook-odbc": { + "support": { + "Windows": "no", + "BSD": "wip" + }, + "notes": true, + "type": "addon", + "arg-type": "none", + "ext-depends": [ + "pdo", + "swoole" + ], + "lib-depends": [ + "unixodbc" + ] + }, "swoole-hook-pgsql": { "support": { "Windows": "no", @@ -908,22 +934,6 @@ "swoole" ] }, - "swoole-hook-odbc": { - "support": { - "Windows": "no", - "BSD": "wip" - }, - "notes": true, - "type": "addon", - "arg-type": "none", - "ext-depends": [ - "pdo", - "swoole" - ], - "lib-depends": [ - "unixodbc" - ] - }, "swow": { "support": { "BSD": "wip" @@ -1146,8 +1156,9 @@ "support": { "BSD": "wip" }, - "type": "builtin", - "arg-type": "with-path", + "type": "external", + "source": "ext-zip", + "arg-type": "custom", "arg-type-windows": "enable", "lib-depends-unix": [ "libzip" @@ -1170,6 +1181,7 @@ "lib-depends": [ "zlib" ], + "build-with-php": true, "target": [ "static" ] diff --git a/config/lib.json b/config/lib.json index eccda304..09779cf9 100644 --- a/config/lib.json +++ b/config/lib.json @@ -10,17 +10,19 @@ "micro", "frankenphp" ], + "lib-depends-macos": [ + "lib-base", + "micro", + "libxml2" + ], "lib-suggests-linux": [ "libacl", "brotli", "watcher" ], - "lib-suggests-unix": [ + "lib-suggests-macos": [ "brotli", "watcher" - ], - "lib-depends-macos": [ - "libxml2" ] }, "micro": { @@ -201,7 +203,7 @@ "openssl", "libcares" ], - "provide-pre-built": true, + "cpp-library": true, "frameworks": [ "CoreFoundation" ] @@ -229,6 +231,7 @@ }, "imagemagick": { "source": "imagemagick", + "cpp-library": true, "pkg-configs": [ "Magick++-7.Q16HDRI", "MagickCore-7.Q16HDRI", @@ -342,6 +345,15 @@ ], "cpp-library": true }, + "libedit": { + "source": "libedit", + "static-libs-unix": [ + "libedit.a" + ], + "lib-depends": [ + "ncurses" + ] + }, "libevent": { "source": "libevent", "static-libs-unix": [ @@ -451,6 +463,7 @@ }, "libmemcached": { "source": "libmemcached", + "cpp-library": true, "static-libs-unix": [ "libmemcached.a", "libmemcachedprotocol.a", @@ -547,6 +560,21 @@ "zstd" ] }, + "liburing": { + "source": "liburing", + "pkg-configs": [ + "liburing", + "liburing-ffi" + ], + "static-libs-linux": [ + "liburing.a", + "liburing-ffi.a" + ], + "headers-linux": [ + "liburing/", + "liburing.h" + ] + }, "libuuid": { "source": "libuuid", "static-libs-unix": [ @@ -595,7 +623,6 @@ ], "lib-suggests-unix": [ "xz", - "icu", "zlib" ], "lib-depends-windows": [ @@ -769,7 +796,7 @@ "libxml2", "openssl", "zlib", - "readline" + "libedit" ], "lib-suggests": [ "icu", @@ -821,6 +848,7 @@ }, "snappy": { "source": "snappy", + "cpp-library": true, "static-libs-unix": [ "libsnappy.a" ], @@ -866,6 +894,7 @@ }, "watcher": { "source": "watcher", + "cpp-library": true, "static-libs-unix": [ "libwatcher-c.a" ], diff --git a/config/pkg.json b/config/pkg.json index 00e83595..d3b4fb90 100644 --- a/config/pkg.json +++ b/config/pkg.json @@ -23,8 +23,8 @@ "type": "url", "url": "https://dl.static-php.dev/static-php-cli/deps/nasm/nasm-2.16.01-win64.zip", "extract-files": { - "nasm-2.16.01/nasm.exe": "{php_sdk_path}/bin/nasm.exe", - "nasm-2.16.01/ndisasm.exe": "{php_sdk_path}/bin/ndisasm.exe" + "nasm.exe": "{php_sdk_path}/bin/nasm.exe", + "ndisasm.exe": "{php_sdk_path}/bin/ndisasm.exe" } }, "pkg-config-aarch64-linux": { @@ -84,7 +84,7 @@ "repo": "upx/upx", "match": "upx.+-win64\\.zip", "extract-files": { - "upx-*-win64/upx.exe": "{pkg_root_path}/bin/upx.exe" + "upx.exe": "{pkg_root_path}/bin/upx.exe" } }, "zig-aarch64-linux": { diff --git a/config/source.json b/config/source.json index f020ae06..77e5a5cf 100644 --- a/config/source.json +++ b/config/source.json @@ -47,7 +47,7 @@ "provide-pre-built": true, "license": { "type": "file", - "path": "doc/COPYING" + "path": "doc/COPYING.LGPL" } }, "brotli": { @@ -263,6 +263,15 @@ "path": "LICENSE" } }, + "ext-zip": { + "type": "url", + "url": "https://pecl.php.net/get/zip", + "filename": "ext-zip.tgz", + "license": { + "type": "file", + "path": "LICENSE" + } + }, "ext-zstd": { "type": "git", "path": "php-src/ext/zstd", @@ -297,16 +306,17 @@ "regex": "/href=\"(?gettext-(?[^\"]+)\\.tar\\.xz)\"/", "license": { "type": "file", - "path": "COPYING" + "path": "gettext-runtime/intl/COPYING.LIB" } }, "gmp": { - "type": "url", - "url": "https://dl.static-php.dev/static-php-cli/deps/gmp/gmp-6.3.0.tar.xz", + "type": "filelist", + "url": "https://gmplib.org/download/gmp/", + "regex": "/href=\"(?gmp-(?[^\"]+)\\.tar\\.xz)\"/", "provide-pre-built": true, "alt": { - "type": "ghtagtar", - "repo": "alisw/GMP" + "type": "url", + "url": "https://dl.static-php.dev/static-php-cli/deps/gmp/gmp-6.3.0.tar.xz" }, "license": { "type": "text", @@ -324,7 +334,7 @@ }, "grpc": { "type": "git", - "rev": "v1.68.x", + "rev": "v1.75.x", "url": "https://github.com/grpc/grpc.git", "provide-pre-built": true, "license": { @@ -440,7 +450,7 @@ "provide-pre-built": true, "license": { "type": "file", - "path": "doc/COPYING" + "path": "doc/COPYING.LGPL" } }, "libaom": { @@ -499,6 +509,16 @@ "path": "COPYING" } }, + "libedit": { + "type": "filelist", + "url": "https://thrysoee.dk/editline/", + "regex": "/href=\"(?libedit-(?[^\"]+)\\.tar\\.gz)\"/", + "provide-pre-built": true, + "license": { + "type": "file", + "path": "COPYING" + } + }, "libevent": { "type": "ghrel", "repo": "libevent/libevent", @@ -547,7 +567,7 @@ "provide-pre-built": true, "license": { "type": "file", - "path": "COPYING" + "path": "COPYING.LIB" } }, "libiconv-win": { @@ -660,6 +680,15 @@ "path": "LICENSE.md" } }, + "liburing": { + "type": "ghtar", + "repo": "axboe/liburing", + "prefer-stable": true, + "license": { + "type": "file", + "path": "COPYING" + } + }, "libuuid": { "type": "git", "url": "https://github.com/static-php/libuuid.git", @@ -890,7 +919,7 @@ "postgresql": { "type": "ghtagtar", "repo": "postgres/postgres", - "match": "REL_16_\\d+", + "match": "REL_18_\\d+", "license": { "type": "file", "path": "COPYRIGHT" @@ -981,7 +1010,6 @@ }, "snappy": { "type": "git", - "repo": "google/snappy", "rev": "main", "url": "https://github.com/google/snappy", "license": { @@ -990,9 +1018,8 @@ } }, "spx": { - "type": "git", - "rev": "master", - "url": "https://github.com/static-php/php-spx.git", + "type": "pie", + "repo": "noisebynorthwest/php-spx", "path": "php-src/ext/spx", "license": { "type": "file", @@ -1146,14 +1173,5 @@ "type": "file", "path": "LICENSE" } - }, - "liburing": { - "type": "ghtar", - "repo": "axboe/liburing", - "prefer-stable": true, - "license": { - "type": "file", - "path": "COPYING" - } } } diff --git a/docs/en/develop/source-module.md b/docs/en/develop/source-module.md index 4b1b1fc0..51c3ba3c 100644 --- a/docs/en/develop/source-module.md +++ b/docs/en/develop/source-module.md @@ -36,6 +36,7 @@ The following is the source download configuration corresponding to the `libeven The most important field here is `type`. Currently, the types it supports are: - `url`: Directly use URL to download, for example: `https://download.libsodium.org/libsodium/releases/libsodium-1.0.18.tar.gz`. +- `pie`: Download PHP extensions from Packagist using the PIE (PHP Installer for Extensions) standard. - `ghrel`: Use the GitHub Release API to download, download the artifacts uploaded from the latest version released by maintainers. - `ghtar`: Use the GitHub Release API to download. Different from `ghrel`, `ghtar` is downloaded from the `source code (tar.gz)` in the latest Release of the project. @@ -89,6 +90,37 @@ Example (download the imagick extension and extract it to the extension storage } ``` +## Download type - pie + +PIE (PHP Installer for Extensions) type sources refer to downloading PHP extensions from Packagist that follow the PIE standard. +This method automatically fetches extension information from the Packagist repository and downloads the appropriate distribution file. + +The parameters included are: + +- `repo`: The Packagist vendor/package name, such as `vendor/package-name` + +Example (download a PHP extension from Packagist using PIE): + +```json +{ + "ext-example": { + "type": "pie", + "repo": "vendor/example-extension", + "path": "php-src/ext/example", + "license": { + "type": "file", + "path": "LICENSE" + } + } +} +``` + +::: tip +The PIE download type will automatically detect the extension information from Packagist metadata, +including the download URL, version, and distribution type. +The extension must be marked as `type: php-ext` or contain `php-ext` metadata in its Packagist package definition. +::: + ## Download type - ghrel ghrel will download files from Assets uploaded in GitHub Release. diff --git a/docs/en/guide/index.md b/docs/en/guide/index.md index 253562b5..54e7840c 100644 --- a/docs/en/guide/index.md +++ b/docs/en/guide/index.md @@ -21,19 +21,30 @@ The following is the architecture support situation, where :gear: represents sup | Windows | :gear: :computer: | | | FreeBSD | :computer: | :computer: | -Among them, Linux is currently only tested on Ubuntu, Debian, and Alpine distributions, -and other distributions have not been tested, which cannot guarantee successful compilation. -For untested distributions, local compilation can be done using methods such as Docker to avoid environmental issues. +Current supported PHP versions for compilation: -There are two architectures for macOS: `x86_64` and `Arm`, but binaries compiled on one architecture cannot be directly used on the other architecture. -Rosetta 2 cannot guarantee that programs compiled with `Arm` architecture can fully run on `x86_64` environment. +> :warning: Partial support, there may be issues with new beta versions and old versions. +> +> :heavy_check_mark: Supported +> +> :x: Not supported -Windows currently only supports the x86_64 architecture, and does not support 32-bit x86 or arm64 architecture. +| PHP Version | Status | Comment | +|-------------|--------------------|-------------------------------------------------------------------------------------------------------------------------| +| 7.2 | :x: | | +| 7.3 | :x: | phpmicro and many extensions do not support 7.3, 7.4 versions | +| 7.4 | :x: | phpmicro and many extensions do not support 7.3, 7.4 versions | +| 8.0 | :warning: | PHP official has stopped maintaining 8.0, we no longer handle 8.0 related backport support | +| 8.1 | :warning: | PHP official only provides security updates for 8.1, we no longer handle 8.1 related backport support after 8.5 release | +| 8.2 | :heavy_check_mark: | | +| 8.3 | :heavy_check_mark: | | +| 8.4 | :heavy_check_mark: | | +| 8.5 (beta) | :warning: | PHP 8.5 is currently in beta stage | -## Supported PHP Version +> This table shows the support status of static-php-cli for building corresponding versions, not the PHP official support status for that version. -Currently, static php cli supports PHP versions 8.1 to 8.5, and theoretically supports PHP 8.0 and earlier versions. -Simply select the earlier version when downloading. -However, due to some extensions and special components that have stopped supporting earlier versions of PHP, -static-php-cli will not explicitly support earlier versions. +## PHP Support Versions + +Currently, static-php-cli supports PHP versions 8.2 ~ 8.5, and theoretically supports PHP 8.1 and earlier versions, just select the earlier version when downloading. +However, due to some extensions and special components that have stopped supporting earlier versions of PHP, static-php-cli will not explicitly support earlier versions. We recommend that you compile the latest PHP version possible for a better experience. diff --git a/docs/zh/develop/source-module.md b/docs/zh/develop/source-module.md index 00feb3c4..769ffa08 100644 --- a/docs/zh/develop/source-module.md +++ b/docs/zh/develop/source-module.md @@ -30,6 +30,7 @@ static-php-cli 的下载资源模块是一个主要的功能,它包含了所 这里最主要的字段是 `type`,目前它支持的类型有: - `url`: 直接使用 URL 下载,例如:`https://download.libsodium.org/libsodium/releases/libsodium-1.0.18.tar.gz`。 +- `pie`: 使用 PIE(PHP Installer for Extensions)标准从 Packagist 下载 PHP 扩展。 - `ghrel`: 使用 GitHub Release API 下载,即从 GitHub 项目发布的最新版本中上传的附件下载。 - `ghtar`: 使用 GitHub Release API 下载,与 `ghrel` 不同的是,`ghtar` 是从项目的最新 Release 中找 `source code (tar.gz)` 下载的。 - `ghtagtar`: 使用 GitHub Release API 下载,与 `ghtar` 相比,`ghtagtar` 可以从 `tags` 列表找最新的,并下载 `tar.gz` 格式的源码(因为有些项目只使用了 `tag` 发布版本)。 @@ -77,6 +78,36 @@ url 类型的资源指的是从 URL 直接下载文件。 } ``` +## 下载类型 - pie + +PIE(PHP Installer for Extensions)类型的资源是从 Packagist 下载遵循 PIE 标准的 PHP 扩展。 +该方法会自动从 Packagist 仓库获取扩展信息,并下载相应的分发文件。 + +包含的参数有: + +- `repo`: Packagist 的 vendor/package 名称,如 `vendor/package-name` + +例子(使用 PIE 从 Packagist 下载 PHP 扩展): + +```json +{ + "ext-example": { + "type": "pie", + "repo": "vendor/example-extension", + "path": "php-src/ext/example", + "license": { + "type": "file", + "path": "LICENSE" + } + } +} +``` + +::: tip +PIE 下载类型会自动从 Packagist 元数据中检测扩展信息,包括下载 URL、版本和分发类型。 +扩展必须在其 Packagist 包定义中标记为 `type: php-ext` 或包含 `php-ext` 元数据。 +::: + ## 下载类型 - ghrel ghrel 会从 GitHub Release 中上传的 Assets 下载文件。首先使用 GitHub Release API 获取最新版本,然后根据正则匹配方式下载相应的文件。 diff --git a/docs/zh/guide/index.md b/docs/zh/guide/index.md index 318c40b8..8e5727c2 100644 --- a/docs/zh/guide/index.md +++ b/docs/zh/guide/index.md @@ -19,16 +19,30 @@ static-php-cli 是一个用于构建静态编译的 PHP 二进制的工具,目 | Windows | :gear: :computer: | | | FreeBSD | :computer: | :computer: | -其中,Linux 目前仅在 Ubuntu、Debian、Alpine 发行版测试通过,其他发行版未进行测试,不能保证编译成功。 -对于未经过测试的发行版,可以使用 Docker 等方式本地编译,避免环境导致的问题。 +当前支持编译的 PHP 版本: -macOS 下支持 x86_64 和 Arm 两种架构,但在其中一个架构上编译的二进制无法直接在另一个架构上使用。 -Rosetta 2 不能保证 Arm 架构编译的程序可以完全运行在 x86_64 环境下。 +> :warning: 部分支持,对于新的测试版和旧版本可能存在问题。 +> +> :heavy_check_mark: 支持 +> +> :x: 不支持 -Windows 目前只支持 x86_64 架构,不支持 32 位 x86、不支持 arm64 架构。 +| PHP Version | Status | Comment | +|-------------|--------------------|---------------------------------------------------------| +| 7.2 | :x: | | +| 7.3 | :x: | phpmicro 和许多扩展不支持 7.3、7.4 版本 | +| 7.4 | :x: | phpmicro 和许多扩展不支持 7.3、7.4 版本 | +| 8.0 | :warning: | PHP 官方已停止 8.0 的维护,我们不再处理 8.0 相关的 backport 支持 | +| 8.1 | :warning: | PHP 官方仅对 8.1 提供安全更新,在 8.5 发布后我们不再处理 8.1 相关的 backport 支持 | +| 8.2 | :heavy_check_mark: | | +| 8.3 | :heavy_check_mark: | | +| 8.4 | :heavy_check_mark: | | +| 8.5 (beta) | :warning: | PHP 8.5 目前处于 beta 阶段 | + +> 这个表格的支持状态是 static-php-cli 对构建对应版本的支持情况,不是 PHP 官方对该版本的支持情况。 ## PHP 支持版本 -目前,static-php-cli 对 PHP 8.1 ~ 8.5 版本是支持的,对于 PHP 8.0 及更早版本理论上支持,只需下载时选择早期版本即可。 +目前,static-php-cli 对 PHP 8.2 ~ 8.5 版本是支持的,对于 PHP 8.1 及更早版本理论上支持,只需下载时选择早期版本即可。 但由于部分扩展和特殊组件已对早期版本的 PHP 停止了支持,所以 static-php-cli 不会明确支持早期版本。 我们推荐你编译尽可能新的 PHP 版本,以获得更好的体验。 diff --git a/src/SPC/ConsoleApplication.php b/src/SPC/ConsoleApplication.php index c60ca4b0..ba2d38e0 100644 --- a/src/SPC/ConsoleApplication.php +++ b/src/SPC/ConsoleApplication.php @@ -34,7 +34,7 @@ use Symfony\Component\Console\Application; */ final class ConsoleApplication extends Application { - public const string VERSION = '2.7.3'; + public const string VERSION = '2.7.5'; public function __construct() { diff --git a/src/SPC/builder/BuilderBase.php b/src/SPC/builder/BuilderBase.php index b410aab3..fdba936d 100644 --- a/src/SPC/builder/BuilderBase.php +++ b/src/SPC/builder/BuilderBase.php @@ -10,6 +10,7 @@ use SPC\exception\WrongUsageException; use SPC\store\Config; use SPC\store\FileSystem; use SPC\store\LockFile; +use SPC\store\pkg\GoXcaddy; use SPC\store\SourceManager; use SPC\store\SourcePatcher; use SPC\util\AttributeMapper; @@ -127,27 +128,6 @@ abstract class BuilderBase return array_filter($this->exts, fn ($ext) => $ext->isBuildStatic()); } - /** - * Check if there is a cpp extensions or libraries. - */ - public function hasCpp(): bool - { - // judge cpp-extension - $exts = array_keys($this->getExts(false)); - foreach ($exts as $ext) { - if (Config::getExt($ext, 'cpp-extension', false) === true) { - return true; - } - } - $libs = array_keys($this->getLibs()); - foreach ($libs as $lib) { - if (Config::getLib($lib, 'cpp-library', false) === true) { - return true; - } - } - return false; - } - /** * Set libs only mode. * @@ -507,8 +487,7 @@ abstract class BuilderBase throw new WrongUsageException('FrankenPHP SAPI is only available on Linux and macOS!'); } // frankenphp needs package go-xcaddy installed - $pkg_dir = PKG_ROOT_PATH . '/go-xcaddy-' . arch2gnu(php_uname('m')) . '-' . osfamily2shortname(); - if (!file_exists("{$pkg_dir}/bin/go") || !file_exists("{$pkg_dir}/bin/xcaddy")) { + if (!GoXcaddy::isInstalled()) { global $argv; throw new WrongUsageException("FrankenPHP SAPI requires the go-xcaddy package, please install it first: {$argv[0]} install-pkg go-xcaddy"); } diff --git a/src/SPC/builder/Extension.php b/src/SPC/builder/Extension.php index 71bbf9d4..5b6a3329 100644 --- a/src/SPC/builder/Extension.php +++ b/src/SPC/builder/Extension.php @@ -10,8 +10,6 @@ use SPC\exception\ValidationException; use SPC\exception\WrongUsageException; use SPC\store\Config; use SPC\store\FileSystem; -use SPC\toolchain\ToolchainManager; -use SPC\toolchain\ZigToolchain; use SPC\util\SPCConfigUtil; use SPC\util\SPCTarget; @@ -223,12 +221,24 @@ class Extension public function patchBeforeSharedMake(): bool { $config = (new SPCConfigUtil($this->builder))->config([$this->getName()], array_map(fn ($l) => $l->getName(), $this->builder->getLibs())); - [$staticLibs] = $this->splitLibsIntoStaticAndShared($config['libs']); - FileSystem::replaceFileRegex( - $this->source_dir . '/Makefile', - '/^(.*_SHARED_LIBADD\s*=.*)$/m', - '$1 ' . trim($staticLibs) - ); + [$staticLibs, $sharedLibs] = $this->splitLibsIntoStaticAndShared($config['libs']); + $lstdcpp = str_contains($sharedLibs, '-l:libstdc++.a') ? '-l:libstdc++.a' : null; + $lstdcpp ??= str_contains($sharedLibs, '-lstdc++') ? '-lstdc++' : ''; + + $makefileContent = file_get_contents($this->source_dir . '/Makefile'); + if (preg_match('/^(.*_SHARED_LIBADD\s*=\s*)(.*)$/m', $makefileContent, $matches)) { + $prefix = $matches[1]; + $currentLibs = trim($matches[2]); + $newLibs = trim("{$currentLibs} {$staticLibs} {$lstdcpp}"); + $deduplicatedLibs = deduplicate_flags($newLibs); + + FileSystem::replaceFileRegex( + $this->source_dir . '/Makefile', + '/^(.*_SHARED_LIBADD\s*=.*)$/m', + $prefix . $deduplicatedLibs + ); + } + if ($objs = getenv('SPC_EXTRA_RUNTIME_OBJECTS')) { FileSystem::replaceFileRegex( $this->source_dir . '/Makefile', @@ -295,7 +305,7 @@ class Extension // Run compile check if build target is cli // If you need to run some check, overwrite this or add your assert in src/globals/ext-tests/{extension_name}.php $sharedExtensions = $this->getSharedExtensionLoadString(); - [$ret, $out] = shell()->execWithResult(BUILD_BIN_PATH . '/php -n' . $sharedExtensions . ' --ri "' . $this->getDistName() . '"'); + [$ret] = shell()->execWithResult(BUILD_BIN_PATH . '/php -n' . $sharedExtensions . ' --ri "' . $this->getDistName() . '"'); if ($ret !== 0) { throw new ValidationException( "extension {$this->getName()} failed compile check: php-cli returned {$ret}", @@ -325,7 +335,7 @@ class Extension { // Run compile check if build target is cli // If you need to run some check, overwrite this or add your assert in src/globals/ext-tests/{extension_name}.php - [$ret] = cmd()->execWithResult(BUILD_ROOT_PATH . '/bin/php.exe -n --ri "' . $this->getDistName() . '"', false); + [$ret] = cmd()->execWithResult(BUILD_BIN_PATH . '/php.exe -n --ri "' . $this->getDistName() . '"', false); if ($ret !== 0) { throw new ValidationException("extension {$this->getName()} failed compile check: php-cli returned {$ret}", validation_module: "Extension {$this->getName()} sanity check"); } @@ -402,26 +412,7 @@ class Extension */ public function buildUnixShared(): void { - $config = (new SPCConfigUtil($this->builder))->config( - [$this->getName()], - array_map(fn ($l) => $l->getName(), $this->getLibraryDependencies(recursive: true)), - $this->builder->getOption('with-suggested-exts'), - $this->builder->getOption('with-suggested-libs'), - ); - [$staticLibs, $sharedLibs] = $this->splitLibsIntoStaticAndShared($config['libs']); - $preStatic = PHP_OS_FAMILY === 'Darwin' ? '' : '-Wl,--start-group '; - $postStatic = PHP_OS_FAMILY === 'Darwin' ? '' : ' -Wl,--end-group '; - $env = [ - 'CFLAGS' => $config['cflags'], - 'CXXFLAGS' => $config['cflags'], - 'LDFLAGS' => $config['ldflags'], - 'LIBS' => clean_spaces("{$preStatic} {$staticLibs} {$postStatic} {$sharedLibs}"), - 'LD_LIBRARY_PATH' => BUILD_LIB_PATH, - ]; - if (ToolchainManager::getToolchainClass() === ZigToolchain::class && SPCTarget::getTargetOS() === 'Linux') { - $env['SPC_COMPILER_EXTRA'] = '-lstdc++'; - } - + $env = $this->getSharedExtensionEnv(); if ($this->patchBeforeSharedPhpize()) { logger()->info("Extension [{$this->getName()}] patched before shared phpize"); } @@ -436,13 +427,15 @@ class Extension logger()->info("Extension [{$this->getName()}] patched before shared configure"); } + $phpvars = getenv('SPC_EXTRA_PHP_VARS') ?: ''; + shell()->cd($this->source_dir) ->setEnv($env) ->appendEnv($this->getExtraEnv()) ->exec( './configure ' . $this->getUnixConfigureArg(true) . ' --with-php-config=' . BUILD_BIN_PATH . '/php-config ' . - '--enable-shared --disable-static' + "--enable-shared --disable-static {$phpvars}" ); if ($this->patchBeforeSharedMake()) { @@ -493,6 +486,30 @@ class Extension return $this->build_static; } + /** + * Returns the environment variables a shared extension needs to be built. + * CFLAGS, CXXFLAGS, LDFLAGS and so on. + */ + protected function getSharedExtensionEnv(): array + { + $config = (new SPCConfigUtil($this->builder))->config( + [$this->getName()], + array_map(fn ($l) => $l->getName(), $this->getLibraryDependencies(recursive: true)), + $this->builder->getOption('with-suggested-exts'), + $this->builder->getOption('with-suggested-libs'), + ); + [$staticLibs, $sharedLibs] = $this->splitLibsIntoStaticAndShared($config['libs']); + $preStatic = PHP_OS_FAMILY === 'Darwin' ? '' : '-Wl,--start-group '; + $postStatic = PHP_OS_FAMILY === 'Darwin' ? '' : ' -Wl,--end-group '; + return [ + 'CFLAGS' => $config['cflags'], + 'CXXFLAGS' => $config['cflags'], + 'LDFLAGS' => $config['ldflags'], + 'LIBS' => clean_spaces("{$preStatic} {$staticLibs} {$postStatic} {$sharedLibs}"), + 'LD_LIBRARY_PATH' => BUILD_LIB_PATH, + ]; + } + protected function addLibraryDependency(string $name, bool $optional = false): void { $depLib = $this->builder->getLib($name); @@ -565,12 +582,12 @@ class Extension $added = 0; foreach ($ret as $depName => $dep) { foreach ($dep->getDependencies(true) as $depdepName => $depdep) { - if (!in_array($depdepName, array_keys($deps), true)) { + if (!array_key_exists($depdepName, $deps)) { $deps[$depdepName] = $depdep; ++$added; } } - if (!in_array($depName, array_keys($deps), true)) { + if (!array_key_exists($depName, $deps)) { $deps[$depName] = $dep; } } diff --git a/src/SPC/builder/extension/dba.php b/src/SPC/builder/extension/dba.php index abda5651..d1f8cda3 100644 --- a/src/SPC/builder/extension/dba.php +++ b/src/SPC/builder/extension/dba.php @@ -12,7 +12,7 @@ class dba extends Extension { public function getUnixConfigureArg(bool $shared = false): string { - $qdbm = $this->builder->getLib('qdbm') ? (' --with-qdbm=' . ($shared ? 'shared,' : '') . BUILD_ROOT_PATH) : ''; + $qdbm = $this->builder->getLib('qdbm') ? (' --with-qdbm=' . BUILD_ROOT_PATH) : ''; return '--enable-dba' . ($shared ? '=shared' : '') . $qdbm; } diff --git a/src/SPC/builder/extension/gettext.php b/src/SPC/builder/extension/gettext.php index dc4f58aa..303cc389 100644 --- a/src/SPC/builder/extension/gettext.php +++ b/src/SPC/builder/extension/gettext.php @@ -15,7 +15,11 @@ class gettext extends Extension public function patchBeforeBuildconf(): bool { if ($this->builder instanceof MacOSBuilder) { - FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/ext/gettext/config.m4', 'AC_CHECK_LIB($GETTEXT_CHECK_IN_LIB', 'AC_CHECK_LIB(intl'); + FileSystem::replaceFileStr( + SOURCE_PATH . '/php-src/ext/gettext/config.m4', + ['AC_CHECK_LIB($GETTEXT_CHECK_IN_LIB', 'AC_CHECK_LIB([$GETTEXT_CHECK_IN_LIB'], + ['AC_CHECK_LIB(intl', 'AC_CHECK_LIB([intl'] // new php versions use a bracket + ); } return true; } diff --git a/src/SPC/builder/extension/grpc.php b/src/SPC/builder/extension/grpc.php index 61af6c04..7388eaff 100644 --- a/src/SPC/builder/extension/grpc.php +++ b/src/SPC/builder/extension/grpc.php @@ -56,4 +56,11 @@ class grpc extends Extension GlobalEnvManager::putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS=' . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') . ' -Wno-strict-prototypes'); return true; } + + protected function getSharedExtensionEnv(): array + { + $env = parent::getSharedExtensionEnv(); + $env['CPPFLAGS'] = $env['CXXFLAGS'] . ' -Wno-attributes'; + return $env; + } } diff --git a/src/SPC/builder/extension/memcached.php b/src/SPC/builder/extension/memcached.php index a1b88ac6..983b5b64 100644 --- a/src/SPC/builder/extension/memcached.php +++ b/src/SPC/builder/extension/memcached.php @@ -17,6 +17,10 @@ class memcached extends Extension '--with-libmemcached-dir=' . BUILD_ROOT_PATH . ' ' . '--disable-memcached-sasl ' . '--enable-memcached-json ' . + ($this->builder->getLib('zstd') ? '--with-zstd ' : '') . + ($this->builder->getExt('igbinary') ? '--enable-memcached-igbinary ' : '') . + ($this->builder->getExt('session') ? '--enable-memcached-session ' : '') . + ($this->builder->getExt('msgpack') ? '--enable-memcached-msgpack ' : '') . '--with-system-fastlz'; } } diff --git a/src/SPC/builder/extension/readline.php b/src/SPC/builder/extension/readline.php index a8ee48aa..035d7b43 100644 --- a/src/SPC/builder/extension/readline.php +++ b/src/SPC/builder/extension/readline.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace SPC\builder\extension; use SPC\builder\Extension; +use SPC\exception\ValidationException; use SPC\store\FileSystem; use SPC\util\CustomExt; @@ -23,12 +24,7 @@ class readline extends Extension public function getUnixConfigureArg(bool $shared = false): string { - $enable = '--without-libedit --with-readline=' . BUILD_ROOT_PATH; - if ($this->builder->getPHPVersionID() < 84000) { - // the check uses `char rl_pending_input()` instead of `extern int rl_pending_input`, which makes LTO fail - $enable .= ' ac_cv_lib_readline_rl_pending_input=yes'; - } - return $enable; + return '--with-libedit --without-readline'; } public function buildUnixShared(): void @@ -39,4 +35,13 @@ class readline extends Extension } parent::buildUnixShared(); } + + public function runCliCheckUnix(): void + { + parent::runCliCheckUnix(); + [$ret, $out] = shell()->execWithResult('printf "exit\n" | ' . BUILD_BIN_PATH . '/php -a'); + if ($ret !== 0 || !str_contains(implode("\n", $out), 'Interactive shell')) { + throw new ValidationException("readline extension failed sanity check. Code: {$ret}, output: " . implode("\n", $out)); + } + } } diff --git a/src/SPC/builder/extension/simdjson.php b/src/SPC/builder/extension/simdjson.php index 71796ebe..914fd674 100644 --- a/src/SPC/builder/extension/simdjson.php +++ b/src/SPC/builder/extension/simdjson.php @@ -6,6 +6,8 @@ namespace SPC\builder\extension; use SPC\builder\Extension; use SPC\store\FileSystem; +use SPC\toolchain\ToolchainManager; +use SPC\toolchain\ZigToolchain; use SPC\util\CustomExt; #[CustomExt('simdjson')] @@ -17,7 +19,7 @@ class simdjson extends Extension FileSystem::replaceFileRegex( SOURCE_PATH . '/php-src/ext/simdjson/config.m4', '/php_version=(`.*`)$/m', - 'php_version=' . strval($php_ver) + 'php_version=' . $php_ver ); FileSystem::replaceFileStr( SOURCE_PATH . '/php-src/ext/simdjson/config.m4', @@ -31,4 +33,18 @@ class simdjson extends Extension ); return true; } + + public function getSharedExtensionEnv(): array + { + $env = parent::getSharedExtensionEnv(); + if (ToolchainManager::getToolchainClass() === ZigToolchain::class) { + $extra = getenv('SPC_COMPILER_EXTRA'); + if (!str_contains((string) $extra, '-lstdc++')) { + f_putenv('SPC_COMPILER_EXTRA=' . clean_spaces($extra . ' -lstdc++')); + } + $env['CFLAGS'] .= ' -Xclang -target-feature -Xclang +evex512'; + $env['CXXFLAGS'] .= ' -Xclang -target-feature -Xclang +evex512'; + } + return $env; + } } diff --git a/src/SPC/builder/extension/spx.php b/src/SPC/builder/extension/spx.php index f5e736d5..a5a8ad71 100644 --- a/src/SPC/builder/extension/spx.php +++ b/src/SPC/builder/extension/spx.php @@ -13,7 +13,7 @@ class spx extends Extension { public function getUnixConfigureArg(bool $shared = false): string { - $arg = '--enable-spx' . ($shared ? '=shared' : ''); + $arg = '--enable-SPX' . ($shared ? '=shared' : ''); if ($this->builder->getLib('zlib') !== null) { $arg .= ' --with-zlib-dir=' . BUILD_ROOT_PATH; } @@ -29,4 +29,20 @@ class spx extends Extension ); return true; } + + public function patchBeforeBuildconf(): bool + { + FileSystem::replaceFileStr( + $this->source_dir . '/config.m4', + 'CFLAGS="$CFLAGS -Werror -Wall -O3 -pthread -std=gnu90"', + 'CFLAGS="$CFLAGS -pthread"' + ); + FileSystem::replaceFileStr( + $this->source_dir . '/src/php_spx.h', + "extern zend_module_entry spx_module_entry;\n", + "extern zend_module_entry spx_module_entry;;\n#define phpext_spx_ptr &spx_module_entry\n" + ); + FileSystem::copy($this->source_dir . '/src/php_spx.h', $this->source_dir . '/php_spx.h'); + return true; + } } diff --git a/src/SPC/builder/extension/swoole.php b/src/SPC/builder/extension/swoole.php index a4a531cf..7ff4b331 100644 --- a/src/SPC/builder/extension/swoole.php +++ b/src/SPC/builder/extension/swoole.php @@ -69,12 +69,15 @@ class swoole extends Extension $arg .= $this->builder->getExt('swoole-hook-pgsql') ? ' --enable-swoole-pgsql' : ' --disable-swoole-pgsql'; $arg .= $this->builder->getExt('swoole-hook-mysql') ? ' --enable-mysqlnd' : ' --disable-mysqlnd'; $arg .= $this->builder->getExt('swoole-hook-sqlite') ? ' --enable-swoole-sqlite' : ' --disable-swoole-sqlite'; - if ($this->builder->getExt('swoole-hook-odbc')) { $config = (new SPCConfigUtil($this->builder, ['libs_only_deps' => true]))->config([], ['unixodbc']); $arg .= ' --with-swoole-odbc=unixODBC,' . BUILD_ROOT_PATH . ' SWOOLE_ODBC_LIBS="' . $config['libs'] . '"'; } + if ($this->getExtVersion() >= '6.1.0') { + $arg .= ' --enable-swoole-stdext'; + } + if (SPCTarget::getTargetOS() === 'Darwin') { $arg .= ' ac_cv_lib_pthread_pthread_barrier_init=no'; } diff --git a/src/SPC/builder/extension/zip.php b/src/SPC/builder/extension/zip.php new file mode 100644 index 00000000..eab5e40a --- /dev/null +++ b/src/SPC/builder/extension/zip.php @@ -0,0 +1,17 @@ + SPCTarget::getRuntimeLibs(), // do not pass static libraries here yet, they may contain polyfills for libc functions! ]); + $phpvars = getenv('SPC_EXTRA_PHP_VARS') ?: ''; + $embed_type = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static'; if ($embed_type !== 'static' && SPCTarget::isStatic()) { throw new WrongUsageException( @@ -103,22 +106,22 @@ class LinuxBuilder extends UnixBuilderBase ); } - shell()->cd(SOURCE_PATH . '/php-src') - ->exec( - $php_configure_env . ' ' . - getenv('SPC_CMD_PREFIX_PHP_CONFIGURE') . ' ' . - ($enableCli ? '--enable-cli ' : '--disable-cli ') . - ($enableFpm ? '--enable-fpm ' . ($this->getLib('libacl') !== null ? '--with-fpm-acl ' : '') : '--disable-fpm ') . - ($enableEmbed ? "--enable-embed={$embed_type} " : '--disable-embed ') . - ($enableMicro ? '--enable-micro=all-static ' : '--disable-micro ') . - ($enableCgi ? '--enable-cgi ' : '--disable-cgi ') . - $config_file_path . - $config_file_scan_dir . - $json_74 . - $zts . - $maxExecutionTimers . - $this->makeStaticExtensionArgs() . ' ' - ); + $this->seekPhpSrcLogFileOnException(fn () => shell()->cd(SOURCE_PATH . '/php-src')->exec( + $php_configure_env . ' ' . + getenv('SPC_CMD_PREFIX_PHP_CONFIGURE') . ' ' . + ($enableCli ? '--enable-cli ' : '--disable-cli ') . + ($enableFpm ? '--enable-fpm ' . ($this->getLib('libacl') !== null ? '--with-fpm-acl ' : '') : '--disable-fpm ') . + ($enableEmbed ? "--enable-embed={$embed_type} " : '--disable-embed ') . + ($enableMicro ? '--enable-micro=all-static ' : '--disable-micro ') . + ($enableCgi ? '--enable-cgi ' : '--disable-cgi ') . + $config_file_path . + $config_file_scan_dir . + $json_74 . + $zts . + $maxExecutionTimers . + $phpvars . ' ' . + $this->makeStaticExtensionArgs() . ' ' + )); $this->emitPatchPoint('before-php-make'); SourcePatcher::patchBeforeMake($this); @@ -148,16 +151,22 @@ class LinuxBuilder extends UnixBuilderBase } $this->buildEmbed(); } - // build dynamic extensions if needed, must happen before building FrankenPHP to make sure we export all necessary, undefined symbols - $shared_extensions = array_map('trim', array_filter(explode(',', $this->getOption('build-shared')))); - if (!empty($shared_extensions)) { - logger()->info('Building shared extensions ...'); - $this->buildSharedExts(); - } if ($enableFrankenphp) { logger()->info('building frankenphp'); $this->buildFrankenphp(); } + $shared_extensions = array_map('trim', array_filter(explode(',', $this->getOption('build-shared')))); + if (!empty($shared_extensions)) { + if (SPCTarget::isStatic()) { + throw new WrongUsageException( + "You're building against musl libc statically (the default on Linux), but you're trying to build shared extensions.\n" . + 'Static musl libc does not implement `dlopen`, so your php binary is not able to load shared extensions.' . "\n" . + 'Either use SPC_LIBC=glibc to link against glibc on a glibc OS, or use SPC_TARGET="native-native-musl -dynamic" to link against musl libc dynamically using `zig cc`.' + ); + } + logger()->info('Building shared extensions...'); + $this->buildSharedExts(); + } } public function testPHP(int $build_target = BUILD_TARGET_NONE) @@ -171,11 +180,19 @@ class LinuxBuilder extends UnixBuilderBase */ protected function buildCli(): void { + if ($this->getExt('readline') && SPCTarget::isStatic()) { + SourcePatcher::patchFile('musl_static_readline.patch', SOURCE_PATH . '/php-src'); + } + $vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars()); - $SPC_CMD_PREFIX_PHP_MAKE = getenv('SPC_CMD_PREFIX_PHP_MAKE') ?: 'make'; + $concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : ''; shell()->cd(SOURCE_PATH . '/php-src') ->exec('sed -i "s|//lib|/lib|g" Makefile') - ->exec("{$SPC_CMD_PREFIX_PHP_MAKE} {$vars} cli"); + ->exec("make {$concurrency} {$vars} cli"); + + if ($this->getExt('readline') && SPCTarget::isStatic()) { + SourcePatcher::patchFile('musl_static_readline.patch', SOURCE_PATH . '/php-src', true); + } if (!$this->getOption('no-strip', false)) { shell()->cd(SOURCE_PATH . '/php-src/sapi/cli')->exec('strip --strip-unneeded php'); @@ -191,10 +208,10 @@ class LinuxBuilder extends UnixBuilderBase protected function buildCgi(): void { $vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars()); - $SPC_CMD_PREFIX_PHP_MAKE = getenv('SPC_CMD_PREFIX_PHP_MAKE') ?: 'make'; + $concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : ''; shell()->cd(SOURCE_PATH . '/php-src') ->exec('sed -i "s|//lib|/lib|g" Makefile') - ->exec("{$SPC_CMD_PREFIX_PHP_MAKE} {$vars} cgi"); + ->exec("make {$concurrency} {$vars} cgi"); if (!$this->getOption('no-strip', false)) { shell()->cd(SOURCE_PATH . '/php-src/sapi/cgi')->exec('strip --strip-unneeded php-cgi'); @@ -226,11 +243,11 @@ class LinuxBuilder extends UnixBuilderBase // patch fake cli for micro $vars['EXTRA_CFLAGS'] .= $enable_fake_cli; $vars = SystemUtil::makeEnvVarString($vars); - $SPC_CMD_PREFIX_PHP_MAKE = getenv('SPC_CMD_PREFIX_PHP_MAKE') ?: 'make'; + $concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : ''; shell()->cd(SOURCE_PATH . '/php-src') ->exec('sed -i "s|//lib|/lib|g" Makefile') - ->exec("{$SPC_CMD_PREFIX_PHP_MAKE} {$vars} micro"); + ->exec("make {$concurrency} {$vars} micro"); $this->processMicroUPX(); @@ -247,10 +264,10 @@ class LinuxBuilder extends UnixBuilderBase protected function buildFpm(): void { $vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars()); - $SPC_CMD_PREFIX_PHP_MAKE = getenv('SPC_CMD_PREFIX_PHP_MAKE') ?: 'make'; + $concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : ''; shell()->cd(SOURCE_PATH . '/php-src') ->exec('sed -i "s|//lib|/lib|g" Makefile') - ->exec("{$SPC_CMD_PREFIX_PHP_MAKE} {$vars} fpm"); + ->exec("make {$concurrency} {$vars} fpm"); if (!$this->getOption('no-strip', false)) { shell()->cd(SOURCE_PATH . '/php-src/sapi/fpm')->exec('strip --strip-unneeded php-fpm'); @@ -267,12 +284,17 @@ class LinuxBuilder extends UnixBuilderBase */ protected function buildEmbed(): void { + $sharedExts = array_filter($this->exts, static fn ($ext) => $ext->isBuildShared()); + $sharedExts = array_filter($sharedExts, static function ($ext) { + return Config::getExt($ext->getName(), 'build-with-php') === true; + }); + $install_modules = $sharedExts ? 'install-modules' : ''; $vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars()); - + $concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : ''; shell()->cd(SOURCE_PATH . '/php-src') ->exec('sed -i "s|//lib|/lib|g" Makefile') ->exec('sed -i "s|^EXTENSION_DIR = .*|EXTENSION_DIR = /' . basename(BUILD_MODULES_PATH) . '|" Makefile') - ->exec(getenv('SPC_CMD_PREFIX_PHP_MAKE') . ' INSTALL_ROOT=' . BUILD_ROOT_PATH . " {$vars} install"); + ->exec("make {$concurrency} INSTALL_ROOT=" . BUILD_ROOT_PATH . " {$vars} install-sapi {$install_modules} install-build install-headers install-programs"); $ldflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS') ?: ''; $libDir = BUILD_LIB_PATH; @@ -327,7 +349,7 @@ class LinuxBuilder extends UnixBuilderBase $target = "{$libDir}/{$realLibName}"; if (file_exists($target)) { [, $output] = shell()->execWithResult('readelf -d ' . escapeshellarg($target)); - $output = join("\n", $output); + $output = implode("\n", $output); if (preg_match('/SONAME.*\[(.+)]/', $output, $sonameMatch)) { $currentSoname = $sonameMatch[1]; if ($currentSoname !== basename($target)) { @@ -361,12 +383,12 @@ class LinuxBuilder extends UnixBuilderBase $config = (new SPCConfigUtil($this, ['libs_only_deps' => true, 'absolute_libs' => true]))->config($this->ext_list, $this->lib_list, $this->getOption('with-suggested-exts'), $this->getOption('with-suggested-libs')); $static = SPCTarget::isStatic() ? '-all-static' : ''; $lib = BUILD_LIB_PATH; - return [ + return array_filter([ 'EXTRA_CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), 'EXTRA_LIBS' => $config['libs'], 'EXTRA_LDFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), 'EXTRA_LDFLAGS_PROGRAM' => "-L{$lib} {$static} -pie", - ]; + ]); } /** diff --git a/src/SPC/builder/linux/library/libedit.php b/src/SPC/builder/linux/library/libedit.php new file mode 100644 index 00000000..ef869212 --- /dev/null +++ b/src/SPC/builder/linux/library/libedit.php @@ -0,0 +1,12 @@ +cd(SOURCE_PATH . '/php-src') - ->exec( - getenv('SPC_CMD_PREFIX_PHP_CONFIGURE') . ' ' . + $this->seekPhpSrcLogFileOnException(fn () => shell()->cd(SOURCE_PATH . '/php-src')->exec( + getenv('SPC_CMD_PREFIX_PHP_CONFIGURE') . ' ' . ($enableCli ? '--enable-cli ' : '--disable-cli ') . ($enableFpm ? '--enable-fpm ' : '--disable-fpm ') . ($enableEmbed ? "--enable-embed={$embed_type} " : '--disable-embed ') . @@ -132,7 +132,7 @@ class MacOSBuilder extends UnixBuilderBase $zts . $this->makeStaticExtensionArgs() . ' ' . $envs_build_php - ); + )); $this->emitPatchPoint('before-php-make'); SourcePatcher::patchBeforeMake($this); @@ -162,15 +162,15 @@ class MacOSBuilder extends UnixBuilderBase } $this->buildEmbed(); } + if ($enableFrankenphp) { + logger()->info('building frankenphp'); + $this->buildFrankenphp(); + } $shared_extensions = array_map('trim', array_filter(explode(',', $this->getOption('build-shared')))); if (!empty($shared_extensions)) { logger()->info('Building shared extensions ...'); $this->buildSharedExts(); } - if ($enableFrankenphp) { - logger()->info('building frankenphp'); - $this->buildFrankenphp(); - } } public function testPHP(int $build_target = BUILD_TARGET_NONE) @@ -187,8 +187,8 @@ class MacOSBuilder extends UnixBuilderBase $vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars()); $shell = shell()->cd(SOURCE_PATH . '/php-src'); - $SPC_CMD_PREFIX_PHP_MAKE = getenv('SPC_CMD_PREFIX_PHP_MAKE') ?: 'make'; - $shell->exec("{$SPC_CMD_PREFIX_PHP_MAKE} {$vars} cli"); + $concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : ''; + $shell->exec("make {$concurrency} {$vars} cli"); if (!$this->getOption('no-strip', false)) { $shell->exec('dsymutil -f sapi/cli/php')->exec('strip -S sapi/cli/php'); } @@ -200,8 +200,8 @@ class MacOSBuilder extends UnixBuilderBase $vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars()); $shell = shell()->cd(SOURCE_PATH . '/php-src'); - $SPC_CMD_PREFIX_PHP_MAKE = getenv('SPC_CMD_PREFIX_PHP_MAKE') ?: 'make'; - $shell->exec("{$SPC_CMD_PREFIX_PHP_MAKE} {$vars} cgi"); + $concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : ''; + $shell->exec("make {$concurrency} {$vars} cgi"); if (!$this->getOption('no-strip', false)) { $shell->exec('dsymutil -f sapi/cgi/php-cgi')->exec('strip -S sapi/cgi/php-cgi'); } @@ -230,7 +230,8 @@ class MacOSBuilder extends UnixBuilderBase $shell = shell()->cd(SOURCE_PATH . '/php-src'); // build - $shell->exec(getenv('SPC_CMD_PREFIX_PHP_MAKE') . " {$vars} micro"); + $concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : ''; + $shell->exec("make {$concurrency} {$vars} micro"); // strip if (!$this->getOption('no-strip', false)) { $shell->exec('dsymutil -f sapi/micro/micro.sfx')->exec('strip -S sapi/micro/micro.sfx'); @@ -251,7 +252,8 @@ class MacOSBuilder extends UnixBuilderBase $vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars()); $shell = shell()->cd(SOURCE_PATH . '/php-src'); - $shell->exec(getenv('SPC_CMD_PREFIX_PHP_MAKE') . " {$vars} fpm"); + $concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : ''; + $shell->exec("make {$concurrency} {$vars} fpm"); if (!$this->getOption('no-strip', false)) { $shell->exec('dsymutil -f sapi/fpm/php-fpm')->exec('strip -S sapi/fpm/php-fpm'); } @@ -263,10 +265,15 @@ class MacOSBuilder extends UnixBuilderBase */ protected function buildEmbed(): void { + $sharedExts = array_filter($this->exts, static fn ($ext) => $ext->isBuildShared()); + $sharedExts = array_filter($sharedExts, static function ($ext) { + return Config::getExt($ext->getName(), 'build-with-php') === true; + }); + $install_modules = $sharedExts ? 'install-modules' : ''; $vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars()); - + $concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : ''; shell()->cd(SOURCE_PATH . '/php-src') - ->exec(getenv('SPC_CMD_PREFIX_PHP_MAKE') . ' INSTALL_ROOT=' . BUILD_ROOT_PATH . " {$vars} install"); + ->exec("make {$concurrency} INSTALL_ROOT=" . BUILD_ROOT_PATH . " {$vars} install-sapi {$install_modules} install-build install-headers install-programs"); if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'static') { $AR = getenv('AR') ?: 'ar'; @@ -280,10 +287,10 @@ class MacOSBuilder extends UnixBuilderBase private function getMakeExtraVars(): array { $config = (new SPCConfigUtil($this, ['libs_only_deps' => true]))->config($this->ext_list, $this->lib_list, $this->getOption('with-suggested-exts'), $this->getOption('with-suggested-libs')); - return [ + return array_filter([ 'EXTRA_CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), 'EXTRA_LDFLAGS_PROGRAM' => '-L' . BUILD_LIB_PATH, 'EXTRA_LIBS' => $config['libs'], - ]; + ]); } } diff --git a/src/SPC/builder/macos/library/libedit.php b/src/SPC/builder/macos/library/libedit.php new file mode 100644 index 00000000..18d2c3d1 --- /dev/null +++ b/src/SPC/builder/macos/library/libedit.php @@ -0,0 +1,12 @@ +processFrankenphpApp(); $nobrotli = $this->getLib('brotli') === null ? ',nobrotli' : ''; $nowatcher = $this->getLib('watcher') === null ? ',nowatcher' : ''; @@ -339,11 +340,11 @@ abstract class UnixBuilderBase extends BuilderBase } } $debugFlags = $this->getOption('no-strip') ? '-w -s ' : ''; - $extLdFlags = "-extldflags '-pie{$dynamic_exports}'"; + $extLdFlags = "-extldflags '-pie{$dynamic_exports} {$this->arch_ld_flags}'"; $muslTags = ''; $staticFlags = ''; if (SPCTarget::isStatic()) { - $extLdFlags = "-extldflags '-static-pie -Wl,-z,stack-size=0x80000{$dynamic_exports}'"; + $extLdFlags = "-extldflags '-static-pie -Wl,-z,stack-size=0x80000{$dynamic_exports} {$this->arch_ld_flags}'"; $muslTags = 'static_build,'; $staticFlags = '-static-pie'; } @@ -351,7 +352,6 @@ abstract class UnixBuilderBase extends BuilderBase $config = (new SPCConfigUtil($this))->config($this->ext_list, $this->lib_list); $cflags = "{$this->arch_c_flags} {$config['cflags']} " . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'); $libs = $config['libs']; - $libs .= PHP_OS_FAMILY === 'Linux' ? ' -lrt' : ''; // Go's gcc driver doesn't automatically link against -lgcov or -lrt. Ugly, but necessary fix. if ((str_contains((string) getenv('SPC_DEFAULT_C_FLAGS'), '-fprofile') || str_contains((string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), '-fprofile')) && @@ -359,7 +359,7 @@ abstract class UnixBuilderBase extends BuilderBase $cflags .= ' -Wno-error=missing-profile'; $libs .= ' -lgcov'; } - $env = [ + $env = [...[ 'CGO_ENABLED' => '1', 'CGO_CFLAGS' => clean_spaces($cflags), 'CGO_LDFLAGS' => "{$this->arch_ld_flags} {$staticFlags} {$config['ldflags']} {$libs}", @@ -369,12 +369,7 @@ abstract class UnixBuilderBase extends BuilderBase "v{$frankenPhpVersion} PHP {$libphpVersion} Caddy'\\\" " . "-tags={$muslTags}nobadger,nomysql,nopgx{$nobrotli}{$nowatcher}", 'LD_LIBRARY_PATH' => BUILD_LIB_PATH, - ]; - foreach (GoXcaddy::getEnvironment() as $key => $value) { - if ($key !== 'PATH') { - $env[$key] = $value; - } - } + ], ...GoXcaddy::getEnvironment()]; shell()->cd(BUILD_BIN_PATH) ->setEnv($env) ->exec("xcaddy build --output frankenphp {$xcaddyModules}"); @@ -387,4 +382,20 @@ abstract class UnixBuilderBase extends BuilderBase } } } + + /** + * Seek php-src/config.log when building PHP, add it to exception. + */ + protected function seekPhpSrcLogFileOnException(callable $callback): void + { + try { + $callback(); + } catch (SPCException $e) { + if (file_exists(SOURCE_PATH . '/php-src/config.log')) { + $e->addExtraLogFile('php-src config.log', 'php-src.config.log'); + copy(SOURCE_PATH . '/php-src/config.log', SPC_LOGS_DIR . '/php-src.config.log'); + } + throw $e; + } + } } diff --git a/src/SPC/builder/unix/library/attr.php b/src/SPC/builder/unix/library/attr.php index b33cf987..67ac5feb 100644 --- a/src/SPC/builder/unix/library/attr.php +++ b/src/SPC/builder/unix/library/attr.php @@ -17,7 +17,7 @@ trait attr ->exec('libtoolize --force --copy') ->exec('./autogen.sh || autoreconf -if') ->configure('--disable-nls') - ->make(); + ->make('install-attributes_h install-data install-libattr_h install-libLTLIBRARIES install-pkgincludeHEADERS install-pkgconfDATA', with_install: false); $this->patchPkgconfPrefix(['libattr.pc'], PKGCONF_PATCH_PREFIX); } } diff --git a/src/SPC/builder/unix/library/gettext.php b/src/SPC/builder/unix/library/gettext.php index f0fdae54..332e25c9 100644 --- a/src/SPC/builder/unix/library/gettext.php +++ b/src/SPC/builder/unix/library/gettext.php @@ -31,7 +31,7 @@ trait gettext $autoconf->addConfigureArgs('--disable-threads'); } - $autoconf->configure()->make(); + $autoconf->configure()->make(dir: $this->getSourceDir() . '/gettext-runtime/intl'); $this->patchLaDependencyPrefix(); } } diff --git a/src/SPC/builder/unix/library/imagemagick.php b/src/SPC/builder/unix/library/imagemagick.php index 2d312549..42064e2d 100644 --- a/src/SPC/builder/unix/library/imagemagick.php +++ b/src/SPC/builder/unix/library/imagemagick.php @@ -12,6 +12,11 @@ trait imagemagick { protected function build(): void { + $original_ldflags = $this->builder->arch_ld_flags; + if (str_contains($this->builder->arch_ld_flags, '-Wl,--as-needed')) { + $this->builder->arch_ld_flags = str_replace('-Wl,--as-needed', '', $original_ldflags); + } + $ac = UnixAutoconfExecutor::create($this) ->optionalLib('libzip', ...ac_with_args('zip')) ->optionalLib('libjpeg', ...ac_with_args('jpeg')) @@ -32,7 +37,7 @@ trait imagemagick ); // special: linux-static target needs `-static` - $ldflags = SPCTarget::isStatic() ? ('-static -ldl') : '-ldl'; + $ldflags = SPCTarget::isStatic() ? '-static -ldl' : '-ldl'; // special: macOS needs -iconv $libs = SPCTarget::getTargetOS() === 'Darwin' ? '-liconv' : ''; @@ -45,6 +50,8 @@ trait imagemagick $ac->configure()->make(); + $this->builder->arch_ld_flags = $original_ldflags; + $filelist = [ 'ImageMagick.pc', 'ImageMagick-7.Q16HDRI.pc', diff --git a/src/SPC/builder/unix/library/libacl.php b/src/SPC/builder/unix/library/libacl.php index 223e09fc..8c5d699d 100644 --- a/src/SPC/builder/unix/library/libacl.php +++ b/src/SPC/builder/unix/library/libacl.php @@ -26,7 +26,7 @@ trait libacl ->exec('libtoolize --force --copy') ->exec('./autogen.sh || autoreconf -if') ->configure('--disable-nls', '--disable-tests') - ->make(); + ->make('install-acl_h install-libacl_h install-data install-libLTLIBRARIES install-pkgincludeHEADERS install-sysincludeHEADERS install-pkgconfDATA', with_install: false); $this->patchPkgconfPrefix(['libacl.pc'], PKGCONF_PATCH_PREFIX); } } diff --git a/src/SPC/builder/unix/library/libedit.php b/src/SPC/builder/unix/library/libedit.php new file mode 100644 index 00000000..2de9077f --- /dev/null +++ b/src/SPC/builder/unix/library/libedit.php @@ -0,0 +1,30 @@ +source_dir . '/src/sys.h', + '|//#define\s+strl|', + '#define strl' + ); + return true; + } + + protected function build(): void + { + UnixAutoconfExecutor::create($this) + ->appendEnv(['CFLAGS' => '-D__STDC_ISO_10646__=201103L']) + ->configure() + ->make(); + $this->patchPkgconfPrefix(['libedit.pc']); + } +} diff --git a/src/SPC/builder/unix/library/libiconv.php b/src/SPC/builder/unix/library/libiconv.php index fd68e309..5ccc9484 100644 --- a/src/SPC/builder/unix/library/libiconv.php +++ b/src/SPC/builder/unix/library/libiconv.php @@ -10,7 +10,13 @@ trait libiconv { protected function build(): void { - UnixAutoconfExecutor::create($this)->configure('--enable-extra-encodings')->make(); + UnixAutoconfExecutor::create($this) + ->configure( + '--enable-extra-encodings', + '--enable-year2038', + ) + ->make('install-lib', with_install: false) + ->make('install-lib', with_install: false, dir: $this->getSourceDir() . '/libcharset'); $this->patchLaDependencyPrefix(); } } diff --git a/src/SPC/builder/unix/library/libxml2.php b/src/SPC/builder/unix/library/libxml2.php index 87fbab10..7152c0c4 100644 --- a/src/SPC/builder/unix/library/libxml2.php +++ b/src/SPC/builder/unix/library/libxml2.php @@ -20,17 +20,17 @@ trait libxml2 "-DZLIB_INCLUDE_DIR={$this->getIncludeDir()}", '-DLIBXML2_WITH_ZLIB=OFF', ) - ->optionalLib('icu', ...cmake_boolean_args('LIBXML2_WITH_ICU')) ->optionalLib('xz', ...cmake_boolean_args('LIBXML2_WITH_LZMA')) ->addConfigureArgs( '-DLIBXML2_WITH_ICONV=ON', + '-DLIBXML2_WITH_ICU=OFF', // optional, but discouraged: https://gitlab.gnome.org/GNOME/libxml2/-/blob/master/README.md '-DLIBXML2_WITH_PYTHON=OFF', '-DLIBXML2_WITH_PROGRAMS=OFF', '-DLIBXML2_WITH_TESTS=OFF', ); if ($this instanceof LinuxLibraryBase) { - $cmake->addConfigureArgs('-DIconv_IS_BUILD_IN=OFF'); + $cmake->addConfigureArgs('-DIconv_IS_BUILT_IN=OFF'); } $cmake->build(); diff --git a/src/SPC/builder/unix/library/libzip.php b/src/SPC/builder/unix/library/libzip.php index 931bfcc3..ad0befea 100644 --- a/src/SPC/builder/unix/library/libzip.php +++ b/src/SPC/builder/unix/library/libzip.php @@ -22,6 +22,7 @@ trait libzip '-DBUILD_EXAMPLES=OFF', '-DBUILD_REGRESS=OFF', '-DBUILD_TOOLS=OFF', + '-DBUILD_OSSFUZZ=OFF', ) ->build(); $this->patchPkgconfPrefix(['libzip.pc'], PKGCONF_PATCH_PREFIX); diff --git a/src/SPC/builder/unix/library/ncurses.php b/src/SPC/builder/unix/library/ncurses.php index 3899e559..27725c3d 100644 --- a/src/SPC/builder/unix/library/ncurses.php +++ b/src/SPC/builder/unix/library/ncurses.php @@ -38,7 +38,7 @@ trait ncurses ->make(); $final = FileSystem::scanDirFiles(BUILD_BIN_PATH, relative: true); // Remove the new files - $new_files = array_diff($final, $filelist); + $new_files = array_diff($final, $filelist ?: []); foreach ($new_files as $file) { @unlink(BUILD_BIN_PATH . '/' . $file); } diff --git a/src/SPC/builder/unix/library/postgresql.php b/src/SPC/builder/unix/library/postgresql.php index 3bc6835d..67935d14 100644 --- a/src/SPC/builder/unix/library/postgresql.php +++ b/src/SPC/builder/unix/library/postgresql.php @@ -4,93 +4,83 @@ declare(strict_types=1); namespace SPC\builder\unix\library; -use SPC\builder\linux\library\LinuxLibraryBase; -use SPC\exception\BuildFailureException; use SPC\exception\FileSystemException; use SPC\store\FileSystem; +use SPC\util\PkgConfigUtil; +use SPC\util\SPCConfigUtil; use SPC\util\SPCTarget; trait postgresql { + public function patchBeforeBuild(): bool + { + // fix aarch64 build on glibc 2.17 (e.g. CentOS 7) + if (SPCTarget::getLibcVersion() === '2.17' && GNU_ARCH === 'aarch64') { + try { + FileSystem::replaceFileStr("{$this->source_dir}/src/port/pg_popcount_aarch64.c", 'HWCAP_SVE', '0'); + FileSystem::replaceFileStr( + "{$this->source_dir}/src/port/pg_crc32c_armv8_choose.c", + '#if defined(__linux__) && !defined(__aarch64__) && !defined(HWCAP2_CRC32)', + '#if defined(__linux__) && !defined(HWCAP_CRC32)' + ); + } catch (FileSystemException) { + // allow file not-existence to make it compatible with old and new version + } + } + // skip the test on platforms where libpq infrastructure may be provided by statically-linked libraries + FileSystem::replaceFileStr("{$this->source_dir}/src/interfaces/libpq/Makefile", 'invokes exit\'; exit 1;', 'invokes exit\';'); + // disable shared libs build + FileSystem::replaceFileStr( + "{$this->source_dir}/src/Makefile.shlib", + [ + '$(LINK.shared) -o $@ $(OBJS) $(LDFLAGS) $(LDFLAGS_SL) $(SHLIB_LINK)', + '$(INSTALL_SHLIB) $< \'$(DESTDIR)$(pkglibdir)/$(shlib)\'', + '$(INSTALL_SHLIB) $< \'$(DESTDIR)$(libdir)/$(shlib)\'', + '$(INSTALL_SHLIB) $< \'$(DESTDIR)$(bindir)/$(shlib)\'', + ], + '' + ); + return true; + } + protected function build(): void { - $builddir = BUILD_ROOT_PATH; - $envs = ''; - $packages = 'zlib openssl readline libxml-2.0'; - $optional_packages = [ - 'zstd' => 'libzstd', - 'ldap' => 'ldap', - 'libxslt' => 'libxslt', - 'icu' => 'icu-i18n', + $libs = array_map(fn ($x) => $x->getName(), $this->getDependencies()); + $spc = new SPCConfigUtil($this->getBuilder(), ['no_php' => true, 'libs_only_deps' => true]); + $config = $spc->config(libraries: $libs, include_suggest_lib: $this->builder->getOption('with-suggested-libs')); + + $env_vars = [ + 'CFLAGS' => $config['cflags'], + 'CPPFLAGS' => '-DPIC', + 'LDFLAGS' => $config['ldflags'], + 'LIBS' => $config['libs'], ]; - $error_exec_cnt = 0; - foreach ($optional_packages as $lib => $pkg) { - if ($this->getBuilder()->getLib($lib)) { - $packages .= ' ' . $pkg; - $output = shell()->execWithResult("pkg-config --static {$pkg}"); - $error_exec_cnt += $output[0] === 0 ? 0 : 1; - logger()->info(var_export($output[1], true)); - } - } - - $output = shell()->execWithResult("pkg-config --cflags-only-I --static {$packages}"); - $error_exec_cnt += $output[0] === 0 ? 0 : 1; - $macos_15_bug_cflags = PHP_OS_FAMILY === 'Darwin' ? ' -Wno-unguarded-availability-new' : ''; - $cflags = ''; - if (!empty($output[1][0])) { - $cflags = $output[1][0]; - $envs .= ' CPPFLAGS="-DPIC"'; - $cflags = "{$cflags} -fno-ident{$macos_15_bug_cflags}"; - } - $output = shell()->execWithResult("pkg-config --libs-only-L --static {$packages}"); - $error_exec_cnt += $output[0] === 0 ? 0 : 1; - if (!empty($output[1][0])) { - $ldflags = $output[1][0]; - $envs .= SPCTarget::isStatic() ? " LDFLAGS=\"{$ldflags} -static\" " : " LDFLAGS=\"{$ldflags}\" "; - } - $output = shell()->execWithResult("pkg-config --libs-only-l --static {$packages}"); - $error_exec_cnt += $output[0] === 0 ? 0 : 1; - if (!empty($output[1][0])) { - $libs = $output[1][0]; - $libcpp = ''; - if ($this->builder->getLib('icu')) { - $libcpp = $this instanceof LinuxLibraryBase ? ' -lstdc++' : ' -lc++'; - } - $envs .= " LIBS=\"{$libs}{$libcpp}\" "; - } - if ($error_exec_cnt > 0) { - throw new BuildFailureException('Failed to get pkg-config information!'); + if ($ldLibraryPath = getenv('SPC_LD_LIBRARY_PATH')) { + $env_vars['LD_LIBRARY_PATH'] = $ldLibraryPath; } FileSystem::resetDir($this->source_dir . '/build'); - $version = $this->getVersion(); - // 16.1 workaround - if (version_compare($version, '16.1') >= 0) { - # 有静态链接配置 参考文件: src/interfaces/libpq/Makefile - shell()->cd($this->source_dir . '/build') - ->exec('sed -i.backup "s/invokes exit\'; exit 1;/invokes exit\';/" ../src/interfaces/libpq/Makefile') - ->exec('sed -i.backup "278 s/^/# /" ../src/Makefile.shlib') - ->exec('sed -i.backup "402 s/^/# /" ../src/Makefile.shlib'); - } else { - throw new BuildFailureException('Unsupported version for postgresql: ' . $version . ' !'); - } + // php source relies on the non-private encoding functions in libpgcommon.a + FileSystem::replaceFileStr( + "{$this->source_dir}/src/common/Makefile", + '$(OBJS_FRONTEND): CPPFLAGS += -DUSE_PRIVATE_ENCODING_FUNCS', + '$(OBJS_FRONTEND): CPPFLAGS += -UUSE_PRIVATE_ENCODING_FUNCS -DFRONTEND', + ); // configure - shell()->cd($this->source_dir . '/build')->initializeEnv($this) - ->appendEnv(['CFLAGS' => $cflags]) + $shell = shell()->cd("{$this->source_dir}/build")->initializeEnv($this) + ->appendEnv($env_vars) ->exec( - "{$envs} ../configure " . - "--prefix={$builddir} " . - ($this->builder->getOption('enable-zts') ? '--enable-thread-safety ' : '--disable-thread-safety ') . + '../configure ' . + "--prefix={$this->getBuildRootPath()} " . '--enable-coverage=no ' . '--with-ssl=openssl ' . '--with-readline ' . '--with-libxml ' . ($this->builder->getLib('icu') ? '--with-icu ' : '--without-icu ') . ($this->builder->getLib('ldap') ? '--with-ldap ' : '--without-ldap ') . - // '--without-ldap ' . ($this->builder->getLib('libxslt') ? '--with-libxslt ' : '--without-libxslt ') . ($this->builder->getLib('zstd') ? '--with-zstd ' : '--without-zstd ') . '--without-lz4 ' . @@ -99,32 +89,29 @@ trait postgresql '--without-pam ' . '--without-bonjour ' . '--without-tcl ' - ) - ->exec($envs . ' make -C src/bin/pg_config install') - ->exec($envs . ' make -C src/include install') - ->exec($envs . ' make -C src/common install') - ->exec($envs . ' make -C src/port install') - ->exec($envs . ' make -C src/interfaces/libpq install'); + ); + + // patch ldap lib + if ($this->builder->getLib('ldap')) { + $libs = PkgConfigUtil::getLibsArray('ldap'); + $libs = clean_spaces(implode(' ', $libs)); + FileSystem::replaceFileStr($this->source_dir . '/build/config.status', '-lldap', $libs); + FileSystem::replaceFileStr($this->source_dir . '/build/src/Makefile.global', '-lldap', $libs); + } + + $shell + ->exec('make -C src/bin/pg_config install') + ->exec('make -C src/include install') + ->exec('make -C src/common install') + ->exec('make -C src/port install') + ->exec('make -C src/interfaces/libpq install'); // remove dynamic libs shell()->cd($this->source_dir . '/build') - ->exec("rm -rf {$builddir}/lib/*.so.*") - ->exec("rm -rf {$builddir}/lib/*.so") - ->exec("rm -rf {$builddir}/lib/*.dylib"); + ->exec("rm -rf {$this->getBuildRootPath()}/lib/*.so.*") + ->exec("rm -rf {$this->getBuildRootPath()}/lib/*.so") + ->exec("rm -rf {$this->getBuildRootPath()}/lib/*.dylib"); - FileSystem::replaceFileStr(BUILD_LIB_PATH . '/pkgconfig/libpq.pc', '-lldap', '-lldap -llber'); - } - - private function getVersion(): string - { - try { - $file = FileSystem::readFile($this->source_dir . '/meson.build'); - if (preg_match("/^\\s+version:\\s?'(.*)'/m", $file, $match)) { - return $match[1]; - } - return 'unknown'; - } catch (FileSystemException) { - return 'unknown'; - } + FileSystem::replaceFileStr("{$this->getLibDir()}/pkgconfig/libpq.pc", '-lldap', '-lldap -llber'); } } diff --git a/src/SPC/command/BuildPHPCommand.php b/src/SPC/command/BuildPHPCommand.php index f8ab384b..c5c60d26 100644 --- a/src/SPC/command/BuildPHPCommand.php +++ b/src/SPC/command/BuildPHPCommand.php @@ -223,11 +223,9 @@ class BuildPHPCommand extends BuildCommand // ---------- When using bin/spc-alpine-docker, the build root path is different from the host system ---------- $build_root_path = BUILD_ROOT_PATH; - $cwd = getcwd(); $fixed = ''; + $build_root_path = get_display_path($build_root_path); if (!empty(getenv('SPC_FIX_DEPLOY_ROOT'))) { - str_replace($cwd, '', $build_root_path); - $build_root_path = getenv('SPC_FIX_DEPLOY_ROOT') . '/' . basename($build_root_path); $fixed = ' (host system)'; } if (($rule & BUILD_TARGET_CLI) === BUILD_TARGET_CLI) { diff --git a/src/SPC/command/CraftCommand.php b/src/SPC/command/CraftCommand.php index 27b3c38f..6c40133d 100644 --- a/src/SPC/command/CraftCommand.php +++ b/src/SPC/command/CraftCommand.php @@ -5,6 +5,8 @@ declare(strict_types=1); namespace SPC\command; use SPC\exception\ValidationException; +use SPC\store\pkg\GoXcaddy; +use SPC\store\pkg\Zig; use SPC\toolchain\ToolchainManager; use SPC\toolchain\ZigToolchain; use SPC\util\ConfigValidator; @@ -63,7 +65,7 @@ class CraftCommand extends BuildCommand } } // install go and xcaddy for frankenphp - if (in_array('frankenphp', $craft['sapi'])) { + if (in_array('frankenphp', $craft['sapi']) && !GoXcaddy::isInstalled()) { $retcode = $this->runCommand('install-pkg', 'go-xcaddy'); if ($retcode !== 0) { $this->output->writeln('craft go-xcaddy failed'); @@ -71,7 +73,7 @@ class CraftCommand extends BuildCommand } } // install zig if requested - if (ToolchainManager::getToolchainClass() === ZigToolchain::class) { + if (ToolchainManager::getToolchainClass() === ZigToolchain::class && !Zig::isInstalled()) { $retcode = $this->runCommand('install-pkg', 'zig'); if ($retcode !== 0) { $this->output->writeln('craft zig failed'); diff --git a/src/SPC/command/DownloadCommand.php b/src/SPC/command/DownloadCommand.php index b9bae6a5..00bcc194 100644 --- a/src/SPC/command/DownloadCommand.php +++ b/src/SPC/command/DownloadCommand.php @@ -106,7 +106,7 @@ class DownloadCommand extends BaseCommand } // retry - $retry = intval($this->getOption('retry')); + $retry = (int) $this->getOption('retry'); f_putenv('SPC_DOWNLOAD_RETRIES=' . $retry); // Use shallow-clone can reduce git resource download @@ -265,7 +265,7 @@ class DownloadCommand extends BaseCommand f_passthru((PHP_OS_FAMILY === 'Windows' ? 'rmdir /s /q ' : 'rm -rf ') . DOWNLOAD_PATH); } // unzip command check - if (PHP_OS_FAMILY !== 'Windows' && !$this->findCommand('unzip')) { + if (PHP_OS_FAMILY !== 'Windows' && !self::findCommand('unzip')) { $this->output->writeln('Missing unzip command, you need to install it first !'); $this->output->writeln('You can use "bin/spc doctor" command to check and install required tools'); return static::FAILURE; diff --git a/src/SPC/doctor/item/LinuxToolCheckList.php b/src/SPC/doctor/item/LinuxToolCheckList.php index c3611dcc..a23ad6d5 100644 --- a/src/SPC/doctor/item/LinuxToolCheckList.php +++ b/src/SPC/doctor/item/LinuxToolCheckList.php @@ -28,7 +28,7 @@ class LinuxToolCheckList public const TOOLS_DEBIAN = [ 'make', 'bison', 're2c', 'flex', 'git', 'autoconf', 'automake', 'autopoint', - 'tar', 'unzip', 'gzip', + 'tar', 'unzip', 'gzip', 'gcc', 'g++', 'bzip2', 'cmake', 'patch', 'xz', 'libtoolize', 'which', 'patchelf', @@ -37,7 +37,7 @@ class LinuxToolCheckList public const TOOLS_RHEL = [ 'perl', 'make', 'bison', 're2c', 'flex', 'git', 'autoconf', 'automake', - 'tar', 'unzip', 'gzip', 'gcc', + 'tar', 'unzip', 'gzip', 'gcc', 'g++', 'bzip2', 'cmake', 'patch', 'which', 'xz', 'libtool', 'gettext-devel', 'patchelf', @@ -53,7 +53,8 @@ class LinuxToolCheckList 'base-devel' => 'automake', 'gettext-devel' => 'gettextize', 'gettext-dev' => 'gettextize', - 'perl-IPC-Cmd' => '/usr/share/doc/perl-IPC-Cmd', + 'perl-IPC-Cmd' => '/usr/share/perl5/vendor_perl/IPC/Cmd.pm', + 'perl-Time-Piece' => '/usr/lib64/perl5/Time/Piece.pm', ]; /** @noinspection PhpUnused */ @@ -65,7 +66,7 @@ class LinuxToolCheckList $required = match ($distro['dist']) { 'alpine' => self::TOOLS_ALPINE, 'redhat' => self::TOOLS_RHEL, - 'centos' => array_merge(self::TOOLS_RHEL, ['perl-IPC-Cmd']), + 'centos' => array_merge(self::TOOLS_RHEL, ['perl-IPC-Cmd', 'perl-Time-Piece']), 'arch' => self::TOOLS_ARCH, default => self::TOOLS_DEBIAN, }; @@ -81,14 +82,14 @@ class LinuxToolCheckList return CheckResult::ok(); } - #[AsCheckItem('if cmake version >= 3.18', limit_os: 'Linux')] + #[AsCheckItem('if cmake version >= 3.22', limit_os: 'Linux')] public function checkCMakeVersion(): ?CheckResult { $ver = get_cmake_version(); if ($ver === null) { return CheckResult::fail('Failed to get cmake version'); } - if (version_compare($ver, '3.18.0') < 0) { + if (version_compare($ver, '3.22.0') < 0) { return CheckResult::fail('cmake version is too low (' . $ver . '), please update it manually!'); } return CheckResult::ok($ver); diff --git a/src/SPC/exception/ExceptionHandler.php b/src/SPC/exception/ExceptionHandler.php index 44b82b06..d784be71 100644 --- a/src/SPC/exception/ExceptionHandler.php +++ b/src/SPC/exception/ExceptionHandler.php @@ -137,13 +137,18 @@ class ExceptionHandler self::logError("\n----------------------------------------\n"); - self::logError('⚠ The ' . ConsoleColor::cyan('console output log') . ConsoleColor::red(' is saved in ') . ConsoleColor::none(SPC_OUTPUT_LOG)); + // convert log file path if in docker + $spc_log_convert = get_display_path(SPC_OUTPUT_LOG); + $shell_log_convert = get_display_path(SPC_SHELL_LOG); + $spc_logs_dir_convert = get_display_path(SPC_LOGS_DIR); + + self::logError('⚠ The ' . ConsoleColor::cyan('console output log') . ConsoleColor::red(' is saved in ') . ConsoleColor::none($spc_log_convert)); if (file_exists(SPC_SHELL_LOG)) { - self::logError('⚠ The ' . ConsoleColor::cyan('shell output log') . ConsoleColor::red(' is saved in ') . ConsoleColor::none(SPC_SHELL_LOG)); + self::logError('⚠ The ' . ConsoleColor::cyan('shell output log') . ConsoleColor::red(' is saved in ') . ConsoleColor::none($shell_log_convert)); } if ($e->getExtraLogFiles() !== []) { foreach ($e->getExtraLogFiles() as $key => $file) { - self::logError("⚠ Log file [{$key}] is saved in: " . ConsoleColor::none(SPC_LOGS_DIR . "/{$file}")); + self::logError("⚠ Log file [{$key}] is saved in: " . ConsoleColor::none("{$spc_logs_dir_convert}/{$file}")); } } if (!defined('DEBUG_MODE')) { diff --git a/src/SPC/store/Downloader.php b/src/SPC/store/Downloader.php index d659253e..63bec807 100644 --- a/src/SPC/store/Downloader.php +++ b/src/SPC/store/Downloader.php @@ -16,6 +16,42 @@ use SPC\util\SPCTarget; */ class Downloader { + /** + * Get latest version from PIE config (Packagist) + * + * @param string $name Source name + * @param array $source Source meta info: [repo] + * @return array [url, filename] + */ + public static function getPIEInfo(string $name, array $source): array + { + $packagist_url = "https://repo.packagist.org/p2/{$source['repo']}.json"; + logger()->debug("Fetching {$name} source from packagist index: {$packagist_url}"); + $data = json_decode(self::curlExec( + url: $packagist_url, + retries: self::getRetryAttempts() + ), true); + if (!isset($data['packages'][$source['repo']]) || !is_array($data['packages'][$source['repo']])) { + throw new DownloaderException("failed to find {$name} repo info from packagist"); + } + // get the first version + $first = $data['packages'][$source['repo']][0] ?? []; + // check 'type' => 'php-ext' or contains 'php-ext' key + if (!isset($first['php-ext'])) { + throw new DownloaderException("failed to find {$name} php-ext info from packagist, maybe not a php extension package"); + } + // get download link from dist + $dist_url = $first['dist']['url'] ?? null; + $dist_type = $first['dist']['type'] ?? null; + if (!$dist_url || !$dist_type) { + throw new DownloaderException("failed to find {$name} dist info from packagist"); + } + $name = str_replace('/', '_', $source['repo']); + $version = $first['version'] ?? 'unknown'; + // file name use: $name-$version.$dist_type + return [$dist_url, "{$name}-{$version}.{$dist_type}"]; + } + /** * Get latest version from BitBucket tag * @@ -65,19 +101,19 @@ class Downloader url: "https://api.github.com/repos/{$source['repo']}/{$type}", hooks: [[CurlHook::class, 'setupGithubToken']], retries: self::getRetryAttempts() - ), true); + ), true, 512, JSON_THROW_ON_ERROR); $url = null; - for ($i = 0; $i < count($data); ++$i) { - if (($data[$i]['prerelease'] ?? false) === true && ($source['prefer-stable'] ?? false)) { + foreach ($data as $rel) { + if (($rel['prerelease'] ?? false) === true && ($source['prefer-stable'] ?? false)) { continue; } if (!($source['match'] ?? null)) { - $url = $data[$i]['tarball_url'] ?? null; + $url = $rel['tarball_url'] ?? null; break; } - if (preg_match('|' . $source['match'] . '|', $data[$i]['tarball_url'])) { - $url = $data[$i]['tarball_url']; + if (preg_match('|' . $source['match'] . '|', $rel['tarball_url'])) { + $url = $rel['tarball_url']; break; } } @@ -232,7 +268,8 @@ class Downloader $quiet = !defined('DEBUG_MODE') ? '-q --quiet' : ''; $git = SPC_GIT_EXEC; $shallow = defined('GIT_SHALLOW_CLONE') ? '--depth 1 --single-branch' : ''; - $recursive = ($submodules === null) ? '--recursive' : ''; + $recursive = ($submodules === null && defined('GIT_SHALLOW_CLONE')) ? '--recursive --shallow-submodules' : null; + $recursive ??= $submodules === null ? '--recursive' : ''; try { self::registerCancelEvent(function () use ($download_path) { @@ -243,8 +280,9 @@ class Downloader }); f_passthru("{$git} clone {$quiet} --config core.autocrlf=false --branch \"{$branch}\" {$shallow} {$recursive} \"{$url}\" \"{$download_path}\""); if ($submodules !== null) { + $depth_flag = defined('GIT_SHALLOW_CLONE') ? '--depth 1' : ''; foreach ($submodules as $submodule) { - f_passthru("cd \"{$download_path}\" && {$git} submodule update --init " . escapeshellarg($submodule)); + f_passthru("cd \"{$download_path}\" && {$git} submodule update --init {$depth_flag} " . escapeshellarg($submodule)); } } } catch (SPCException $e) { @@ -315,91 +353,14 @@ class Downloader if (self::isAlreadyDownloaded($name, $force, SPC_DOWNLOAD_PACKAGE)) { return; } - - try { - switch ($pkg['type']) { - case 'bitbuckettag': // BitBucket Tag - [$url, $filename] = self::getLatestBitbucketTag($name, $pkg); - self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null, SPC_DOWNLOAD_PACKAGE); - break; - case 'ghtar': // GitHub Release (tar) - [$url, $filename] = self::getLatestGithubTarball($name, $pkg); - self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null, SPC_DOWNLOAD_PACKAGE, hooks: [[CurlHook::class, 'setupGithubToken']]); - break; - case 'ghtagtar': // GitHub Tag (tar) - [$url, $filename] = self::getLatestGithubTarball($name, $pkg, 'tags'); - self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null, SPC_DOWNLOAD_PACKAGE, hooks: [[CurlHook::class, 'setupGithubToken']]); - break; - case 'ghrel': // GitHub Release (uploaded) - [$url, $filename] = self::getLatestGithubRelease($name, $pkg); - self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null, SPC_DOWNLOAD_PACKAGE, ['Accept: application/octet-stream'], [[CurlHook::class, 'setupGithubToken']]); - break; - case 'filelist': // Basic File List (regex based crawler) - [$url, $filename] = self::getFromFileList($name, $pkg); - self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null, SPC_DOWNLOAD_PACKAGE); - break; - case 'url': // Direct download URL - $url = $pkg['url']; - $filename = $pkg['filename'] ?? basename($pkg['url']); - self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null, SPC_DOWNLOAD_PACKAGE); - break; - case 'git': // Git repo - self::downloadGit( - $name, - $pkg['url'], - $pkg['rev'], - $pkg['submodules'] ?? null, - $pkg['extract'] ?? null, - self::getRetryAttempts(), - SPC_DOWNLOAD_PRE_BUILT - ); - break; - case 'local': - // Local directory, do nothing, just lock it - logger()->debug("Locking local source {$name}"); - LockFile::lockSource($name, [ - 'source_type' => SPC_SOURCE_LOCAL, - 'dirname' => $pkg['dirname'], - 'move_path' => $pkg['extract'] ?? null, - 'lock_as' => SPC_DOWNLOAD_PACKAGE, - ]); - break; - case 'custom': // Custom download method, like API-based download or other - $classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/store/pkg', 'SPC\store\pkg'); - if (isset($pkg['func']) && is_callable($pkg['func'])) { - $pkg['name'] = $name; - $pkg['func']($force, $pkg, SPC_DOWNLOAD_PACKAGE); - break; - } - foreach ($classes as $class) { - if (is_a($class, CustomPackage::class, true) && $class !== CustomPackage::class) { - $cls = new $class(); - if (in_array($name, $cls->getSupportName())) { - (new $class())->fetch($name, $force, $pkg); - break; - } - } - } - break; - default: - throw new DownloaderException('unknown source type: ' . $pkg['type']); - } - } catch (\Throwable $e) { - // Because sometimes files downloaded through the command line are not automatically deleted after a failure. - // Here we need to manually delete the file if it is detected to exist. - if (isset($filename) && file_exists(DOWNLOAD_PATH . '/' . $filename)) { - logger()->warning('Deleting download file: ' . $filename); - unlink(DOWNLOAD_PATH . '/' . $filename); - } - throw new DownloaderException('Download failed! ' . $e->getMessage()); - } + self::downloadByType($pkg['type'], $name, $pkg, $force, SPC_DOWNLOAD_PACKAGE); } /** * Download source * * @param string $name source name - * @param null|array{ + * @param null|array{ * type: string, * repo: ?string, * url: ?string, @@ -414,7 +375,7 @@ class Downloader * path: ?string, * text: ?string * } - * } $source source meta info: [type, path, rev, url, filename, regex, license] + * } $source source meta info: [type, path, rev, url, filename, regex, license] * @param bool $force Whether to force download (default: false) * @param int $download_as Lock source type (default: SPC_LOCK_SOURCE) */ @@ -437,80 +398,7 @@ class Downloader return; } - try { - switch ($source['type']) { - case 'bitbuckettag': // BitBucket Tag - [$url, $filename] = self::getLatestBitbucketTag($name, $source); - self::downloadFile($name, $url, $filename, $source['path'] ?? null, $download_as); - break; - case 'ghtar': // GitHub Release (tar) - [$url, $filename] = self::getLatestGithubTarball($name, $source); - self::downloadFile($name, $url, $filename, $source['path'] ?? null, $download_as, hooks: [[CurlHook::class, 'setupGithubToken']]); - break; - case 'ghtagtar': // GitHub Tag (tar) - [$url, $filename] = self::getLatestGithubTarball($name, $source, 'tags'); - self::downloadFile($name, $url, $filename, $source['path'] ?? null, $download_as, hooks: [[CurlHook::class, 'setupGithubToken']]); - break; - case 'ghrel': // GitHub Release (uploaded) - [$url, $filename] = self::getLatestGithubRelease($name, $source); - self::downloadFile($name, $url, $filename, $source['path'] ?? null, $download_as, ['Accept: application/octet-stream'], [[CurlHook::class, 'setupGithubToken']]); - break; - case 'filelist': // Basic File List (regex based crawler) - [$url, $filename] = self::getFromFileList($name, $source); - self::downloadFile($name, $url, $filename, $source['path'] ?? null, $download_as); - break; - case 'url': // Direct download URL - $url = $source['url']; - $filename = $source['filename'] ?? basename($source['url']); - self::downloadFile($name, $url, $filename, $source['path'] ?? null, $download_as); - break; - case 'git': // Git repo - self::downloadGit( - $name, - $source['url'], - $source['rev'], - $source['submodules'] ?? null, - $source['path'] ?? null, - self::getRetryAttempts(), - $download_as - ); - break; - case 'local': - // Local directory, do nothing, just lock it - logger()->debug("Locking local source {$name}"); - LockFile::lockSource($name, [ - 'source_type' => SPC_SOURCE_LOCAL, - 'dirname' => $source['dirname'], - 'move_path' => $source['extract'] ?? null, - 'lock_as' => $download_as, - ]); - break; - case 'custom': // Custom download method, like API-based download or other - if (isset($source['func']) && is_callable($source['func'])) { - $source['name'] = $name; - $source['func']($force, $source, $download_as); - break; - } - $classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/store/source', 'SPC\store\source'); - foreach ($classes as $class) { - if (is_a($class, CustomSourceBase::class, true) && $class::NAME === $name) { - (new $class())->fetch($force, $source, $download_as); - break; - } - } - break; - default: - throw new DownloaderException('unknown source type: ' . $source['type']); - } - } catch (\Throwable $e) { - // Because sometimes files downloaded through the command line are not automatically deleted after a failure. - // Here we need to manually delete the file if it is detected to exist. - if (isset($filename) && file_exists(DOWNLOAD_PATH . '/' . $filename)) { - logger()->warning('Deleting download file: ' . $filename); - unlink(DOWNLOAD_PATH . '/' . $filename); - } - throw new DownloaderException('Download failed! ' . $e->getMessage()); - } + self::downloadByType($source['type'], $name, $source, $force, $download_as); } /** @@ -711,4 +599,109 @@ class Downloader } return false; } + + /** + * Download by type. + * + * @param string $type Types + * @param string $name Download item name + * @param array{ + * url?: string, + * repo?: string, + * rev?: string, + * path?: string, + * filename?: string, + * dirname?: string, + * match?: string, + * prefer-stable?: bool, + * extract?: string, + * submodules?: array, + * provide-pre-built?: bool, + * func?: ?callable, + * license?: array + * } $conf Download item config + * @param bool $force Force download + * @param int $download_as Lock source type + */ + private static function downloadByType(string $type, string $name, array $conf, bool $force, int $download_as): void + { + try { + switch ($type) { + case 'pie': // Packagist + [$url, $filename] = self::getPIEInfo($name, $conf); + self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as, hooks: [[CurlHook::class, 'setupGithubToken']]); + break; + case 'bitbuckettag': // BitBucket Tag + [$url, $filename] = self::getLatestBitbucketTag($name, $conf); + self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as); + break; + case 'ghtar': // GitHub Release (tar) + [$url, $filename] = self::getLatestGithubTarball($name, $conf); + self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as, hooks: [[CurlHook::class, 'setupGithubToken']]); + break; + case 'ghtagtar': // GitHub Tag (tar) + [$url, $filename] = self::getLatestGithubTarball($name, $conf, 'tags'); + self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as, hooks: [[CurlHook::class, 'setupGithubToken']]); + break; + case 'ghrel': // GitHub Release (uploaded) + [$url, $filename] = self::getLatestGithubRelease($name, $conf); + self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as, ['Accept: application/octet-stream'], [[CurlHook::class, 'setupGithubToken']]); + break; + case 'filelist': // Basic File List (regex based crawler) + [$url, $filename] = self::getFromFileList($name, $conf); + self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as); + break; + case 'url': // Direct download URL + $url = $conf['url']; + $filename = $conf['filename'] ?? basename($conf['url']); + self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as); + break; + case 'git': // Git repo + self::downloadGit($name, $conf['url'], $conf['rev'], $conf['submodules'] ?? null, $conf['path'] ?? $conf['extract'] ?? null, self::getRetryAttempts(), $download_as); + break; + case 'local': // Local directory, do nothing, just lock it + LockFile::lockSource($name, [ + 'source_type' => SPC_SOURCE_LOCAL, + 'dirname' => $conf['dirname'], + 'move_path' => $conf['path'] ?? $conf['extract'] ?? null, + 'lock_as' => $download_as, + ]); + break; + case 'custom': // Custom download method, like API-based download or other + if (isset($conf['func'])) { + $conf['name'] = $name; + $conf['func']($force, $conf, $download_as); + break; + } + $classes = [ + ...FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/store/source', 'SPC\store\source'), + ...FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/store/pkg', 'SPC\store\pkg'), + ]; + foreach ($classes as $class) { + if (is_a($class, CustomSourceBase::class, true) && $class::NAME === $name) { + (new $class())->fetch($force, $conf, $download_as); + break; + } + if (is_a($class, CustomPackage::class, true) && $class !== CustomPackage::class) { + $cls = new $class(); + if (in_array($name, $cls->getSupportName())) { + (new $class())->fetch($name, $force, $conf); + break; + } + } + } + break; + default: + throw new DownloaderException("Unknown download type: {$type}"); + } + } catch (\Throwable $e) { + // Because sometimes files downloaded through the command line are not automatically deleted after a failure. + // Here we need to manually delete the file if it is detected to exist. + if (isset($filename) && file_exists(DOWNLOAD_PATH . '/' . $filename)) { + logger()->warning("Deleting download file: {$filename}"); + unlink(DOWNLOAD_PATH . '/' . $filename); + } + throw new DownloaderException("Download failed: {$e->getMessage()}"); + } + } } diff --git a/src/SPC/store/FileSystem.php b/src/SPC/store/FileSystem.php index a40f5f01..f6ce371e 100644 --- a/src/SPC/store/FileSystem.php +++ b/src/SPC/store/FileSystem.php @@ -274,7 +274,7 @@ class FileSystem public static function convertWinPathToMinGW(string $path): string { if (preg_match('/^[A-Za-z]:/', $path)) { - $path = '/' . strtolower(substr($path, 0, 1)) . '/' . str_replace('\\', '/', substr($path, 2)); + $path = '/' . strtolower($path[0]) . '/' . str_replace('\\', '/', substr($path, 2)); } return $path; } @@ -314,8 +314,13 @@ class FileSystem $sub_file = self::convertPath($dir . '/' . $v); if (is_dir($sub_file) && $recursive) { # 如果是 目录 且 递推 , 则递推添加下级文件 - $list = array_merge($list, self::scanDirFiles($sub_file, $recursive, $relative)); - } elseif (is_file($sub_file) || is_dir($sub_file) && !$recursive && $include_dir) { + $sub_list = self::scanDirFiles($sub_file, $recursive, $relative); + if (is_array($sub_list)) { + foreach ($sub_list as $item) { + $list[] = $item; + } + } + } elseif (is_file($sub_file) || (is_dir($sub_file) && !$recursive && $include_dir)) { # 如果是 文件 或 (是 目录 且 不递推 且 包含目录) if (is_string($relative) && mb_strpos($sub_file, $relative) === 0) { $list[] = ltrim(mb_substr($sub_file, mb_strlen($relative)), '/\\'); @@ -440,7 +445,7 @@ class FileSystem public static function writeFile(string $path, mixed $content, ...$args): bool|int|string { $dir = pathinfo(self::convertPath($path), PATHINFO_DIRNAME); - if (!is_dir($dir) && !mkdir($dir, 0755, true)) { + if (!is_dir($dir) && !mkdir($dir, 0755, true) && !is_dir($dir)) { throw new FileSystemException('Write file failed, cannot create parent directory: ' . $dir); } return file_put_contents($path, $content, ...$args); @@ -579,7 +584,7 @@ class FileSystem 'tar', 'xz', 'txz' => f_passthru("tar -xf {$filename} -C {$target} --strip-components 1"), 'tgz', 'gz' => f_passthru("tar -xzf {$filename} -C {$target} --strip-components 1"), 'bz2' => f_passthru("tar -xjf {$filename} -C {$target} --strip-components 1"), - 'zip' => f_passthru("unzip {$filename} -d {$target}"), + 'zip' => self::unzipWithStrip($filename, $target), default => throw new FileSystemException('unknown archive format: ' . $filename), }; } elseif (PHP_OS_FAMILY === 'Windows') { @@ -594,7 +599,7 @@ class FileSystem match (self::extname($filename)) { 'tar' => f_passthru("tar -xf {$filename} -C {$target} --strip-components 1"), 'xz', 'txz', 'gz', 'tgz', 'bz2' => cmd()->execWithResult("\"{$_7z}\" x -so {$filename} | tar -f - -x -C \"{$target}\" --strip-components 1"), - 'zip' => f_passthru("\"{$_7z}\" x {$filename} -o{$target} -y"), + 'zip' => self::unzipWithStrip($filename, $target), default => throw new FileSystemException("unknown archive format: {$filename}"), }; } @@ -630,7 +635,7 @@ class FileSystem private static function extractWithType(string $source_type, string $filename, string $extract_path): void { - logger()->debug('Extracting source [' . $source_type . ']: ' . $filename); + logger()->debug("Extracting source [{$source_type}]: {$filename}"); /* @phpstan-ignore-next-line */ match ($source_type) { SPC_SOURCE_ARCHIVE => self::extractArchive($filename, $extract_path), @@ -639,4 +644,107 @@ class FileSystem SPC_SOURCE_LOCAL => symlink(self::convertPath($filename), $extract_path), }; } + + /** + * Move file or directory, handling cross-device scenarios + * Uses rename() if possible, falls back to copy+delete for cross-device moves + * + * @param string $source Source path + * @param string $dest Destination path + */ + private static function moveFileOrDir(string $source, string $dest): void + { + $source = self::convertPath($source); + $dest = self::convertPath($dest); + + // Try rename first (fast, atomic) + if (@rename($source, $dest)) { + return; + } + + if (is_dir($source)) { + self::copyDir($source, $dest); + self::removeDir($source); + } else { + if (!copy($source, $dest)) { + throw new FileSystemException("Failed to copy file from {$source} to {$dest}"); + } + if (!unlink($source)) { + throw new FileSystemException("Failed to remove source file: {$source}"); + } + } + } + + /** + * Unzip file with stripping top-level directory + */ + private static function unzipWithStrip(string $zip_file, string $extract_path): void + { + $temp_dir = self::convertPath(sys_get_temp_dir() . '/spc_unzip_' . bin2hex(random_bytes(16))); + $zip_file = self::convertPath($zip_file); + $extract_path = self::convertPath($extract_path); + + // extract to temp dir + self::createDir($temp_dir); + + if (PHP_OS_FAMILY === 'Windows') { + $mute = defined('DEBUG_MODE') ? '' : ' > NUL'; + // use php-sdk-binary-tools/bin/7za.exe + $_7z = self::convertPath(getenv('PHP_SDK_PATH') . '/bin/7za.exe'); + f_passthru("\"{$_7z}\" x {$zip_file} -o{$temp_dir} -y{$mute}"); + } else { + $mute = defined('DEBUG_MODE') ? '' : ' > /dev/null'; + f_passthru("unzip \"{$zip_file}\" -d \"{$temp_dir}\"{$mute}"); + } + // scan first level dirs (relative, not recursive, include dirs) + $contents = self::scanDirFiles($temp_dir, false, true, true); + if ($contents === false) { + throw new FileSystemException('Cannot scan unzip temp dir: ' . $temp_dir); + } + // if extract path already exists, remove it + if (is_dir($extract_path)) { + self::removeDir($extract_path); + } + // if only one dir, move its contents to extract_path + $subdir = self::convertPath("{$temp_dir}/{$contents[0]}"); + if (count($contents) === 1 && is_dir($subdir)) { + self::moveFileOrDir($subdir, $extract_path); + } else { + // else, if it contains only one dir, strip dir and copy other files + $dircount = 0; + $dir = []; + $top_files = []; + foreach ($contents as $item) { + if (is_dir(self::convertPath("{$temp_dir}/{$item}"))) { + ++$dircount; + $dir[] = $item; + } else { + $top_files[] = $item; + } + } + // extract dir contents to extract_path + self::createDir($extract_path); + // extract move dir + if ($dircount === 1) { + $sub_contents = self::scanDirFiles("{$temp_dir}/{$dir[0]}", false, true, true); + if ($sub_contents === false) { + throw new FileSystemException("Cannot scan unzip temp sub-dir: {$dir[0]}"); + } + foreach ($sub_contents as $sub_item) { + self::moveFileOrDir(self::convertPath("{$temp_dir}/{$dir[0]}/{$sub_item}"), self::convertPath("{$extract_path}/{$sub_item}")); + } + } else { + foreach ($dir as $item) { + self::moveFileOrDir(self::convertPath("{$temp_dir}/{$item}"), self::convertPath("{$extract_path}/{$item}")); + } + } + // move top-level files to extract_path + foreach ($top_files as $top_file) { + self::moveFileOrDir(self::convertPath("{$temp_dir}/{$top_file}"), self::convertPath("{$extract_path}/{$top_file}")); + } + } + + // Clean up temp directory + self::removeDir($temp_dir); + } } diff --git a/src/SPC/store/LockFile.php b/src/SPC/store/LockFile.php index cab5b9da..88ecd6cb 100644 --- a/src/SPC/store/LockFile.php +++ b/src/SPC/store/LockFile.php @@ -155,8 +155,8 @@ class LockFile * @param string $name Source name * @param array{ * source_type: string, - * dirname: ?string, - * filename: ?string, + * dirname?: ?string, + * filename?: ?string, * move_path: ?string, * lock_as: int * } $data Source data diff --git a/src/SPC/store/SourcePatcher.php b/src/SPC/store/SourcePatcher.php index c08204f5..c7738e92 100644 --- a/src/SPC/store/SourcePatcher.php +++ b/src/SPC/store/SourcePatcher.php @@ -44,7 +44,7 @@ class SourcePatcher } foreach ($builder->getLibs() as $lib) { if ($lib->patchBeforeBuildconf() === true) { - logger()->info("Library [{$lib->getName()}]patched before buildconf"); + logger()->info("Library [{$lib->getName()}] patched before buildconf"); } } // patch windows php 8.1 bug diff --git a/src/SPC/store/pkg/CustomPackage.php b/src/SPC/store/pkg/CustomPackage.php index 093f65d0..eb972d74 100644 --- a/src/SPC/store/pkg/CustomPackage.php +++ b/src/SPC/store/pkg/CustomPackage.php @@ -30,10 +30,14 @@ abstract class CustomPackage /** * Get the environment variables this package needs to be usable. - * PATH needs to be appended, rather than replaced. */ abstract public static function getEnvironment(): array; + /** + * Get the PATH required to use this package. + */ + abstract public static function getPath(): ?string; + abstract public static function isInstalled(): bool; /** diff --git a/src/SPC/store/pkg/GoXcaddy.php b/src/SPC/store/pkg/GoXcaddy.php index 13041a6f..0c1c6f8c 100644 --- a/src/SPC/store/pkg/GoXcaddy.php +++ b/src/SPC/store/pkg/GoXcaddy.php @@ -13,18 +13,7 @@ class GoXcaddy extends CustomPackage { public static function isInstalled(): bool { - $arch = arch2gnu(php_uname('m')); - $os = match (PHP_OS_FAMILY) { - 'Windows' => 'win', - 'Darwin' => 'macos', - 'BSD' => 'freebsd', - default => 'linux', - }; - - $packageName = "go-xcaddy-{$arch}-{$os}"; - $pkgroot = PKG_ROOT_PATH; - $folder = "{$pkgroot}/{$packageName}"; - + $folder = PKG_ROOT_PATH . '/go-xcaddy'; return is_dir($folder) && is_file("{$folder}/bin/go") && is_file("{$folder}/bin/xcaddy"); } @@ -59,7 +48,7 @@ class GoXcaddy extends CustomPackage 'macos' => 'darwin', default => throw new \InvalidArgumentException('Unsupported OS: ' . $name), }; - $go_version = '1.24.4'; + $go_version = '1.25.0'; $config = [ 'type' => 'url', 'url' => "https://go.dev/dl/go{$go_version}.{$os}-{$arch}.tar.gz", @@ -70,15 +59,15 @@ class GoXcaddy extends CustomPackage public function extract(string $name): void { $pkgroot = PKG_ROOT_PATH; - $go_exec = "{$pkgroot}/{$name}/bin/go"; - $xcaddy_exec = "{$pkgroot}/{$name}/bin/xcaddy"; + $go_exec = "{$pkgroot}/go-xcaddy/bin/go"; + $xcaddy_exec = "{$pkgroot}/go-xcaddy/bin/xcaddy"; if (file_exists($go_exec) && file_exists($xcaddy_exec)) { return; } $lock = json_decode(FileSystem::readFile(LockFile::LOCK_FILE), true); $source_type = $lock[$name]['source_type']; $filename = DOWNLOAD_PATH . '/' . ($lock[$name]['filename'] ?? $lock[$name]['dirname']); - $extract = $lock[$name]['move_path'] ?? "{$pkgroot}/{$name}"; + $extract = $lock[$name]['move_path'] ?? "{$pkgroot}/go-xcaddy"; FileSystem::extractPackage($name, $source_type, $filename, $extract); @@ -91,9 +80,9 @@ class GoXcaddy extends CustomPackage // install xcaddy without using musl tools, xcaddy build requires dynamic linking shell() ->appendEnv([ - 'PATH' => "{$pkgroot}/{$name}/bin:" . $sanitizedPath, - 'GOROOT' => "{$pkgroot}/{$name}", - 'GOBIN' => "{$pkgroot}/{$name}/bin", + 'PATH' => "{$pkgroot}/go-xcaddy/bin:" . $sanitizedPath, + 'GOROOT' => "{$pkgroot}/go-xcaddy", + 'GOBIN' => "{$pkgroot}/go-xcaddy/bin", 'GOPATH' => "{$pkgroot}/go", ]) ->exec('CC=cc go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest'); @@ -101,22 +90,17 @@ class GoXcaddy extends CustomPackage public static function getEnvironment(): array { - $arch = arch2gnu(php_uname('m')); - $os = match (PHP_OS_FAMILY) { - 'Windows' => 'win', - 'Darwin' => 'macos', - 'BSD' => 'freebsd', - default => 'linux', - }; - - $packageName = "go-xcaddy-{$arch}-{$os}"; + $packageName = 'go-xcaddy'; $pkgroot = PKG_ROOT_PATH; - return [ - 'PATH' => "{$pkgroot}/{$packageName}/bin", 'GOROOT' => "{$pkgroot}/{$packageName}", 'GOBIN' => "{$pkgroot}/{$packageName}/bin", 'GOPATH' => "{$pkgroot}/go", ]; } + + public static function getPath(): ?string + { + return PKG_ROOT_PATH . '/go-xcaddy/bin'; + } } diff --git a/src/SPC/store/pkg/Zig.php b/src/SPC/store/pkg/Zig.php index 3ac90c6d..e9865dbe 100644 --- a/src/SPC/store/pkg/Zig.php +++ b/src/SPC/store/pkg/Zig.php @@ -103,7 +103,7 @@ class Zig extends CustomPackage public function extract(string $name): void { $pkgroot = PKG_ROOT_PATH; - $zig_bin_dir = "{$pkgroot}/{$name}"; + $zig_bin_dir = "{$pkgroot}/zig"; $files = ['zig', 'zig-cc', 'zig-c++', 'zig-ar', 'zig-ld.lld', 'zig-ranlib', 'zig-objcopy']; $all_exist = true; @@ -120,7 +120,7 @@ class Zig extends CustomPackage $lock = json_decode(FileSystem::readFile(LockFile::LOCK_FILE), true); $source_type = $lock[$name]['source_type']; $filename = DOWNLOAD_PATH . '/' . ($lock[$name]['filename'] ?? $lock[$name]['dirname']); - $extract = "{$pkgroot}/{$name}"; + $extract = "{$pkgroot}/zig"; FileSystem::extractPackage($name, $source_type, $filename, $extract); @@ -129,34 +129,12 @@ class Zig extends CustomPackage public static function getEnvironment(): array { - $arch = arch2gnu(php_uname('m')); - $os = match (PHP_OS_FAMILY) { - 'Windows' => 'win', - 'Darwin' => 'macos', - 'BSD' => 'freebsd', - default => 'linux', - }; - - $packageName = "zig-{$arch}-{$os}"; - $path = PKG_ROOT_PATH . "/{$packageName}"; - - return [ - 'PATH' => $path, - ]; + return []; } - private static function getPath(): string + public static function getPath(): ?string { - $arch = arch2gnu(php_uname('m')); - $os = match (PHP_OS_FAMILY) { - 'Windows' => 'win', - 'Darwin' => 'macos', - 'BSD' => 'freebsd', - default => 'linux', - }; - - $packageName = "zig-{$arch}-{$os}"; - return PKG_ROOT_PATH . "/{$packageName}"; + return PKG_ROOT_PATH . '/zig'; } private function createZigCcScript(string $bin_dir): void diff --git a/src/SPC/store/scripts/zig-cc.sh b/src/SPC/store/scripts/zig-cc.sh index 388a3655..6b340bc1 100644 --- a/src/SPC/store/scripts/zig-cc.sh +++ b/src/SPC/store/scripts/zig-cc.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")" -BUILDROOT_ABS="$(realpath "$SCRIPT_DIR/../../buildroot/include" 2>/dev/null || true)" +BUILDROOT_ABS="$(realpath "$SCRIPT_DIR/../../../buildroot/include" 2>/dev/null || true)" PARSED_ARGS=() while [[ $# -gt 0 ]]; do @@ -19,9 +19,15 @@ while [[ $# -gt 0 ]]; do ARG_ABS="$(realpath "$ARG" 2>/dev/null || true)" [[ "$ARG_ABS" == "$BUILDROOT_ABS" ]] && PARSED_ARGS+=("-I$ARG") || PARSED_ARGS+=("-isystem$ARG") ;; - -march=*|-mcpu=*) # replace -march=x86-64 with -march=x86_64 + -march=*|-mcpu=*) OPT_NAME="${1%%=*}" OPT_VALUE="${1#*=}" + # Skip armv8- flags entirely as Zig doesn't support them + if [[ "$OPT_VALUE" == armv8-* ]]; then + shift + continue + fi + # replace -march=x86-64 with -march=x86_64 OPT_VALUE="${OPT_VALUE//-/_}" PARSED_ARGS+=("${OPT_NAME}=${OPT_VALUE}") shift diff --git a/src/SPC/store/source/PhpSource.php b/src/SPC/store/source/PhpSource.php index c33e172f..1c0be85f 100644 --- a/src/SPC/store/source/PhpSource.php +++ b/src/SPC/store/source/PhpSource.php @@ -16,11 +16,11 @@ class PhpSource extends CustomSourceBase { $major = defined('SPC_BUILD_PHP_VERSION') ? SPC_BUILD_PHP_VERSION : '8.4'; if ($major === '8.5') { - Downloader::downloadSource('php-src', ['type' => 'url', 'url' => 'https://downloads.php.net/~edorian/php-8.5.0beta1.tar.xz'], $force); + Downloader::downloadSource('php-src', ['type' => 'url', 'url' => 'https://downloads.php.net/~daniels/php-8.5.0RC3.tar.xz'], $force); } elseif ($major === 'git') { Downloader::downloadSource('php-src', ['type' => 'git', 'url' => 'https://github.com/php/php-src.git', 'rev' => 'master'], $force); } else { - Downloader::downloadSource('php-src', self::getLatestPHPInfo($major), $force); + Downloader::downloadSource('php-src', $this->getLatestPHPInfo($major), $force); } } @@ -33,7 +33,7 @@ class PhpSource extends CustomSourceBase // 查找最新的小版本号 $info = json_decode(Downloader::curlExec( url: "https://www.php.net/releases/index.php?json&version={$major_version}", - retries: intval(getenv('SPC_DOWNLOAD_RETRIES') ?: 0) + retries: (int) getenv('SPC_DOWNLOAD_RETRIES') ?: 0 ), true); if (!isset($info['version'])) { throw new DownloaderException("Version {$major_version} not found."); diff --git a/src/SPC/toolchain/ZigToolchain.php b/src/SPC/toolchain/ZigToolchain.php index 3a929f2b..bb423db2 100644 --- a/src/SPC/toolchain/ZigToolchain.php +++ b/src/SPC/toolchain/ZigToolchain.php @@ -42,10 +42,10 @@ class ZigToolchain implements ToolchainInterface public function afterInit(): void { - if (!is_dir(Zig::getEnvironment()['PATH'])) { + if (!Zig::isInstalled()) { throw new EnvironmentException('You are building with zig, but zig is not installed, please install zig first. (You can use `doctor` command to install it)'); } - GlobalEnvManager::addPathIfNotExists(Zig::getEnvironment()['PATH']); + GlobalEnvManager::addPathIfNotExists(Zig::getPath()); f_passthru('ulimit -n 2048'); // zig opens extra file descriptors, so when a lot of extensions are built statically, 1024 is not enough $cflags = getenv('SPC_DEFAULT_C_FLAGS') ?: ''; $cxxflags = getenv('SPC_DEFAULT_CXX_FLAGS') ?: ''; @@ -64,6 +64,11 @@ class ZigToolchain implements ToolchainInterface $extra_libs = trim($extra_libs . ' -lunwind'); GlobalEnvManager::putenv("SPC_EXTRA_LIBS={$extra_libs}"); } + $cflags = getenv('SPC_DEFAULT_C_FLAGS') ?: getenv('CFLAGS') ?: ''; + $has_avx512 = str_contains($cflags, '-mavx512') || str_contains($cflags, '-march=x86-64-v4'); + if (!$has_avx512) { + GlobalEnvManager::putenv('SPC_EXTRA_PHP_VARS=php_cv_have_avx512=no php_cv_have_avx512vbmi=no'); + } } public function getCompilerInfo(): ?string diff --git a/src/SPC/util/ConfigValidator.php b/src/SPC/util/ConfigValidator.php index 58dedea6..445c6242 100644 --- a/src/SPC/util/ConfigValidator.php +++ b/src/SPC/util/ConfigValidator.php @@ -11,6 +11,150 @@ use Symfony\Component\Yaml\Yaml; class ConfigValidator { + /** + * Global field type definitions + * Maps field names to their expected types and validation rules + * Note: This only includes fields used in config files (source.json, lib.json, ext.json, pkg.json, pre-built.json) + */ + private const array FIELD_TYPES = [ + // String fields + 'url' => 'string', // url + 'regex' => 'string', // regex pattern + 'rev' => 'string', // revision/branch + 'repo' => 'string', // repository name + 'match' => 'string', // match pattern (aaa*bbb) + 'filename' => 'string', // filename + 'path' => 'string', // copy path + 'extract' => 'string', // copy path (alias of path) + 'dirname' => 'string', // directory name for local source + 'source' => 'string', // the source name that this item uses + 'match-pattern-linux' => 'string', // pre-built match pattern for linux + 'match-pattern-macos' => 'string', // pre-built match pattern for macos + 'match-pattern-windows' => 'string', // pre-built match pattern for windows + + // Boolean fields + 'prefer-stable' => 'bool', // prefer stable releases + 'provide-pre-built' => 'bool', // provide pre-built binaries + 'notes' => 'bool', // whether to show notes in docs + 'cpp-library' => 'bool', // whether this is a C++ library + 'cpp-extension' => 'bool', // whether this is a C++ extension + 'build-with-php' => 'bool', // whether if this extension can be built to shared with PHP source together + 'zend-extension' => 'bool', // whether this is a zend extension + 'unix-only' => 'bool', // whether this extension is only for unix-like systems + + // Array fields + 'submodules' => 'array', // git submodules list (for git source type) + 'lib-depends' => 'list', + 'lib-suggests' => 'list', + 'ext-depends' => 'list', + 'ext-suggests' => 'list', + 'static-libs' => 'list', + 'pkg-configs' => 'list', // required pkg-config files without suffix (e.g. [libwebp]) + 'headers' => 'list', // required header files + 'bin' => 'list', // required binary files + 'frameworks' => 'list', // shared library frameworks (macOS) + + // Object/assoc array fields + 'support' => 'object', // extension OS support docs + 'extract-files' => 'object', // pkg.json extract files mapping with match pattern + 'alt' => 'object|bool', // alternative source/package + 'license' => 'object|array', // license information + 'target' => 'array', // extension build targets (default: [static], alternate: [shared] or both) + + // Special/mixed fields + 'func' => 'callable', // custom download function for custom source/package type + 'type' => 'string', // type field (validated separately) + ]; + + /** + * Source/Package download type validation rules + * Maps type names to [required_props, optional_props] + */ + private const array SOURCE_TYPE_FIELDS = [ + 'filelist' => [['url', 'regex'], []], + 'git' => [['url', 'rev'], ['path', 'extract', 'submodules']], + 'ghtagtar' => [['repo'], ['path', 'extract', 'prefer-stable', 'match']], + 'ghtar' => [['repo'], ['path', 'extract', 'prefer-stable', 'match']], + 'ghrel' => [['repo', 'match'], ['path', 'extract', 'prefer-stable']], + 'url' => [['url'], ['filename', 'path', 'extract']], + 'bitbuckettag' => [['repo'], ['path', 'extract']], + 'local' => [['dirname'], ['path', 'extract']], + 'pie' => [['repo'], ['path']], + 'custom' => [[], ['func']], + ]; + + /** + * Source.json specific fields [field_name => required] + * Note: 'type' is validated separately in validateSourceTypeConfig + * Field types are defined in FIELD_TYPES constant + */ + private const array SOURCE_FIELDS = [ + 'type' => true, // source type (must be SOURCE_TYPE_FIELDS key) + 'provide-pre-built' => false, // whether to provide pre-built binaries + 'alt' => false, // alternative source configuration + 'license' => false, // license information for source + // ... other fields are validated based on source type + ]; + + /** + * Lib.json specific fields [field_name => required] + * Field types are defined in FIELD_TYPES constant + */ + private const array LIB_FIELDS = [ + 'type' => false, // lib type (lib/package/target/root) + 'source' => false, // the source name that this lib uses + 'lib-depends' => false, // required libraries + 'lib-suggests' => false, // suggested libraries + 'static-libs' => false, // Generated static libraries + 'pkg-configs' => false, // Generated pkg-config files + 'cpp-library' => false, // whether this is a C++ library + 'headers' => false, // Generated header files + 'bin' => false, // Generated binary files + 'frameworks' => false, // Used shared library frameworks (macOS) + ]; + + /** + * Ext.json specific fields [field_name => required] + * Field types are defined in FIELD_TYPES constant + */ + private const array EXT_FIELDS = [ + 'type' => true, // extension type (builtin/external/addon/wip) + 'source' => false, // the source name that this extension uses + 'support' => false, // extension OS support docs + 'notes' => false, // whether to show notes in docs + 'cpp-extension' => false, // whether this is a C++ extension + 'build-with-php' => false, // whether if this extension can be built to shared with PHP source together + 'target' => false, // extension build targets (default: [static], alternate: [shared] or both) + 'lib-depends' => false, + 'lib-suggests' => false, + 'ext-depends' => false, + 'ext-suggests' => false, + 'frameworks' => false, + 'zend-extension' => false, // whether this is a zend extension + 'unix-only' => false, // whether this extension is only for unix-like systems + ]; + + /** + * Pkg.json specific fields [field_name => required] + * Field types are defined in FIELD_TYPES constant + */ + private const array PKG_FIELDS = [ + 'type' => true, // package type (same as source type) + 'extract-files' => false, // files to extract mapping (source pattern => target path) + ]; + + /** + * Pre-built.json specific fields [field_name => required] + * Field types are defined in FIELD_TYPES constant + */ + private const array PRE_BUILT_FIELDS = [ + 'repo' => true, // repository name for pre-built binaries + 'prefer-stable' => false, // prefer stable releases + 'match-pattern-linux' => false, // pre-built match pattern for linux + 'match-pattern-macos' => false, // pre-built match pattern for macos + 'match-pattern-windows' => false, // pre-built match pattern for windows + ]; + /** * Validate source.json * @@ -22,33 +166,20 @@ class ConfigValidator // Validate basic source type configuration self::validateSourceTypeConfig($src, $name, 'source'); - // Check source-specific fields + // Validate all source-specific fields using unified method + self::validateConfigFields($src, $name, 'source', self::SOURCE_FIELDS); + + // Check for unknown fields + self::validateAllowedFields($src, $name, 'source', self::SOURCE_FIELDS); + // check if alt is valid - if (isset($src['alt'])) { - if (!is_assoc_array($src['alt']) && !is_bool($src['alt'])) { - throw new ValidationException("source {$name} alt must be object or boolean"); - } - if (is_assoc_array($src['alt'])) { - // validate alt source recursively - self::validateSource([$name . '_alt' => $src['alt']]); - } - } - - // check if provide-pre-built is boolean - if (isset($src['provide-pre-built']) && !is_bool($src['provide-pre-built'])) { - throw new ValidationException("source {$name} provide-pre-built must be boolean"); - } - - // check if prefer-stable is boolean - if (isset($src['prefer-stable']) && !is_bool($src['prefer-stable'])) { - throw new ValidationException("source {$name} prefer-stable must be boolean"); + if (isset($src['alt']) && is_assoc_array($src['alt'])) { + // validate alt source recursively + self::validateSource([$name . '_alt' => $src['alt']]); } // check if license is valid if (isset($src['license'])) { - if (!is_array($src['license'])) { - throw new ValidationException("source {$name} license must be an object or array"); - } if (is_assoc_array($src['license'])) { self::checkSingleLicense($src['license'], $name); } elseif (is_list_array($src['license'])) { @@ -58,8 +189,6 @@ class ConfigValidator } self::checkSingleLicense($license, $name); } - } else { - throw new ValidationException("source {$name} license must be an object or array"); } } } @@ -71,9 +200,8 @@ class ConfigValidator if (!is_array($data)) { throw new ValidationException('lib.json is broken'); } - // check each lib + foreach ($data as $name => $lib) { - // check if lib is an assoc array if (!is_assoc_array($lib)) { throw new ValidationException("lib {$name} is not an object"); } @@ -89,36 +217,22 @@ class ConfigValidator if (isset($lib['source']) && !empty($source_data) && !isset($source_data[$lib['source']])) { throw new ValidationException("lib {$name} assigns an invalid source: {$lib['source']}"); } - // check if source is string - if (isset($lib['source']) && !is_string($lib['source'])) { - throw new ValidationException("lib {$name} source must be string"); - } - // check if [lib-depends|lib-suggests|static-libs|headers|bin][-windows|-unix|-macos|-linux] are valid list array + + // Validate basic fields using unified method + self::validateConfigFields($lib, $name, 'lib', self::LIB_FIELDS); + + // Validate list array fields with suffixes $suffixes = ['', '-windows', '-unix', '-macos', '-linux']; - foreach ($suffixes as $suffix) { - if (isset($lib['lib-depends' . $suffix]) && !is_list_array($lib['lib-depends' . $suffix])) { - throw new ValidationException("lib {$name} lib-depends must be a list"); - } - if (isset($lib['lib-suggests' . $suffix]) && !is_list_array($lib['lib-suggests' . $suffix])) { - throw new ValidationException("lib {$name} lib-suggests must be a list"); - } - if (isset($lib['static-libs' . $suffix]) && !is_list_array($lib['static-libs' . $suffix])) { - throw new ValidationException("lib {$name} static-libs must be a list"); - } - if (isset($lib['pkg-configs' . $suffix]) && !is_list_array($lib['pkg-configs' . $suffix])) { - throw new ValidationException("lib {$name} pkg-configs must be a list"); - } - if (isset($lib['headers' . $suffix]) && !is_list_array($lib['headers' . $suffix])) { - throw new ValidationException("lib {$name} headers must be a list"); - } - if (isset($lib['bin' . $suffix]) && !is_list_array($lib['bin' . $suffix])) { - throw new ValidationException("lib {$name} bin must be a list"); - } - } - // check if frameworks is a list array - if (isset($lib['frameworks']) && !is_list_array($lib['frameworks'])) { - throw new ValidationException("lib {$name} frameworks must be a list"); + $fields = ['lib-depends', 'lib-suggests', 'static-libs', 'pkg-configs', 'headers', 'bin']; + self::validateListArrayFields($lib, $name, 'lib', $fields, $suffixes); + + // Validate frameworks (special case without suffix) + if (isset($lib['frameworks'])) { + self::validateFieldType('frameworks', $lib['frameworks'], $name, 'lib'); } + + // Check for unknown fields + self::validateAllowedFields($lib, $name, 'lib', self::LIB_FIELDS); } } @@ -127,61 +241,34 @@ class ConfigValidator if (!is_array($data)) { throw new ValidationException('ext.json is broken'); } - // check each extension + foreach ($data as $name => $ext) { - // check if ext is an assoc array if (!is_assoc_array($ext)) { throw new ValidationException("ext {$name} is not an object"); } - // check if ext has valid type + if (!in_array($ext['type'] ?? '', ['builtin', 'external', 'addon', 'wip'])) { throw new ValidationException("ext {$name} type is invalid"); } - // check if external ext has source + + // Check source field requirement if (($ext['type'] ?? '') === 'external' && !isset($ext['source'])) { throw new ValidationException("ext {$name} does not assign any source"); } - // check if source is string - if (isset($ext['source']) && !is_string($ext['source'])) { - throw new ValidationException("ext {$name} source must be string"); - } - // check if support is valid - if (isset($ext['support']) && !is_assoc_array($ext['support'])) { - throw new ValidationException("ext {$name} support must be an object"); - } - // check if notes is boolean - if (isset($ext['notes']) && !is_bool($ext['notes'])) { - throw new ValidationException("ext {$name} notes must be boolean"); - } - // check if [lib-depends|lib-suggests|ext-depends][-windows|-unix|-macos|-linux] are valid list array + + // Validate basic fields using unified method + self::validateConfigFields($ext, $name, 'ext', self::EXT_FIELDS); + + // Validate list array fields with suffixes $suffixes = ['', '-windows', '-unix', '-macos', '-linux']; - foreach ($suffixes as $suffix) { - if (isset($ext['lib-depends' . $suffix]) && !is_list_array($ext['lib-depends' . $suffix])) { - throw new ValidationException("ext {$name} lib-depends must be a list"); - } - if (isset($ext['lib-suggests' . $suffix]) && !is_list_array($ext['lib-suggests' . $suffix])) { - throw new ValidationException("ext {$name} lib-suggests must be a list"); - } - if (isset($ext['ext-depends' . $suffix]) && !is_list_array($ext['ext-depends' . $suffix])) { - throw new ValidationException("ext {$name} ext-depends must be a list"); - } - } - // check if arg-type is valid - if (isset($ext['arg-type'])) { - $valid_arg_types = ['enable', 'with', 'with-path', 'custom', 'none', 'enable-path']; - if (!in_array($ext['arg-type'], $valid_arg_types)) { - throw new ValidationException("ext {$name} arg-type is invalid"); - } - } - // check if arg-type with suffix is valid - foreach ($suffixes as $suffix) { - if (isset($ext['arg-type' . $suffix])) { - $valid_arg_types = ['enable', 'with', 'with-path', 'custom', 'none', 'enable-path']; - if (!in_array($ext['arg-type' . $suffix], $valid_arg_types)) { - throw new ValidationException("ext {$name} arg-type{$suffix} is invalid"); - } - } - } + $fields = ['lib-depends', 'lib-suggests', 'ext-depends', 'ext-suggests']; + self::validateListArrayFields($ext, $name, 'ext', $fields, $suffixes); + + // Validate arg-type fields + self::validateArgTypeFields($ext, $name, $suffixes); + + // Check for unknown fields + self::validateAllowedFields($ext, $name, 'ext', self::EXT_FIELDS); } } @@ -200,12 +287,11 @@ class ConfigValidator // Validate basic source type configuration (reuse from source validation) self::validateSourceTypeConfig($pkg, $name, 'pkg'); - // Check pkg-specific fields - // check if extract-files is valid + // Validate all pkg-specific fields using unified method + self::validateConfigFields($pkg, $name, 'pkg', self::PKG_FIELDS); + + // Validate extract-files content (object validation is done by validateFieldType) if (isset($pkg['extract-files'])) { - if (!is_assoc_array($pkg['extract-files'])) { - throw new ValidationException("pkg {$name} extract-files must be an object"); - } // check each extract file mapping foreach ($pkg['extract-files'] as $source => $target) { if (!is_string($source) || !is_string($target)) { @@ -213,6 +299,9 @@ class ConfigValidator } } } + + // Check for unknown fields + self::validateAllowedFields($pkg, $name, 'pkg', self::PKG_FIELDS); } } @@ -227,18 +316,11 @@ class ConfigValidator throw new ValidationException('pre-built.json is broken'); } - // Check required fields - if (!isset($data['repo'])) { - throw new ValidationException('pre-built.json must have [repo] field'); - } - if (!is_string($data['repo'])) { - throw new ValidationException('pre-built.json [repo] must be string'); - } + // Validate all fields using unified method + self::validateConfigFields($data, 'pre-built', 'pre-built', self::PRE_BUILT_FIELDS); - // Check optional prefer-stable field - if (isset($data['prefer-stable']) && !is_bool($data['prefer-stable'])) { - throw new ValidationException('pre-built.json [prefer-stable] must be boolean'); - } + // Check for unknown fields + self::validateAllowedFields($data, 'pre-built', 'pre-built', self::PRE_BUILT_FIELDS); // Check match pattern fields (at least one must exist) $pattern_fields = ['match-pattern-linux', 'match-pattern-macos', 'match-pattern-windows']; @@ -247,9 +329,6 @@ class ConfigValidator foreach ($pattern_fields as $field) { if (isset($data[$field])) { $has_pattern = true; - if (!is_string($data[$field])) { - throw new ValidationException("pre-built.json [{$field}] must be string"); - } // Validate pattern contains required placeholders if (!str_contains($data[$field], '{name}')) { throw new ValidationException("pre-built.json [{$field}] must contain {name} placeholder"); @@ -403,6 +482,52 @@ class ConfigValidator return $craft; } + /** + * Validate a field based on its global type definition + * + * @param string $field Field name + * @param mixed $value Field value + * @param string $name Item name (for error messages) + * @param string $type Item type (for error messages) + * @return bool Returns true if validation passes + */ + private static function validateFieldType(string $field, mixed $value, string $name, string $type): bool + { + // Check if field exists in FIELD_TYPES + if (!isset(self::FIELD_TYPES[$field])) { + // Try to strip suffix and check base field name + $suffixes = ['-windows', '-unix', '-macos', '-linux']; + $base_field = $field; + foreach ($suffixes as $suffix) { + if (str_ends_with($field, $suffix)) { + $base_field = substr($field, 0, -strlen($suffix)); + break; + } + } + + if (!isset(self::FIELD_TYPES[$base_field])) { + // Unknown field is not allowed - strict validation + throw new ValidationException("{$type} {$name} has unknown field [{$field}]"); + } + + // Use base field type for validation + $expected_type = self::FIELD_TYPES[$base_field]; + } else { + $expected_type = self::FIELD_TYPES[$field]; + } + + return match ($expected_type) { + 'string' => is_string($value) ?: throw new ValidationException("{$type} {$name} [{$field}] must be string"), + 'bool' => is_bool($value) ?: throw new ValidationException("{$type} {$name} [{$field}] must be boolean"), + 'array' => is_array($value) ?: throw new ValidationException("{$type} {$name} [{$field}] must be array"), + 'list' => is_list_array($value) ?: throw new ValidationException("{$type} {$name} [{$field}] must be a list"), + 'object' => is_assoc_array($value) ?: throw new ValidationException("{$type} {$name} [{$field}] must be an object"), + 'object|bool' => (is_assoc_array($value) || is_bool($value)) ?: throw new ValidationException("{$type} {$name} [{$field}] must be object or boolean"), + 'object|array' => is_array($value) ?: throw new ValidationException("{$type} {$name} [{$field}] must be an object or array"), + 'callable' => true, // Skip validation for callable + }; + } + private static function checkSingleLicense(array $license, string $name): void { if (!is_assoc_array($license)) { @@ -414,9 +539,6 @@ class ConfigValidator if (!in_array($license['type'], ['file', 'text'])) { throw new ValidationException("source {$name} license type is invalid"); } - if (!in_array($license['type'], ['file', 'text'])) { - throw new ValidationException("source {$name} license type is invalid"); - } if ($license['type'] === 'file' && !isset($license['path'])) { throw new ValidationException("source {$name} license file must have path"); } @@ -440,68 +562,127 @@ class ConfigValidator if (!is_string($item['type'])) { throw new ValidationException("{$config_type} {$name} type prop must be string"); } - if (!in_array($item['type'], ['filelist', 'git', 'ghtagtar', 'ghtar', 'ghrel', 'url', 'custom'])) { + + if (!isset(self::SOURCE_TYPE_FIELDS[$item['type']])) { throw new ValidationException("{$config_type} {$name} type [{$item['type']}] is invalid"); } - // Validate type-specific requirements - switch ($item['type']) { - case 'filelist': - if (!isset($item['url'], $item['regex'])) { - throw new ValidationException("{$config_type} {$name} needs [url] and [regex] props"); + [$required, $optional] = self::SOURCE_TYPE_FIELDS[$item['type']]; + + // Check required fields exist + foreach ($required as $prop) { + if (!isset($item[$prop])) { + $props = implode('] and [', $required); + throw new ValidationException("{$config_type} {$name} needs [{$props}] props"); + } + } + + // Validate field types using global field type definitions + foreach (array_merge($required, $optional) as $prop) { + if (isset($item[$prop])) { + self::validateFieldType($prop, $item[$prop], $name, $config_type); + } + } + } + + /** + * Validate that fields with suffixes are list arrays + */ + private static function validateListArrayFields(array $item, string $name, string $type, array $fields, array $suffixes): void + { + foreach ($fields as $field) { + foreach ($suffixes as $suffix) { + $key = $field . $suffix; + if (isset($item[$key])) { + self::validateFieldType($key, $item[$key], $name, $type); } - if (!is_string($item['url']) || !is_string($item['regex'])) { - throw new ValidationException("{$config_type} {$name} [url] and [regex] must be string"); + } + } + } + + /** + * Validate arg-type fields with suffixes + */ + private static function validateArgTypeFields(array $item, string $name, array $suffixes): void + { + $valid_arg_types = ['enable', 'with', 'with-path', 'custom', 'none', 'enable-path']; + + foreach (array_merge([''], $suffixes) as $suffix) { + $key = 'arg-type' . $suffix; + if (isset($item[$key]) && !in_array($item[$key], $valid_arg_types)) { + throw new ValidationException("ext {$name} {$key} is invalid"); + } + } + } + + /** + * Unified method to validate config fields based on field definitions + * + * @param array $item Item data to validate + * @param string $name Item name for error messages + * @param string $type Config type (source, lib, ext, pkg, pre-built) + * @param array $field_definitions Field definitions [field_name => required (bool)] + */ + private static function validateConfigFields(array $item, string $name, string $type, array $field_definitions): void + { + foreach ($field_definitions as $field => $required) { + if ($required && !isset($item[$field])) { + throw new ValidationException("{$type} {$name} must have [{$field}] field"); + } + + if (isset($item[$field])) { + self::validateFieldType($field, $item[$field], $name, $type); + } + } + } + + /** + * Validate that item only contains allowed fields + * This method checks for unknown fields based on the config type + * + * @param array $item Item data to validate + * @param string $name Item name for error messages + * @param string $type Config type (source, lib, ext, pkg, pre-built) + * @param array $field_definitions Field definitions [field_name => required (bool)] + */ + private static function validateAllowedFields(array $item, string $name, string $type, array $field_definitions): void + { + // For source and pkg types, we need to check SOURCE_TYPE_FIELDS as well + $allowed_fields = array_keys($field_definitions); + + // For source/pkg, add allowed fields from SOURCE_TYPE_FIELDS based on the type + if (in_array($type, ['source', 'pkg']) && isset($item['type'], self::SOURCE_TYPE_FIELDS[$item['type']])) { + [$required, $optional] = self::SOURCE_TYPE_FIELDS[$item['type']]; + $allowed_fields = array_merge($allowed_fields, $required, $optional); + } + + // For lib and ext types, add fields with suffixes + if (in_array($type, ['lib', 'ext'])) { + $suffixes = ['-windows', '-unix', '-macos', '-linux']; + $base_fields = ['lib-depends', 'lib-suggests', 'static-libs', 'pkg-configs', 'headers', 'bin']; + if ($type === 'ext') { + $base_fields = ['lib-depends', 'lib-suggests', 'ext-depends', 'ext-suggests']; + // Add arg-type fields + foreach (array_merge([''], $suffixes) as $suffix) { + $allowed_fields[] = 'arg-type' . $suffix; } - break; - case 'git': - if (!isset($item['url'], $item['rev'])) { - throw new ValidationException("{$config_type} {$name} needs [url] and [rev] props"); + } + foreach ($base_fields as $field) { + foreach ($suffixes as $suffix) { + $allowed_fields[] = $field . $suffix; } - if (!is_string($item['url']) || !is_string($item['rev'])) { - throw new ValidationException("{$config_type} {$name} [url] and [rev] must be string"); - } - if (isset($item['path']) && !is_string($item['path'])) { - throw new ValidationException("{$config_type} {$name} [path] must be string"); - } - break; - case 'ghtagtar': - case 'ghtar': - if (!isset($item['repo'])) { - throw new ValidationException("{$config_type} {$name} needs [repo] prop"); - } - if (!is_string($item['repo'])) { - throw new ValidationException("{$config_type} {$name} [repo] must be string"); - } - if (isset($item['path']) && !is_string($item['path'])) { - throw new ValidationException("{$config_type} {$name} [path] must be string"); - } - break; - case 'ghrel': - if (!isset($item['repo'], $item['match'])) { - throw new ValidationException("{$config_type} {$name} needs [repo] and [match] props"); - } - if (!is_string($item['repo']) || !is_string($item['match'])) { - throw new ValidationException("{$config_type} {$name} [repo] and [match] must be string"); - } - break; - case 'url': - if (!isset($item['url'])) { - throw new ValidationException("{$config_type} {$name} needs [url] prop"); - } - if (!is_string($item['url'])) { - throw new ValidationException("{$config_type} {$name} [url] must be string"); - } - if (isset($item['filename']) && !is_string($item['filename'])) { - throw new ValidationException("{$config_type} {$name} [filename] must be string"); - } - if (isset($item['path']) && !is_string($item['path'])) { - throw new ValidationException("{$config_type} {$name} [path] must be string"); - } - break; - case 'custom': - // custom type has no specific requirements - break; + } + // frameworks is lib-only + if ($type === 'lib') { + $allowed_fields[] = 'frameworks'; + } + } + + // Check each field in item + foreach (array_keys($item) as $field) { + if (!in_array($field, $allowed_fields)) { + throw new ValidationException("{$type} {$name} has unknown field [{$field}]"); + } } } } diff --git a/src/SPC/util/GlobalEnvManager.php b/src/SPC/util/GlobalEnvManager.php index 9f74d2df..4ce3384d 100644 --- a/src/SPC/util/GlobalEnvManager.php +++ b/src/SPC/util/GlobalEnvManager.php @@ -40,7 +40,12 @@ class GlobalEnvManager if (is_unix()) { self::addPathIfNotExists(BUILD_BIN_PATH); self::addPathIfNotExists(PKG_ROOT_PATH . '/bin'); - self::putenv('PKG_CONFIG_PATH=' . BUILD_LIB_PATH . '/pkgconfig'); + $pkgConfigPath = getenv('PKG_CONFIG_PATH'); + if ($pkgConfigPath !== false) { + self::putenv('PKG_CONFIG_PATH=' . BUILD_LIB_PATH . "/pkgconfig:{$pkgConfigPath}"); + } else { + self::putenv('PKG_CONFIG_PATH=' . BUILD_LIB_PATH . '/pkgconfig'); + } } $ini = self::readIniFile(); diff --git a/src/SPC/util/SPCConfigUtil.php b/src/SPC/util/SPCConfigUtil.php index d19cc858..f321ca10 100644 --- a/src/SPC/util/SPCConfigUtil.php +++ b/src/SPC/util/SPCConfigUtil.php @@ -6,6 +6,7 @@ namespace SPC\util; use SPC\builder\BuilderBase; use SPC\builder\BuilderProvider; +use SPC\builder\Extension; use SPC\exception\WrongUsageException; use SPC\store\Config; use Symfony\Component\Console\Input\ArgvInput; @@ -87,7 +88,7 @@ class SPCConfigUtil if (SPCTarget::getTargetOS() === 'Darwin') { $libs .= " {$this->getFrameworksString($extensions)}"; } - if ($this->builder->hasCpp()) { + if ($this->hasCpp($extensions, $libraries)) { $libcpp = SPCTarget::getTargetOS() === 'Darwin' ? '-lc++' : '-lstdc++'; $libs = str_replace($libcpp, '', $libs) . " {$libcpp}"; } @@ -123,6 +124,27 @@ class SPCConfigUtil ]; } + private function hasCpp(array $extensions, array $libraries): bool + { + // judge cpp-extension + $builderExtNames = array_keys($this->builder->getExts(false)); + $exts = array_unique([...$builderExtNames, ...$extensions]); + + foreach ($exts as $ext) { + if (Config::getExt($ext, 'cpp-extension', false) === true) { + return true; + } + } + $builderLibNames = array_keys($this->builder->getLibs()); + $libs = array_unique([...$builderLibNames, ...$libraries]); + foreach ($libs as $lib) { + if (Config::getLib($lib, 'cpp-library', false) === true) { + return true; + } + } + return false; + } + private function getIncludesString(array $libraries): string { $base = BUILD_INCLUDE_PATH; diff --git a/src/SPC/util/executor/UnixAutoconfExecutor.php b/src/SPC/util/executor/UnixAutoconfExecutor.php index 075d7fd3..f196cb1c 100644 --- a/src/SPC/util/executor/UnixAutoconfExecutor.php +++ b/src/SPC/util/executor/UnixAutoconfExecutor.php @@ -50,18 +50,22 @@ class UnixAutoconfExecutor extends Executor * @param bool $with_clean Whether to clean before building * @param array $after_env_vars Environment variables postfix */ - public function make(string $target = '', false|string $with_install = 'install', bool $with_clean = true, array $after_env_vars = []): static + public function make(string $target = '', false|string $with_install = 'install', bool $with_clean = true, array $after_env_vars = [], ?string $dir = null): static { - return $this->seekLogFileOnException(function () use ($target, $with_install, $with_clean, $after_env_vars) { + return $this->seekLogFileOnException(function () use ($target, $with_install, $with_clean, $after_env_vars, $dir) { + $shell = $this->shell; + if ($dir) { + $shell = $shell->cd($dir); + } if ($with_clean) { - $this->shell->exec('make clean'); + $shell->exec('make clean'); } $after_env_vars_str = $after_env_vars !== [] ? shell()->setEnv($after_env_vars)->getEnvString() : ''; - $this->shell->exec("make -j{$this->library->getBuilder()->concurrency} {$target} {$after_env_vars_str}"); + $shell->exec("make -j{$this->library->getBuilder()->concurrency} {$target} {$after_env_vars_str}"); if ($with_install !== false) { - $this->shell->exec("make {$with_install}"); + $shell->exec("make {$with_install}"); } - return $this->shell; + return $shell; }); } diff --git a/src/SPC/util/executor/UnixCMakeExecutor.php b/src/SPC/util/executor/UnixCMakeExecutor.php index 3e29addf..eceab901 100644 --- a/src/SPC/util/executor/UnixCMakeExecutor.php +++ b/src/SPC/util/executor/UnixCMakeExecutor.php @@ -205,7 +205,7 @@ SET(CMAKE_INSTALL_PREFIX "{$root}") SET(CMAKE_INSTALL_LIBDIR "lib") set(PKG_CONFIG_EXECUTABLE "{$pkgConfigExecutable}") -list(APPEND PKG_CONFIG_EXECUTABLE "--static") +set(PKG_CONFIG_ARGN "--static" CACHE STRING "Extra arguments for pkg-config" FORCE) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) diff --git a/src/SPC/util/shell/UnixShell.php b/src/SPC/util/shell/UnixShell.php index 75a0be3d..2fdc1b07 100644 --- a/src/SPC/util/shell/UnixShell.php +++ b/src/SPC/util/shell/UnixShell.php @@ -8,6 +8,7 @@ use SPC\builder\freebsd\library\BSDLibraryBase; use SPC\builder\linux\library\LinuxLibraryBase; use SPC\builder\macos\library\MacOSLibraryBase; use SPC\exception\SPCInternalException; +use SPC\util\SPCTarget; use ZM\Logger\ConsoleColor; /** @@ -28,6 +29,7 @@ class UnixShell extends Shell public function exec(string $cmd): static { + $cmd = clean_spaces($cmd); /* @phpstan-ignore-next-line */ logger()->info(ConsoleColor::yellow('[EXEC] ') . ConsoleColor::green($cmd)); $original_command = $cmd; @@ -48,7 +50,7 @@ class UnixShell extends Shell 'CFLAGS' => $library->getLibExtraCFlags(), 'CXXFLAGS' => $library->getLibExtraCXXFlags(), 'LDFLAGS' => $library->getLibExtraLdFlags(), - 'LIBS' => $library->getLibExtraLibs(), + 'LIBS' => $library->getLibExtraLibs() . SPCTarget::getRuntimeLibs(), ]); return $this; } diff --git a/src/globals/ext-tests/gettext.php b/src/globals/ext-tests/gettext.php index f34f487a..b5bc901e 100644 --- a/src/globals/ext-tests/gettext.php +++ b/src/globals/ext-tests/gettext.php @@ -4,20 +4,40 @@ declare(strict_types=1); assert(function_exists('gettext')); assert(function_exists('bindtextdomain')); +assert(function_exists('bind_textdomain_codeset')); assert(function_exists('textdomain')); -if (!is_dir('locale/en_US/LC_MESSAGES/')) { - mkdir('locale/en_US/LC_MESSAGES/', 0755, true); +foreach (['en_US', 'en_GB'] as $lc) { + $dir = "locale/{$lc}/LC_MESSAGES"; + if (!is_dir($dir)) { + mkdir($dir, 0755, true); + } + $mo = '3hIElQAAAAACAAAAHAAAACwAAAAFAAAAPAAAAAAAAABQAAAABgAAAFEAAAAXAQAAWAAAAAcAAABwAQAAAQAAAAAAAAAAAAAAAgAAAAAAAAAA56S65L6LAFByb2plY3QtSWQtVmVyc2lvbjogUEFDS0FHRSBWRVJTSU9OClJlcG9ydC1Nc2dpZC1CdWdzLVRvOiAKUE8tUmV2aXNpb24tRGF0ZTogWUVBUi1NTy1EQSBITzpNSytaT05FCkxhc3QtVHJhbnNsYXRvcjogRlVMTCBOQU1FIDxFTUFJTEBBRERSRVNTPgpMYW5ndWFnZS1UZWFtOiBMQU5HVUFHRSA8TExAbGkub3JnPgpMYW5ndWFnZTogCk1JTUUtVmVyc2lvbjogMS4wCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD1VVEYtOApDb250ZW50LVRyYW5zZmVyLUVuY29kaW5nOiA4Yml0CgBFeGFtcGxlAA=='; + $path = "{$dir}/test.mo"; + if (!file_exists($path)) { + file_put_contents($path, base64_decode($mo)); + } } -if (!file_exists('locale/en_US/LC_MESSAGES/test.mo')) { - $mo = '3hIElQAAAAACAAAAHAAAACwAAAAFAAAAPAAAAAAAAABQAAAABgAAAFEAAAAXAQAAWAAAAAcAAABwAQAAAQAAAAAAAAAAAAAAAgAAAAAAAAAA56S65L6LAFByb2plY3QtSWQtVmVyc2lvbjogUEFDS0FHRSBWRVJTSU9OClJlcG9ydC1Nc2dpZC1CdWdzLVRvOiAKUE8tUmV2aXNpb24tRGF0ZTogWUVBUi1NTy1EQSBITzpNSStaT05FCkxhc3QtVHJhbnNsYXRvcjogRlVMTCBOQU1FIDxFTUFJTEBBRERSRVNTPgpMYW5ndWFnZS1UZWFtOiBMQU5HVUFHRSA8TExAbGkub3JnPgpMYW5ndWFnZTogCk1JTUUtVmVyc2lvbjogMS4wCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD1VVEYtOApDb250ZW50LVRyYW5zZmVyLUVuY29kaW5nOiA4Yml0CgBFeGFtcGxlAA=='; - file_put_contents('locale/en_US/LC_MESSAGES/test.mo', base64_decode($mo)); -} -putenv('LANG=en_US'); -assert(setlocale(LC_ALL, 'en_US.utf-8') === 'en_US.utf-8'); + +// Probe for an available English locale +$candidates = [ + 'en_US.UTF-8', 'en_US.utf8', 'en_US.utf-8', 'en_US', + 'en_GB.UTF-8', 'en_GB.utf8', 'en_GB.utf-8', 'en_GB', + 'English_United States.65001', 'English_United States.1252', + 'English_United Kingdom.65001', 'English_United Kingdom.1252', +]; + +$locale = setlocale(LC_ALL, $candidates); +assert($locale !== false); + +putenv('LC_ALL=' . $locale); +putenv('LANG=' . $locale); +putenv('LANGUAGE=' . (stripos($locale, 'US') !== false ? 'en_US:en_GB' : 'en_GB:en_US')); $domain = 'test'; bindtextdomain($domain, 'locale/'); +bind_textdomain_codeset($domain, 'UTF-8'); textdomain($domain); -assert(gettext(json_decode('"\u793a\u4f8b"', true)) === 'Example'); +$src = json_decode('"\u793a\u4f8b"', true); +assert(gettext($src) === 'Example'); diff --git a/src/globals/functions.php b/src/globals/functions.php index 71127960..c8c6a8d0 100644 --- a/src/globals/functions.php +++ b/src/globals/functions.php @@ -245,6 +245,23 @@ function clean_spaces(string $string): string return trim(preg_replace('/\s+/', ' ', $string)); } +/** + * Deduplicate flags in a string. Only the last occurence of each flag will be kept. + * E.g. `-lintl -lstdc++ -lphp -lstdc++` becomes `-lintl -lphp -lstdc++` + * + * @param string $flags the string containing flags to deduplicate + * @return string the deduplicated string with no duplicate flags + */ +function deduplicate_flags(string $flags): string +{ + $tokens = preg_split('/\s+/', trim($flags)); + + // Reverse, unique, reverse back - keeps last occurrence of duplicates + $deduplicated = array_reverse(array_unique(array_reverse($tokens))); + + return implode(' ', $deduplicated); +} + /** * Register a callback function to handle keyboard interrupts (Ctrl+C). * @@ -283,3 +300,20 @@ function strip_ansi_colors(string $text): string // Including color codes, cursor control, clear screen and other control sequences return preg_replace('/\e\[[0-9;]*[a-zA-Z]/', '', $text); } + +/** + * Convert to a real path for display purposes, used in docker volumes. + */ +function get_display_path(string $path): string +{ + $deploy_root = getenv('SPC_FIX_DEPLOY_ROOT'); + if ($deploy_root === false) { + return $path; + } + $cwd = WORKING_DIR; + // replace build root with deploy root, only if path starts with build root + if (str_starts_with($path, $cwd)) { + return $deploy_root . substr($path, strlen($cwd)); + } + throw new WrongUsageException("Cannot convert path: {$path}"); +} diff --git a/src/globals/patch/musl_static_readline.patch b/src/globals/patch/musl_static_readline.patch new file mode 100644 index 00000000..19611621 --- /dev/null +++ b/src/globals/patch/musl_static_readline.patch @@ -0,0 +1,26 @@ +diff --git a/ext/readline/readline_cli.c b/ext/readline/readline_cli.c +index 31212999..d80a21c3 100644 +--- a/ext/readline/readline_cli.c ++++ b/ext/readline/readline_cli.c +@@ -739,8 +739,8 @@ typedef cli_shell_callbacks_t *(__cdecl *get_cli_shell_callbacks)(void); + } while(0) + + #else +-/* + #ifdef COMPILE_DL_READLINE ++/* + This dlsym() is always used as even the CGI SAPI is linked against "CLI"-only + extensions. If that is being changed dlsym() should only be used when building + this extension sharedto offer compatibility. +@@ -754,9 +754,9 @@ this extension sharedto offer compatibility. + (cb) = get_callbacks(); \ + } \ + } while(0) +-/*#else ++#else + #define GET_SHELL_CB(cb) (cb) = php_cli_get_shell_callbacks() +-#endif*/ ++#endif + #endif + + PHP_MINIT_FUNCTION(cli_readline) diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index 764cfc7d..5aef4bd8 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -13,29 +13,28 @@ declare(strict_types=1); // test php version (8.1 ~ 8.4 available, multiple for matrix) $test_php_version = [ - // '8.1', + '8.1', // '8.2', // '8.3', - '8.4', - // '8.5', + // '8.4', + '8.5', // 'git', ]; -// test os (macos-13, macos-14, macos-15, ubuntu-latest, windows-latest are available) +// test os (macos-15-intel, macos-15, ubuntu-latest, windows-latest are available) $test_os = [ - 'macos-13', // bin/spc for x86_64 - // 'macos-14', // bin/spc for arm64 + 'macos-15-intel', // bin/spc for x86_64 'macos-15', // bin/spc for arm64 'ubuntu-latest', // bin/spc-alpine-docker for x86_64 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 - // 'ubuntu-24.04', // bin/spc for x86_64 + 'ubuntu-24.04', // bin/spc for x86_64 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 - // 'ubuntu-24.04-arm', // bin/spc for arm64 + 'ubuntu-24.04-arm', // bin/spc for arm64 // 'windows-latest', // .\bin\spc.ps1 ]; // whether enable thread safe -$zts = true; +$zts = false; $no_strip = false; @@ -50,13 +49,13 @@ $prefer_pre_built = false; // If you want to test your added extensions and libs, add below (comma separated, example `bcmath,openssl`). $extensions = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'bcmath', + 'Linux', 'Darwin' => 'gettext', 'Windows' => 'bcmath,bz2,calendar,ctype,curl,dom,exif,fileinfo,filter,ftp,iconv,xml,mbstring,mbregex,mysqlnd,openssl,pdo,pdo_mysql,pdo_sqlite,phar,session,simplexml,soap,sockets,sqlite3,tokenizer,xmlwriter,xmlreader,zlib,zip', }; // If you want to test shared extensions, add them below (comma separated, example `bcmath,openssl`). $shared_extensions = match (PHP_OS_FAMILY) { - 'Linux' => 'zip', + 'Linux' => '', 'Darwin' => '', 'Windows' => '', }; @@ -156,17 +155,13 @@ if ($shared_extensions) { switch ($argv[2] ?? null) { case 'ubuntu-22.04': case 'ubuntu-22.04-arm': + case 'macos-15': + case 'macos-15-intel': $shared_cmd = ' --build-shared=' . quote2($shared_extensions) . ' '; break; case 'ubuntu-24.04': case 'ubuntu-24.04-arm': break; - case 'macos-13': - case 'macos-14': - case 'macos-15': - $shared_cmd = ' --build-shared=' . quote2($shared_extensions) . ' '; - $no_strip = true; - break; default: $shared_cmd = ''; break; diff --git a/tests/SPC/builder/BuilderTest.php b/tests/SPC/builder/BuilderTest.php index d28bd4d3..b4b6258f 100644 --- a/tests/SPC/builder/BuilderTest.php +++ b/tests/SPC/builder/BuilderTest.php @@ -62,12 +62,6 @@ class BuilderTest extends TestCase $this->assertInstanceOf(Extension::class, $this->builder->getExt('mbregex')); } - public function testHasCpp() - { - // mbregex doesn't have cpp - $this->assertFalse($this->builder->hasCpp()); - } - public function testMakeExtensionArgs() { $this->assertStringContainsString('--enable-mbstring', $this->builder->makeStaticExtensionArgs()); diff --git a/tests/SPC/util/ConfigValidatorTest.php b/tests/SPC/util/ConfigValidatorTest.php index f132636f..aba611a4 100644 --- a/tests/SPC/util/ConfigValidatorTest.php +++ b/tests/SPC/util/ConfigValidatorTest.php @@ -50,7 +50,6 @@ class ConfigValidatorTest extends TestCase 'filename' => 'test.tar.gz', 'path' => 'test/path', 'provide-pre-built' => true, - 'prefer-stable' => false, 'license' => [ 'type' => 'file', 'path' => 'LICENSE', diff --git a/tests/SPC/util/SPCConfigUtilTest.php b/tests/SPC/util/SPCConfigUtilTest.php index 92353824..c4b1427a 100644 --- a/tests/SPC/util/SPCConfigUtilTest.php +++ b/tests/SPC/util/SPCConfigUtilTest.php @@ -44,6 +44,9 @@ class SPCConfigUtilTest extends TestCase public function testConfig(): void { + if (PHP_OS_FAMILY !== 'Linux') { + $this->markTestSkipped('SPCConfigUtil tests are only applicable on Linux.'); + } // normal $result = (new SPCConfigUtil())->config(['bcmath']); $this->assertStringContainsString(BUILD_ROOT_PATH . '/include', $result['cflags']);