Merge branch 'main' into feat/gnu-static

# Conflicts:
#	src/SPC/builder/linux/LinuxBuilder.php
This commit is contained in:
crazywhalecc 2025-03-09 17:44:13 +08:00
commit 23bfad6f87
No known key found for this signature in database
GPG Key ID: 1F4BDD59391F2680
66 changed files with 2320 additions and 1270 deletions

8
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,8 @@
---
name: Bug report
about: Build PHP or library failed, download failed, doesn't seem to work...
title: ''
labels: bug
assignees: crazywhalecc
---

View File

@ -0,0 +1,8 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: new feature
assignees: ''
---

View File

@ -0,0 +1,10 @@
---
name: Something want to know
about: Describe your question about static-php, we will reply ASAP
title: ''
labels: question
assignees: ''
---

View File

@ -7,6 +7,7 @@
> If your PR involves the changes mentioned below and completed the action, please tick the corresponding option.
> If a modification is not involved, please skip it directly.
- [ ] If you modified `*.php`, run `composer cs-fix` at local machine.
- [ ] If it's an extension or dependency update, make sure adding related extensions in `src/global/test-extensions.php`.
- [ ] If you changed the behavior of static-php-cli, update docs in `./docs/`.
- [ ] If you updated `config/xxx.json` content, run `bin/spc dev:sort-config xxx`.

View File

@ -129,7 +129,7 @@ jobs:
- name: "Setup PHP"
uses: shivammathur/setup-php@v2
with:
php-version: ${{ inputs.php-version }}
php-version: 8.4
tools: pecl, composer
extensions: curl, openssl, mbstring
ini-values: memory_limit=-1

View File

@ -6,7 +6,7 @@ on:
version:
required: true
description: php version to compile
default: '8.2'
default: '8.4'
type: choice
options:
- '8.4'

View File

@ -113,7 +113,7 @@ jobs:
- name: "Setup PHP"
uses: shivammathur/setup-php@v2
with:
php-version: 8.2
php-version: 8.4
tools: pecl, composer
extensions: curl, openssl, mbstring
ini-values: memory_limit=-1

View File

@ -9,8 +9,8 @@ on:
workflow_dispatch:
env:
PHP_VERSION: 8.2
MICRO_VERSION: 8.2.18
PHP_VERSION: 8.4
MICRO_VERSION: 8.4.4
jobs:
build-release-artifacts:

View File

@ -34,13 +34,13 @@ jobs:
- name: "Setup PHP"
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
php-version: '8.4'
extensions: curl, openssl, mbstring
ini-values: memory_limit=-1
tools: pecl, composer, php-cs-fixer
- name: Run PHP-CS-Fixer fix
run: php-cs-fixer fix --dry-run --diff --ansi
run: PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix --dry-run --diff --ansi
phpstan:
runs-on: ubuntu-latest
@ -52,7 +52,7 @@ jobs:
- name: "Setup PHP"
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
php-version: '8.4'
extensions: curl, openssl, mbstring
ini-values: memory_limit=-1
tools: composer
@ -79,9 +79,6 @@ jobs:
strategy:
matrix:
include:
- php: '8.1'
- php: '8.2'
- php: '8.3'
- php: '8.4'
steps:
@ -125,7 +122,7 @@ jobs:
- name: "Setup PHP"
uses: shivammathur/setup-php@v2
with:
php-version: 8.2
php-version: 8.4
- name: Define
id: gendef
@ -153,7 +150,7 @@ jobs:
- name: "Setup PHP"
uses: shivammathur/setup-php@v2
with:
php-version: 8.2
php-version: 8.4
tools: pecl, composer
extensions: curl, openssl, mbstring
ini-values: memory_limit=-1

View File

@ -36,7 +36,7 @@ jobs:
with:
coverage: none
tools: composer:v2
php-version: 8.2
php-version: 8.4
ini-values: memory_limit=-1
- name: "Get Composer Cache Directory"

1
.gitignore vendored
View File

@ -32,6 +32,7 @@ packlib_files.txt
!/bin/spc*
!/bin/setup-runtime*
!/bin/spc-alpine-docker
!/bin/php-cs-fixer-wrapper
# exclude windows build tools
/php-sdk-binary-tools/

View File

@ -64,6 +64,8 @@ return (new PhpCsFixer\Config())
'php_unit_test_class_requires_covers' => false,
'phpdoc_var_without_name' => false,
'fully_qualified_strict_types' => false,
'operator_linebreak' => false,
'php_unit_data_provider_method_order' => false,
])
->setFinder(
PhpCsFixer\Finder::create()->in([__DIR__ . '/src', __DIR__ . '/tests/SPC'])

View File

@ -58,7 +58,7 @@ static-php-cli简称 `spc`)有许多特性:
### 编译环境需求
- PHP >= 8.1(这是 spc 自身需要的版本,不是支持的构建版本)
- PHP >= 8.4(这是 spc 自身需要的版本,不是支持的构建版本)
- 扩展:`mbstring,tokenizer,phar`
- 系统安装了 `curl``git`
@ -180,6 +180,9 @@ bin/spc --version
# 检查环境依赖,并根据尝试自动安装缺失的编译工具
./bin/spc doctor --auto-fix
# 输出目标项目依赖的扩展列表
./bin/spc dump-extensions /path/to/your/project --format=text
# 拉取所有依赖库
./bin/spc download --all
# 只拉取编译指定扩展需要的所有依赖(推荐)

View File

@ -67,7 +67,7 @@ which can be downloaded directly according to your needs.
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.1 (This is the version required by spc itself, not the build version)
- 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
@ -194,6 +194,8 @@ Basic usage for building php with some extensions:
# 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)

4
bin/php-cs-fixer-wrapper Executable file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env bash
# get parent dir, and run the php-cs-fixer
PHP_CS_FIXER_IGNORE_ENV=1 "$(dirname "$0")/../vendor/bin/php-cs-fixer" "$@"

View File

@ -25,7 +25,7 @@ __DIR__=$(cd "$(dirname "$0")" && pwd)
__PROJECT__=$(cd "${__DIR__}"/../ && pwd)
# set download dir
__PHP_RUNTIME_URL__="https://dl.static-php.dev/static-php-cli/bulk/php-8.2.13-cli-${__OS_FIXED__}-${__ARCH__}.tar.gz"
__PHP_RUNTIME_URL__="https://dl.static-php.dev/static-php-cli/bulk/php-8.4.4-cli-${__OS_FIXED__}-${__ARCH__}.tar.gz"
__COMPOSER_URL__="https://getcomposer.org/download/latest-stable/composer.phar"
# use china mirror
@ -45,7 +45,7 @@ done
case "$mirror" in
china)
__PHP_RUNTIME_URL__="https://dl.static-php.dev/static-php-cli/bulk/php-8.2.13-cli-${__OS_FIXED__}-${__ARCH__}.tar.gz"
__PHP_RUNTIME_URL__="https://dl.static-php.dev/static-php-cli/bulk/php-8.4.4-cli-${__OS_FIXED__}-${__ARCH__}.tar.gz"
__COMPOSER_URL__="https://mirrors.tencent.com/composer/composer.phar"
;;
@ -61,7 +61,7 @@ test -f "${__PROJECT__}"/downloads/runtime.tar.gz || { echo "Downloading $__PHP_
test -f "${__DIR__}"/php || { tar -xf "${__PROJECT__}"/downloads/runtime.tar.gz -C "${__DIR__}"/ ; }
chmod +x "${__DIR__}"/php
# download composer
test -f "${__DIR__}"/composer || curl -#fSL -o "${__DIR__}"/composer "$__COMPOSER_URL__"
test -f "${__DIR__}"/composer || { echo "Downloading Composer from $__COMPOSER_URL__" && curl -#fSL -o "${__DIR__}"/composer "$__COMPOSER_URL__" ; }
chmod +x "${__DIR__}"/composer
# sanity check for php and composer
"${__DIR__}"/php -v >/dev/null || { echo "Failed to run php" && exit 1; }

View File

@ -51,7 +51,7 @@ if ($action -eq 'add-path') {
}
# get php 8.1 specific version
$API = (Invoke-WebRequest -Uri "https://www.php.net/releases/index.php?json&version=8.3") | ConvertFrom-Json
$API = (Invoke-WebRequest -Uri "https://www.php.net/releases/index.php?json&version=8.4") | ConvertFrom-Json
# php windows download
$PHPRuntimeUrl = "https://windows.php.net/downloads/releases/php-" + $API.version + "-nts-Win32-vs16-x64.zip"

View File

@ -17,6 +17,11 @@ if (PHP_OS_FAMILY === 'Windows' && Phar::running()) {
exec('CHCP 65001');
}
// Print deprecation notice on PHP < 8.4, use red and highlight background
if (PHP_VERSION_ID < 80400) {
echo "\e[43mDeprecation Notice: PHP < 8.4 is deprecated, please upgrade your PHP version.\e[0m\n";
}
try {
(new ConsoleApplication())->run();
} catch (Exception $e) {

View File

@ -50,9 +50,9 @@ else
fi
# Detect docker env is setup
if ! $DOCKER_EXECUTABLE images | grep -q cwcc-spc-$SPC_USE_ARCH; then
if ! $DOCKER_EXECUTABLE images | grep -q cwcc-spc-$SPC_USE_ARCH-v2; then
echo "Docker container does not exist. Building docker image ..."
$DOCKER_EXECUTABLE build -t cwcc-spc-$SPC_USE_ARCH -f- . <<EOF
$DOCKER_EXECUTABLE build -t cwcc-spc-$SPC_USE_ARCH-v2 -f- . <<EOF
FROM $ALPINE_FROM
$SPC_USE_MIRROR
RUN apk update; \
@ -79,21 +79,16 @@ RUN apk update; \
linux-headers \
m4 \
make \
php82 \
php82-common \
php82-pcntl \
php82-phar \
php82-posix \
php82-sodium \
php82-tokenizer \
php82-dom \
php82-xml \
php82-xmlwriter \
composer \
pkgconfig \
wget \
xz
RUN curl -#fSL https://dl.static-php.dev/static-php-cli/bulk/php-8.4.4-cli-linux-\$(uname -m).tar.gz | tar -xz -C /usr/local/bin && \
chmod +x /usr/local/bin/php
RUN curl -#fSL https://getcomposer.org/download/latest-stable/composer.phar -o /usr/local/bin/composer && \
chmod +x /usr/local/bin/composer
WORKDIR /app
ADD ./src /app/src
COPY ./composer.* /app/
@ -124,4 +119,4 @@ MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/pkgroot:/app/pkgroot"
# shellcheck disable=SC2068
# shellcheck disable=SC2086
# shellcheck disable=SC2090
$DOCKER_EXECUTABLE run --rm $INTERACT -e SPC_FIX_DEPLOY_ROOT="$(pwd)" $MOUNT_LIST cwcc-spc-$SPC_USE_ARCH bin/spc $@
$DOCKER_EXECUTABLE run --rm $INTERACT -e SPC_FIX_DEPLOY_ROOT="$(pwd)" $MOUNT_LIST cwcc-spc-$SPC_USE_ARCH-v2 bin/spc $@

View File

@ -9,7 +9,7 @@
}
],
"require": {
"php": ">= 8.1",
"php": ">= 8.3",
"ext-mbstring": "*",
"ext-zlib": "*",
"laravel/prompts": "^0.1.12",
@ -19,7 +19,7 @@
"require-dev": {
"captainhook/captainhook-phar": "^5.23",
"captainhook/hook-installer": "^1.0",
"friendsofphp/php-cs-fixer": "^3.25",
"friendsofphp/php-cs-fixer": "^3.60",
"humbug/box": "^4.5.0 || ^4.6.0",
"nunomaduro/collision": "^7.8",
"phpstan/phpstan": "^1.10",
@ -44,7 +44,7 @@
],
"scripts": {
"analyse": "phpstan analyse --memory-limit 300M",
"cs-fix": "php-cs-fixer fix",
"cs-fix": "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix",
"test": "vendor/bin/phpunit tests/ --no-coverage",
"build:phar": "vendor/bin/box compile"
},

2702
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -355,10 +355,8 @@
"type": "external",
"source": "ext-memcache",
"arg-type": "custom",
"lib-depends": [
"zlib"
],
"ext-depends": [
"zlib",
"session"
]
},
@ -445,6 +443,13 @@
"zlib"
]
},
"opentelemetry": {
"support": {
"BSD": "wip"
},
"type": "external",
"source": "opentelemetry"
},
"parallel": {
"support": {
"BSD": "wip"

View File

@ -1,4 +1,29 @@
{
"lib-base": {
"type": "root",
"lib-depends-unix": [
"pkg-config"
]
},
"php": {
"type": "root",
"source": "php-src",
"lib-depends": [
"micro",
"lib-base"
]
},
"micro": {
"type": "target",
"source": "micro"
},
"pkg-config": {
"type": "package",
"source": "pkg-config",
"bin-unix": [
"pkg-config"
]
},
"brotli": {
"source": "brotli",
"static-libs-unix": [
@ -34,7 +59,7 @@
"libcurl.a"
],
"static-libs-windows": [
"libcurl_a.lib"
"libcurl.lib"
],
"headers": [
"curl"
@ -599,9 +624,6 @@
"zlib"
]
},
"pkg-config": {
"source": "pkg-config"
},
"postgresql": {
"source": "postgresql",
"static-libs-unix": [

View File

@ -683,6 +683,16 @@
"path": "LICENSE.txt"
}
},
"opentelemetry": {
"type": "url",
"url": "https://pecl.php.net/get/opentelemetry",
"path": "php-src/ext/opentelemetry",
"filename": "opentelemetry.tgz",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"parallel": {
"type": "url",
"url": "https://pecl.php.net/get/parallel",

View File

@ -57,56 +57,6 @@ cd static-php-cli
composer update
```
### Use System PHP
Below are some example commands for installing PHP and Composer in the system.
It is recommended to search for the specific installation method yourself or ask the AI search engine to obtain the answer,
which will not be elaborated here.
```bash
# [macOS], need install Homebrew first. See https://brew.sh/
# Remember change your composer executable path. For M1/M2 Chip mac, "/opt/homebrew/bin/", for Intel mac, "/usr/local/bin/". Or add it to your own path.
brew install php wget
wget https://getcomposer.org/download/latest-stable/composer.phar -O /path/to/your/bin/composer && chmod +x /path/to/your/bin/composer
# [Debian], you need to make sure your php version >= 8.1 and composer >= 2.0
sudo apt install php-cli composer php-tokenizer
# [Alpine]
apk add bash file wget xz php81 php81-common php81-pcntl php81-tokenizer php81-phar php81-posix php81-xml composer
```
::: tip
Currently, some versions of Ubuntu install older PHP versions,
so no installation commands are provided. If necessary, it is recommended to add software sources such as ppa first,
and then install the latest version of PHP and tokenizer, XML, and phar extensions.
Older versions of Debian may have an older (<= 7.4) version of PHP installed by default, it is recommended to upgrade Debian first.
:::
### Use Docker
If you don't want to install PHP and Composer runtime environment on your system, you can use the built-in Docker environment build script.
```bash
# To use directly, replace `bin/spc` with `bin/spc-alpine-docker` in all used commands
bin/spc-alpine-docker
```
The first time the command is executed, `docker build` will be used to build a Docker image.
The default built Docker image is the `x86_64` architecture, and the image name is `cwcc-spc-x86_64`.
If you want to build `aarch64` static-php-cli in `x86_64` environment,
you can use qemu to emulate the arm image to run Docker, but the speed will be very slow.
Use command: `SPC_USE_ARCH=aarch64 bin/spc-alpine-docker`.
If it prompts that sudo is required to run after running,
execute the following command once to grant static-php-cli permission to execute sudo:
```bash
export SPC_USE_SUDO=yes
```
### Use Precompiled Static PHP Binaries
If you don't want to use Docker and install PHP in the system,
@ -133,6 +83,56 @@ This script will download two files in total: `bin/php` and `bin/composer`. Afte
it is equivalent to installing PHP in the system, you can directly Use commands such as `composer`, `php -v`, or directly use `bin/spc`.
2. Direct call, such as executing static-php-cli command: `bin/php bin/spc --help`, executing Composer: `bin/php bin/composer update`.
### Use Docker
If you don't want to install PHP and Composer runtime environment on your system, you can use the built-in Docker environment build script.
```bash
# To use directly, replace `bin/spc` with `bin/spc-alpine-docker` in all used commands
bin/spc-alpine-docker
```
The first time the command is executed, `docker build` will be used to build a Docker image.
The default built Docker image is the `x86_64` architecture, and the image name is `cwcc-spc-x86_64`.
If you want to build `aarch64` static-php-cli in `x86_64` environment,
you can use qemu to emulate the arm image to run Docker, but the speed will be very slow.
Use command: `SPC_USE_ARCH=aarch64 bin/spc-alpine-docker`.
If it prompts that sudo is required to run after running,
execute the following command once to grant static-php-cli permission to execute sudo:
```bash
export SPC_USE_SUDO=yes
```
### Use System PHP
Below are some example commands for installing PHP and Composer in the system.
It is recommended to search for the specific installation method yourself or ask the AI search engine to obtain the answer,
which will not be elaborated here.
```bash
# [macOS], need install Homebrew first. See https://brew.sh/
# Remember change your composer executable path. For M1/M2 Chip mac, "/opt/homebrew/bin/", for Intel mac, "/usr/local/bin/". Or add it to your own path.
brew install php wget
wget https://getcomposer.org/download/latest-stable/composer.phar -O /path/to/your/bin/composer && chmod +x /path/to/your/bin/composer
# [Debian], you need to make sure your php version >= 8.1 and composer >= 2.0
sudo apt install php-cli composer php-tokenizer
# [Alpine]
apk add bash file wget xz php81 php81-common php81-pcntl php81-tokenizer php81-phar php81-posix php81-xml composer
```
::: tip
Currently, some versions of Ubuntu install older PHP versions,
so no installation commands are provided. If necessary, it is recommended to add software sources such as ppa first,
and then install the latest version of PHP and tokenizer, XML, and phar extensions.
Older versions of Debian may have an older (<= 7.4) version of PHP installed by default, it is recommended to upgrade Debian first.
:::
## Command - download
Use the command `bin/spc download` to download the source code required for compilation,
@ -397,6 +397,31 @@ manually unpack and copy the package to a specified location, and we can use com
bin/spc extract php-src,libxml2
```
## Command - dump-extensions
Use the command `bin/spc dump-extensions` to export required extensions of the current project.
```bash
# Print the extension list of the project, pass in the root directory of the project containing composer.json
bin/spc dump-extensions /path/to/your/project/
# Print the extension list of the project, excluding development dependencies
bin/spc dump-extensions /path-to/tour/project/ --no-dev
# Output in the extension list format acceptable to the spc command (comma separated)
bin/spc dump-extensions /path-to/tour/project/ --format=text
# Output as a JSON list
bin/spc dump-extensions /path-to/tour/project/ --format=json
# When the project does not have any extensions, output the specified extension combination instead of returning failure
bin/spc dump-extensions /path-to/your/project/ --no-ext-output=mbstring,posix,pcntl,phar
# Do not exclude extensions not supported by spc when outputting
bin/spc dump-extensions /path/to/your/project/ --no-spc-filter
```
It should be noted that the project directory must contain the `vendor/installed.json` and `composer.lock` files, otherwise they cannot be found normally.
## Dev Command - dev
Debug commands refer to a collection of commands that can assist in outputting some information

View File

@ -8,8 +8,15 @@ here will describe how to check the errors by yourself and report Issue.
Problems with downloading resources are one of the most common problems with spc.
The main reason is that the addresses used for SPC download resources are generally the official website of the corresponding project or GitHub, etc.,
and these websites may occasionally go down and block IP addresses.
Currently, version 2.0.0 has not added an automatic retry mechanism, so after encountering a download failure,
you can try to call the download command multiple times. If you confirm that the address is indeed inaccessible,
After encountering a download failure,
you can try to call the download command multiple times.
When downloading extensions, you may eventually see errors like `curl: (56) The requested URL returned error: 403` which are often caused by github rate limiting.
You can verify this by adding `--debug` to the command and will see something like `[DEBU] Running command (no output) : curl -sfSL "https://api.github.com/repos/openssl/openssl/releases"`.
To fix this, [create](https://github.com/settings/tokens) a personal access token on GitHub and set it as an environment variable `GITHUB_TOKEN=<XXX>`.
If you confirm that the address is indeed inaccessible,
you can submit an Issue or PR to update the url or download type.
## Doctor Can't Fix Something

View File

@ -50,29 +50,29 @@ cd static-php-cli
composer update
```
### 使用系统 PHP 环境
### 使用预编译静态 PHP 二进制运行 static-php-cli
下面是系统安装 PHP、Composer 的一些示例命令。具体安装方式建议自行搜索或询问 AI 搜索引擎获取答案,这里不多赘述。
如果你不想使用 Docker、在系统内安装 PHP可以直接下载本项目自身编译好的 php 二进制 cli 程序。使用流程如下:
```bash
# [macOS], 需要先安装 Homebrew. See https://brew.sh/
# Remember change your composer executable path. For M1/M2 Chip mac, "/opt/homebrew/bin/", for Intel mac, "/usr/local/bin/". Or add it to your own path.
brew install php wget
wget https://getcomposer.org/download/latest-stable/composer.phar -O /path/to/your/bin/composer && chmod +x /path/to/your/bin/composer
# [Debian], you need to make sure your php version >= 8.1 and composer >= 2.0
sudo apt install php-cli composer php-tokenizer
# [Alpine]
apk add bash file wget xz php81 php81-common php81-pcntl php81-tokenizer php81-phar php81-posix php81-xml composer
```
使用命令部署环境,此脚本会从 [自托管的服务器](https://dl.static-php.dev/static-php-cli/) 下载一个当前操作系统的 php-cli 包,
并从 [getcomposer](https://getcomposer.org/download/latest-stable/composer.phar) 或 [Aliyun镜像](https://mirrors.aliyun.com/composer/composer.phar) 下载 Composer。
::: tip
目前 Ubuntu 部分版本的 apt 安装的 php 版本较旧,故不提供安装命令。如有需要,建议先添加 ppa 等软件源后,安装最新版的 PHP 以及 tokenizer、xml、phar 扩展。
较老版本的 Debian 默认安装的可能为旧版本(<= 7.4)版本的 PHP建议先升级 Debian。
使用预编译静态 PHP 二进制目前仅支持 Linux 和 macOS。FreeBSD 环境因为缺少自动化构建环境,所以暂不支持。
:::
```bash
bin/setup-runtime
# 对于中国大陆地区等网络环境特殊的用户,可使用镜像站加快下载速度
bin/setup-runtime --mirror china
```
此脚本总共会下载两个文件:`bin/php``bin/composer`,下载完成后,有两种使用方式:
1. 将 `bin/` 目录添加到 PATH 路径中:`export PATH="/path/to/your/static-php-cli/bin:$PATH"`,添加路径后,相当于系统安装了 PHP可直接使用 `composer``php -v` 等命令,也可以直接使用 `bin/spc`
2. 直接调用,比如执行 static-php-cli 命令:`bin/php bin/spc --help`,执行 Composer`bin/php bin/composer update`
### 使用 Docker 环境
如果你不愿意在系统安装 PHP 和 Composer 运行环境,可以使用内置的 Docker 环境构建脚本。
@ -92,28 +92,25 @@ bin/spc-alpine-docker
export SPC_USE_SUDO=yes
```
### 使用预编译静态 PHP 二进制
### 使用系统 PHP 环境
如果你不想使用 Docker、在系统内安装 PHP可以直接下载本项目自身编译好的 php 二进制 cli 程序。使用流程如下:
使用命令部署环境,此脚本会从 [自托管的服务器](https://dl.static-php.dev/static-php-cli/) 下载一个当前操作系统的 php-cli 包,
并从 [getcomposer](https://getcomposer.org/download/latest-stable/composer.phar) 或 [Aliyun镜像](https://mirrors.aliyun.com/composer/composer.phar) 下载 Composer。
::: tip
使用预编译静态 PHP 二进制目前仅支持 Linux 和 macOS。FreeBSD 环境因为缺少自动化构建环境,所以暂不支持。
:::
下面是系统安装 PHP、Composer 的一些示例命令。具体安装方式建议自行搜索或询问 AI 搜索引擎获取答案,这里不多赘述。
```bash
bin/setup-runtime
# [macOS], 需要先安装 Homebrew. See https://brew.sh/
# Remember change your composer executable path. For M1/M2 Chip mac, "/opt/homebrew/bin/", for Intel mac, "/usr/local/bin/". Or add it to your own path.
brew install php wget
wget https://getcomposer.org/download/latest-stable/composer.phar -O /path/to/your/bin/composer && chmod +x /path/to/your/bin/composer
# 对于中国大陆地区等网络环境特殊的用户,可使用镜像站加快下载速度
bin/setup-runtime --mirror china
# [Debian], you need to make sure your php version >= 8.4 and composer >= 2.0
sudo apt install php-cli composer php-tokenizer
```
此脚本总共会下载两个文件:`bin/php``bin/composer`,下载完成后,有两种使用方式:
::: tip
目前 Ubuntu 部分版本的 apt 安装的 php 版本较旧,故不提供安装命令。如有需要,建议先添加 ppa 等软件源后,安装最新版的 PHP 以及 tokenizer、xml、phar 扩展。
1. 将 `bin/` 目录添加到 PATH 路径中:`export PATH="/path/to/your/static-php-cli/bin:$PATH"`,添加路径后,相当于系统安装了 PHP可直接使用 `composer``php -v` 等命令,也可以直接使用 `bin/spc`
2. 直接调用,比如执行 static-php-cli 命令:`bin/php bin/spc --help`,执行 Composer`bin/php bin/composer update`
较老版本的 Debian 默认安装的可能为旧版本(<= 8.3)版本的 PHP建议先升级 Debian 或使用 Docker 或自带的静态二进制环境
:::
## 命令 download - 下载依赖包
@ -353,6 +350,32 @@ memory_limit=1G
bin/spc extract php-src,libxml2
```
## 命令 dump-extensions - 导出项目扩展依赖
使用命令 `bin/spc dump-extensions` 可以导出当前项目的扩展依赖。
```bash
# 打印项目的扩展列表传入项目包含composer.json的根目录
bin/spc dump-extensions /path/to/your/project/
# 打印项目的扩展列表,不包含开发依赖
bin/spc dump-extensions /path-to/tour/project/ --no-dev
# 输出为 spc 命令可接受的扩展列表格式(逗号分割)
bin/spc dump-extensions /path-to/tour/project/ --format=text
# 输出为 JSON 列表
bin/spc dump-extensions /path-to/tour/project/ --format=json
# 当项目没有任何扩展时,输出指定扩展组合,而不是返回失败
bin/spc dump-extensions /path-to/your/project/ --no-ext-output=mbstring,posix,pcntl,phar
# 输出时不排除 spc 不支持的扩展
bin/spc dump-extensions /path/to/your/project/ --no-spc-filter
```
需要注意的是,项目的目录下必须包含 `vendor/installed.json``composer.lock` 文件,否则无法正常获取。
## 调试命令 dev - 调试命令集合
调试命令指的是你在使用 static-php-cli 构建 PHP 或改造、增强 static-php-cli 项目本身的时候,可以辅助输出一些信息的命令集合。

View File

@ -5,7 +5,14 @@
## 下载失败问题
下载资源问题是 spc 最常见的问题之一。主要是由于 spc 下载资源使用的地址一般均为对应项目的官方网站或 GitHub 等,而这些网站可能偶尔会宕机、屏蔽 IP 地址。
目前 2.0.0 版本还没有加入自动重试机制,所以在遇到下载失败后,可以多次尝试调用下载命令。如果确认地址确实无法正常访问,可以提交 Issue 或 PR 更新地址。
在遇到下载失败后,可以多次尝试调用下载命令。
当下载资源时,你可能最终会看到类似 `curl: (56) The requested URL returned error: 403` 的错误,这通常是由于 GitHub 限制导致的。
你可以通过在命令中添加 `--debug` 来验证,会看到类似 `[DEBU] Running command (no output) : curl -sfSL "https://api.github.com/repos/openssl/openssl/releases"` 的输出。
要解决这个问题,可以在 GitHub 上 [创建](https://github.com/settings/token) 一个个人访问令牌,并将其设置为环境变量 `GITHUB_TOKEN=<XXX>`
如果确认地址确实无法正常访问,可以提交 Issue 或 PR 更新地址。
## doctor 无法修复

View File

@ -18,6 +18,7 @@ use SPC\command\dev\PhpVerCommand;
use SPC\command\dev\SortConfigCommand;
use SPC\command\DoctorCommand;
use SPC\command\DownloadCommand;
use SPC\command\DumpExtensionsCommand;
use SPC\command\DumpLicenseCommand;
use SPC\command\ExtractCommand;
use SPC\command\InstallPkgCommand;
@ -31,7 +32,7 @@ use Symfony\Component\Console\Application;
*/
final class ConsoleApplication extends Application
{
public const VERSION = '2.4.4';
public const VERSION = '2.5.0';
public function __construct()
{
@ -54,6 +55,7 @@ final class ConsoleApplication extends Application
new MicroCombineCommand(),
new SwitchPhpVersionCommand(),
new SPCConfigCommand(),
new DumpExtensionsCommand(),
// Dev commands
new AllExtCommand(),

View File

@ -172,7 +172,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
// If check failed, throw RuntimeException
[$ret] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php --ri "' . $this->getDistName() . '"', false);
[$ret] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -n --ri "' . $this->getDistName() . '"', false);
if ($ret !== 0) {
throw new RuntimeException('extension ' . $this->getName() . ' failed compile check: php-cli returned ' . $ret);
}
@ -185,7 +185,7 @@ class Extension
file_get_contents(ROOT_DIR . '/src/globals/ext-tests/' . $this->getName() . '.php')
);
[$ret, $out] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -r "' . trim($test) . '"');
[$ret, $out] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -n -r "' . trim($test) . '"');
if ($ret !== 0) {
if ($this->builder->getOption('debug')) {
var_dump($out);
@ -203,7 +203,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
// If check failed, throw RuntimeException
[$ret] = cmd()->execWithResult(BUILD_ROOT_PATH . '/bin/php.exe --ri "' . $this->getDistName() . '"', false);
[$ret] = cmd()->execWithResult(BUILD_ROOT_PATH . '/bin/php.exe -n --ri "' . $this->getDistName() . '"', false);
if ($ret !== 0) {
throw new RuntimeException('extension ' . $this->getName() . ' failed compile check: php-cli returned ' . $ret);
}
@ -216,7 +216,7 @@ class Extension
file_get_contents(FileSystem::convertPath(ROOT_DIR . '/src/globals/ext-tests/' . $this->getName() . '.php'))
);
[$ret] = cmd()->execWithResult(BUILD_ROOT_PATH . '/bin/php.exe -r "' . trim($test) . '"');
[$ret] = cmd()->execWithResult(BUILD_ROOT_PATH . '/bin/php.exe -n -r "' . trim($test) . '"');
if ($ret !== 0) {
throw new RuntimeException('extension ' . $this->getName() . ' failed sanity check');
}

View File

@ -146,6 +146,17 @@ abstract class LibraryBase
return Config::getLib(static::NAME, 'headers', []);
}
/**
* Get binary files.
*
* @throws FileSystemException
* @throws WrongUsageException
*/
public function getBinaryFiles(): array
{
return Config::getLib(static::NAME, 'bin', []);
}
/**
* @throws WrongUsageException
* @throws FileSystemException
@ -203,7 +214,8 @@ abstract class LibraryBase
}
// force means just build
if ($force_build) {
logger()->info('Building required library [' . static::NAME . ']');
$type = Config::getLib(static::NAME, 'type', 'lib');
logger()->info('Building required ' . $type . ' [' . static::NAME . ']');
// extract first if not exists
if (!is_dir($this->source_dir)) {
@ -236,10 +248,14 @@ abstract class LibraryBase
return LIB_STATUS_OK;
}
}
// pkg-config is treated specially. If it is pkg-config, check if the pkg-config binary exists
if (static::NAME === 'pkg-config' && !file_exists(BUILD_ROOT_PATH . '/bin/pkg-config')) {
$this->tryBuild(true);
return LIB_STATUS_OK;
// current library is package and binary file is not exists
if (Config::getLib(static::NAME, 'type', 'lib') === 'package') {
foreach ($this->getBinaryFiles() as $name) {
if (!file_exists(BUILD_BIN_PATH . "/{$name}")) {
$this->tryBuild(true);
return LIB_STATUS_OK;
}
}
}
// if all the files exist at this point, skip the compilation process
return LIB_STATUS_ALREADY;

View File

@ -26,7 +26,7 @@ class mbregex extends Extension
*/
public function runCliCheckUnix(): void
{
[$ret] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php --ri "mbstring" | grep regex', false);
[$ret] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -n --ri "mbstring" | grep regex', false);
if ($ret !== 0) {
throw new RuntimeException('extension ' . $this->getName() . ' failed compile check: compiled php-cli mbstring extension does not contain regex !');
}
@ -34,7 +34,7 @@ class mbregex extends Extension
public function runCliCheckWindows(): void
{
[$ret, $out] = cmd()->execWithResult(BUILD_ROOT_PATH . '/bin/php --ri "mbstring"', false);
[$ret, $out] = cmd()->execWithResult(BUILD_ROOT_PATH . '/bin/php -n --ri "mbstring"', false);
if ($ret !== 0) {
throw new RuntimeException('extension ' . $this->getName() . ' failed compile check: compiled php-cli does not contain mbstring !');
}

View File

@ -13,6 +13,7 @@ class memcached extends Extension
public function getUnixConfigureArg(): string
{
$rootdir = BUILD_ROOT_PATH;
return "--enable-memcached --with-zlib-dir={$rootdir} --with-libmemcached-dir={$rootdir} --disable-memcached-sasl --enable-memcached-json";
$zlib_dir = $this->builder->getPHPVersionID() >= 80400 ? '' : "--with-zlib-dir={$rootdir}";
return "--enable-memcached {$zlib_dir} --with-libmemcached-dir={$rootdir} --disable-memcached-sasl --enable-memcached-json";
}
}

View File

@ -25,6 +25,7 @@ class openssl extends Extension
public function getUnixConfigureArg(): string
{
return '--with-openssl=' . BUILD_ROOT_PATH . ' --with-openssl-dir=' . BUILD_ROOT_PATH;
$openssl_dir = $this->builder->getPHPVersionID() >= 80400 ? '' : ' --with-openssl-dir=' . BUILD_ROOT_PATH;
return '--with-openssl=' . BUILD_ROOT_PATH . $openssl_dir;
}
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace SPC\builder\extension;
use SPC\builder\Extension;
use SPC\store\FileSystem;
use SPC\util\CustomExt;
use SPC\util\GlobalEnvManager;
#[CustomExt('opentelemetry')]
class opentelemetry extends Extension
{
public function validate(): void
{
if ($this->builder->getPHPVersionID() < 80000 && getenv('SPC_SKIP_PHP_VERSION_CHECK') !== 'yes') {
throw new \RuntimeException('The opentelemetry extension requires PHP 8.0 or later');
}
}
public function patchBeforeBuildconf(): bool
{
if (PHP_OS_FAMILY === 'Windows') {
FileSystem::replaceFileStr(
SOURCE_PATH . '/php-src/ext/opentelemetry/config.w32',
"EXTENSION('opentelemetry', 'opentelemetry.c otel_observer.c', '/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1');",
"EXTENSION('opentelemetry', 'opentelemetry.c otel_observer.c', PHP_OPENTELEMETRY_SHARED, '/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1');"
);
return true;
}
return false;
}
public function patchBeforeMake(): bool
{
// add -Wno-strict-prototypes
GlobalEnvManager::putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS=' . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') . ' -Wno-strict-prototypes');
return true;
}
}

View File

@ -18,7 +18,7 @@ class password_argon2 extends Extension
public function runCliCheckUnix(): void
{
[$ret] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -r "assert(defined(\'PASSWORD_ARGON2I\'));"');
[$ret] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -n -r "assert(defined(\'PASSWORD_ARGON2I\'));"');
if ($ret !== 0) {
throw new RuntimeException('extension ' . $this->getName() . ' failed sanity check');
}

View File

@ -29,7 +29,7 @@ class swoole_hook_mysql extends Extension
if ($this->builder->getExt('swoole') === null) {
return;
}
[$ret, $out] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php --ri "swoole"', false);
[$ret, $out] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -n --ri "swoole"', false);
$out = implode('', $out);
if ($ret !== 0) {
throw new RuntimeException('extension ' . $this->getName() . ' failed compile check: php-cli returned ' . $ret);

View File

@ -37,7 +37,7 @@ class swoole_hook_pgsql extends Extension
if ($this->builder->getExt('swoole') === null) {
return;
}
[$ret, $out] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php --ri "swoole"', false);
[$ret, $out] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -n --ri "swoole"', false);
$out = implode('', $out);
if ($ret !== 0) {
throw new RuntimeException('extension ' . $this->getName() . ' failed compile check: php-cli returned ' . $ret);

View File

@ -37,7 +37,7 @@ class swoole_hook_sqlite extends Extension
if ($this->builder->getExt('swoole') === null) {
return;
}
[$ret, $out] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php --ri "swoole"', false);
[$ret, $out] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -n --ri "swoole"', false);
$out = implode('', $out);
if ($ret !== 0) {
throw new RuntimeException('extension ' . $this->getName() . ' failed compile check: php-cli returned ' . $ret);

View File

@ -12,9 +12,7 @@ class zlib extends Extension
{
public function getUnixConfigureArg(): string
{
if ($this->builder->getPHPVersionID() >= 80400) {
return '--with-zlib';
}
return '--with-zlib --with-zlib-dir="' . BUILD_ROOT_PATH . '"';
$zlib_dir = $this->builder->getPHPVersionID() >= 80400 ? '' : ' --with-zlib-dir=' . BUILD_ROOT_PATH;
return '--with-zlib' . $zlib_dir;
}
}

View File

@ -34,14 +34,14 @@ class LinuxBuilder extends UnixBuilderBase
// check musl-cross make installed if we use musl-cross-make
$arch = arch2gnu(php_uname('m'));
if ($this->libc !== LIBC_GLIBC) {
// set library path, some libraries need it. (We cannot use `putenv` here, because cmake will be confused)
$this->setOptionIfNotExist('library_path', "LIBRARY_PATH=/usr/local/musl/{$arch}-linux-musl/lib");
$this->setOptionIfNotExist('ld_library_path', "LD_LIBRARY_PATH=/usr/local/musl/{$arch}-linux-musl/lib");
}
GlobalEnvManager::init($this);
// set library path, some libraries need it. (We cannot use `putenv` here, because cmake will be confused)
if (!filter_var(getenv('SPC_NO_MUSL_PATH'), FILTER_VALIDATE_BOOLEAN)) {
$this->setOptionIfNotExist('library_path', "LIBRARY_PATH=\"/usr/local/musl/{$arch}-linux-musl/lib\"");
$this->setOptionIfNotExist('ld_library_path', "LD_LIBRARY_PATH=\"/usr/local/musl/{$arch}-linux-musl/lib\"");
}
if (str_ends_with(getenv('CC'), 'linux-musl-gcc') && !file_exists("/usr/local/musl/bin/{$arch}-linux-musl-gcc") && (getenv('SPC_NO_MUSL_PATH') !== 'yes')) {
throw new WrongUsageException('musl-cross-make not installed, please install it first. (You can use `doctor` command to install it)');
}

View File

@ -110,13 +110,11 @@ abstract class UnixBuilderBase extends BuilderBase
$sorted_libraries = DependencyUtil::getLibs($libraries);
}
// pkg-config must be compiled first, whether it is specified or not
if (!in_array('pkg-config', $sorted_libraries)) {
array_unshift($sorted_libraries, 'pkg-config');
}
// add lib object for builder
foreach ($sorted_libraries as $library) {
if (!in_array(Config::getLib($library, 'type', 'lib'), ['lib', 'package'])) {
continue;
}
// if some libs are not supported (but in config "lib.json", throw exception)
if (!isset($support_lib_list[$library])) {
throw new WrongUsageException('library [' . $library . '] is in the lib.json list but not supported to compile, but in the future I will support it!');
@ -142,9 +140,10 @@ abstract class UnixBuilderBase extends BuilderBase
// sanity check for php-cli
if (($build_target & BUILD_TARGET_CLI) === BUILD_TARGET_CLI) {
logger()->info('running cli sanity check');
[$ret, $output] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -r "echo \"hello\";"');
if ($ret !== 0 || trim(implode('', $output)) !== 'hello') {
throw new RuntimeException('cli failed sanity check');
[$ret, $output] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -n -r "echo \"hello\";"');
$raw_output = implode('', $output);
if ($ret !== 0 || trim($raw_output) !== 'hello') {
throw new RuntimeException("cli failed sanity check: ret[{$ret}]. out[{$raw_output}]");
}
foreach ($this->exts as $ext) {

View File

@ -10,14 +10,21 @@ trait gettext
{
$extra = $this->builder->getLib('ncurses') ? ('--with-libncurses-prefix=' . BUILD_ROOT_PATH . ' ') : '';
$extra .= $this->builder->getLib('libxml2') ? ('--with-libxml2-prefix=' . BUILD_ROOT_PATH . ' ') : '';
$zts = $this->builder->getOption('enable-zts') ? '--enable-threads=isoc+posix ' : '--disable-threads ';
$cflags = $this->builder->getOption('enable-zts') ? '-lpthread -D_REENTRANT' : '';
$ldflags = $this->builder->getOption('enable-zts') ? '-lpthread' : '';
shell()->cd($this->source_dir)
->setEnv(['CFLAGS' => $this->getLibExtraCFlags(), 'LDFLAGS' => $this->getLibExtraLdFlags(), 'LIBS' => $this->getLibExtraLibs()])
->setEnv(['CFLAGS' => $this->getLibExtraCFlags() ?: $cflags, 'LDFLAGS' => $this->getLibExtraLdFlags() ?: $ldflags, 'LIBS' => $this->getLibExtraLibs()])
->execWithEnv(
'./configure ' .
'--enable-static ' .
'--disable-shared ' .
'--disable-java ' .
'--disable-c+ ' .
$zts .
$extra .
'--with-included-gettext ' .
'--with-libiconv-prefix=' . BUILD_ROOT_PATH . ' ' .

View File

@ -236,6 +236,9 @@ class WindowsBuilder extends BuilderBase
// add lib object for builder
foreach ($sorted_libraries as $library) {
if (!in_array(Config::getLib($library, 'type', 'lib'), ['lib', 'package'])) {
continue;
}
// if some libs are not supported (but in config "lib.json", throw exception)
if (!isset($support_lib_list[$library])) {
throw new WrongUsageException('library [' . $library . '] is in the lib.json list but not supported to compile, but in the future I will support it!');
@ -277,7 +280,7 @@ class WindowsBuilder extends BuilderBase
// sanity check for php-cli
if (($build_target & BUILD_TARGET_CLI) === BUILD_TARGET_CLI) {
logger()->info('running cli sanity check');
[$ret, $output] = cmd()->execWithResult(BUILD_ROOT_PATH . '\bin\php.exe -r "echo \"hello\";"');
[$ret, $output] = cmd()->execWithResult(BUILD_ROOT_PATH . '\bin\php.exe -n -r "echo \"hello\";"');
if ($ret !== 0 || trim(implode('', $output)) !== 'hello') {
throw new RuntimeException('cli failed sanity check');
}

View File

@ -12,14 +12,41 @@ class curl extends WindowsLibraryBase
protected function build(): void
{
FileSystem::createDir(BUILD_BIN_PATH);
cmd()->cd($this->source_dir . '\winbuild')
// reset cmake
FileSystem::resetDir($this->source_dir . '\cmakebuild');
// lib:zstd
$alt = $this->builder->getLib('zstd') ? '' : '-DCURL_ZSTD=OFF';
// lib:brotli
$alt .= $this->builder->getLib('brotli') ? '' : ' -DCURL_BROTLI=OFF';
// start build
cmd()->cd($this->source_dir)
->execWithWrapper(
$this->builder->makeSimpleWrapper('nmake'),
'/f Makefile.vc WITH_DEVEL=' . BUILD_ROOT_PATH . ' ' .
'WITH_PREFIX=' . BUILD_ROOT_PATH . ' ' .
'mode=static RTLIBCFG=static WITH_SSL=static WITH_NGHTTP2=static WITH_SSH2=static ENABLE_IPV6=yes WITH_ZLIB=static MACHINE=x64 DEBUG=no'
$this->builder->makeSimpleWrapper('cmake'),
'-B cmakebuild ' .
'-A x64 ' .
"-DCMAKE_TOOLCHAIN_FILE={$this->builder->cmake_toolchain_file} " .
'-DCMAKE_BUILD_TYPE=Release ' .
'-DBUILD_SHARED_LIBS=OFF ' .
'-DBUILD_STATIC_LIBS=ON ' .
'-DCURL_STATICLIB=ON ' .
'-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' .
'-DBUILD_CURL_EXE=OFF ' . // disable curl.exe
'-DBUILD_TESTING=OFF ' . // disable tests
'-DBUILD_EXAMPLES=OFF ' . // disable examples
'-DUSE_LIBIDN2=OFF ' . // disable libidn2
'-DCURL_USE_LIBPSL=OFF ' . // disable libpsl
'-DCURL_ENABLE_SSL=ON ' .
'-DUSE_NGHTTP2=ON ' . // enable nghttp2
'-DCURL_USE_LIBSSH2=ON ' . // enable libssh2
'-DENABLE_IPV6=ON ' . // enable ipv6
'-DNGHTTP2_CFLAGS="/DNGHTTP2_STATICLIB" ' .
$alt
)
->execWithWrapper(
$this->builder->makeSimpleWrapper('cmake'),
"--build cmakebuild --config Release --target install -j{$this->builder->concurrency}"
);
FileSystem::copyDir($this->source_dir . '\include\curl', BUILD_INCLUDE_PATH . '\curl');
}
}

View File

@ -46,7 +46,6 @@ abstract class BaseCommand extends Command
E_USER_ERROR => ['PHP Error: ', 'error'],
E_USER_WARNING => ['PHP Warning: ', 'warning'],
E_USER_NOTICE => ['PHP Notice: ', 'notice'],
E_STRICT => ['PHP Strict: ', 'notice'],
E_RECOVERABLE_ERROR => ['PHP Recoverable Error: ', 'error'],
E_DEPRECATED => ['PHP Deprecated: ', 'notice'],
E_USER_DEPRECATED => ['PHP User Deprecated: ', 'notice'],
@ -56,7 +55,7 @@ abstract class BaseCommand extends Command
logger()->{$level_tip[1]}($error);
// 如果 return false 则错误会继续递交给 PHP 标准错误处理
return true;
}, E_ALL | E_STRICT);
});
$version = ConsoleApplication::VERSION;
if (!$this->no_motd) {
echo " _ _ _ _
@ -154,24 +153,24 @@ abstract class BaseCommand extends Command
/**
* Parse extension list from string, replace alias and filter internal extensions.
*
* @param string $ext_list Extension string list, e.g. "mbstring,posix,sockets"
* @param array|string $ext_list Extension string list, e.g. "mbstring,posix,sockets" or array
*/
protected function parseExtensionList(string $ext_list): array
protected function parseExtensionList(array|string $ext_list): array
{
// replace alias
$ls = array_map(function ($x) {
$lower = strtolower(trim($x));
if (isset(SPC_EXTENSION_ALIAS[$lower])) {
logger()->notice("Extension [{$lower}] is an alias of [" . SPC_EXTENSION_ALIAS[$lower] . '], it will be replaced.');
logger()->debug("Extension [{$lower}] is an alias of [" . SPC_EXTENSION_ALIAS[$lower] . '], it will be replaced.');
return SPC_EXTENSION_ALIAS[$lower];
}
return $lower;
}, explode(',', $ext_list));
}, is_array($ext_list) ? $ext_list : explode(',', $ext_list));
// filter internals
return array_values(array_filter($ls, function ($x) {
if (in_array($x, SPC_INTERNAL_EXTENSIONS)) {
logger()->warning("Extension [{$x}] is an builtin extension, it will be ignored.");
logger()->debug("Extension [{$x}] is an builtin extension, it will be ignored.");
return false;
}
return true;

View File

@ -7,6 +7,7 @@ namespace SPC\command;
use SPC\builder\BuilderProvider;
use SPC\exception\ExceptionHandler;
use SPC\exception\WrongUsageException;
use SPC\store\Config;
use SPC\store\FileSystem;
use SPC\store\SourcePatcher;
use SPC\util\DependencyUtil;
@ -32,7 +33,6 @@ class BuildCliCommand extends BuildCommand
$this->addOption('build-embed', null, null, 'Build embed SAPI');
$this->addOption('build-all', null, null, 'Build all SAPI');
$this->addOption('no-strip', null, null, 'build without strip, in order to debug and load external extensions');
$this->addOption('enable-zts', null, null, 'enable ZTS support');
$this->addOption('disable-opcache-jit', null, null, 'disable opcache jit');
$this->addOption('with-config-file-path', null, InputOption::VALUE_REQUIRED, 'Set the path in which to look for php.ini', $isWindows ? null : '/usr/local/etc/php');
$this->addOption('with-config-file-scan-dir', null, InputOption::VALUE_REQUIRED, 'Set the directory to scan for .ini files after reading php.ini', $isWindows ? null : '/usr/local/etc/php/conf.d');
@ -108,13 +108,14 @@ class BuildCliCommand extends BuildCommand
$include_suggest_ext = $this->getOption('with-suggested-exts');
$include_suggest_lib = $this->getOption('with-suggested-libs');
[$extensions, $libraries, $not_included] = DependencyUtil::getExtsAndLibs($extensions, $libraries, $include_suggest_ext, $include_suggest_lib);
$display_libs = array_filter($libraries, fn ($lib) => in_array(Config::getLib($lib, 'type', 'lib'), ['lib', 'package']));
// print info
$indent_texts = [
'Build OS' => PHP_OS_FAMILY . ' (' . php_uname('m') . ')',
'Build SAPI' => $builder->getBuildTypeName($rule),
'Extensions (' . count($extensions) . ')' => implode(',', $extensions),
'Libraries (' . count($libraries) . ')' => implode(',', $libraries),
'Libraries (' . count($libraries) . ')' => implode(',', $display_libs),
'Strip Binaries' => $builder->getOption('no-strip') ? 'no' : 'yes',
'Enable ZTS' => $builder->getOption('enable-zts') ? 'yes' : 'no',
];

View File

@ -33,5 +33,6 @@ abstract class BuildCommand extends BaseCommand
$this->addOption('with-clean', null, null, 'fresh build, remove `source` dir before `make`');
$this->addOption('bloat', null, null, 'add all libraries into binary');
$this->addOption('rebuild', 'r', null, 'Delete old build and rebuild');
$this->addOption('enable-zts', null, null, 'enable ZTS support');
}
}

View File

@ -7,6 +7,7 @@ namespace SPC\command;
use SPC\builder\BuilderProvider;
use SPC\exception\ExceptionHandler;
use SPC\exception\RuntimeException;
use SPC\store\Config;
use SPC\util\DependencyUtil;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
@ -61,7 +62,9 @@ class BuildLibsCommand extends BuildCommand
$builder->setLibsOnly();
// 编译和检查库完整
$libraries = DependencyUtil::getLibs($libraries);
logger()->info('Building libraries: ' . implode(',', $libraries));
$display_libs = array_filter($libraries, fn ($lib) => in_array(Config::getLib($lib, 'type', 'lib'), ['lib', 'package']));
logger()->info('Building libraries: ' . implode(',', $display_libs));
sleep(2);
$builder->proveLibs($libraries);
$builder->validateLibsAndExts();

View File

@ -72,10 +72,6 @@ class DownloadCommand extends BaseCommand
if ($for_ext = $input->getOption('for-extensions')) {
$ext = $this->parseExtensionList($for_ext);
$sources = $this->calculateSourcesByExt($ext, !$input->getOption('without-suggestions'));
if (PHP_OS_FAMILY !== 'Windows') {
array_unshift($sources, 'pkg-config');
}
array_unshift($sources, 'php-src', 'micro');
$final_sources = array_merge($final_sources, array_diff($sources, $final_sources));
}
// mode: --for-libs
@ -323,7 +319,10 @@ class DownloadCommand extends BaseCommand
}
}
foreach ($libraries as $library) {
$sources[] = Config::getLib($library, 'source');
$source = Config::getLib($library, 'source');
if ($source !== null) {
$sources[] = $source;
}
}
return array_values(array_unique($sources));
}

View File

@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
namespace SPC\command;
use SPC\store\FileSystem;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand(name: 'dump-extensions', description: 'Determines the required php extensions')]
class DumpExtensionsCommand extends BaseCommand
{
protected bool $no_motd = true;
public function configure(): void
{
// path to project files or specific composer file
$this->addArgument('path', InputArgument::OPTIONAL, 'Path to project root', '.');
$this->addOption('format', 'F', InputOption::VALUE_REQUIRED, 'Parsed output format', 'default');
// output zero extension replacement rather than exit as failure
$this->addOption('no-ext-output', 'N', InputOption::VALUE_REQUIRED, 'When no extensions found, output default combination (comma separated)');
// no dev
$this->addOption('no-dev', null, null, 'Do not include dev dependencies');
// no spc filter
$this->addOption('no-spc-filter', 'S', null, 'Do not use SPC filter to determine the required extensions');
}
public function handle(): int
{
$path = FileSystem::convertPath($this->getArgument('path'));
$path_installed = FileSystem::convertPath(rtrim($path, '/\\') . '/vendor/composer/installed.json');
$path_lock = FileSystem::convertPath(rtrim($path, '/\\') . '/composer.lock');
$ext_installed = $this->extractFromInstalledJson($path_installed, !$this->getOption('no-dev'));
if ($ext_installed === null) {
if ($this->getOption('format') === 'default') {
$this->output->writeln('<comment>vendor/composer/installed.json load failed, skipped</comment>');
}
$ext_installed = [];
}
$ext_lock = $this->extractFromComposerLock($path_lock, !$this->getOption('no-dev'));
if ($ext_lock === null) {
$this->output->writeln('<error>composer.lock load failed</error>');
return static::FAILURE;
}
$extensions = array_unique(array_merge($ext_installed, $ext_lock));
sort($extensions);
if (empty($extensions)) {
if ($this->getOption('no-ext-output')) {
$this->outputExtensions(explode(',', $this->getOption('no-ext-output')));
return static::SUCCESS;
}
$this->output->writeln('<error>No extensions found</error>');
return static::FAILURE;
}
$this->outputExtensions($extensions);
return static::SUCCESS;
}
private function filterExtensions(array $requirements): array
{
return array_map(
fn ($key) => substr($key, 4),
array_keys(
array_filter($requirements, function ($key) {
return str_starts_with($key, 'ext-');
}, ARRAY_FILTER_USE_KEY)
)
);
}
private function loadJson(string $file): array|bool
{
if (!file_exists($file)) {
return false;
}
$data = json_decode(file_get_contents($file), true);
if (!$data) {
return false;
}
return $data;
}
private function extractFromInstalledJson(string $file, bool $include_dev = true): ?array
{
if (!($data = $this->loadJson($file))) {
return null;
}
$packages = $data['packages'] ?? [];
if (!$include_dev) {
$packages = array_filter($packages, fn ($package) => !in_array($package['name'], $data['dev-package-names'] ?? []));
}
return array_merge(
...array_map(fn ($x) => isset($x['require']) ? $this->filterExtensions($x['require']) : [], $packages)
);
}
private function extractFromComposerLock(string $file, bool $include_dev = true): ?array
{
if (!($data = $this->loadJson($file))) {
return null;
}
// get packages ext
$packages = $data['packages'] ?? [];
$exts = array_merge(
...array_map(fn ($package) => $this->filterExtensions($package['require'] ?? []), $packages)
);
// get dev packages ext
if ($include_dev) {
$packages = $data['packages-dev'] ?? [];
$exts = array_merge(
$exts,
...array_map(fn ($package) => $this->filterExtensions($package['require'] ?? []), $packages)
);
}
// get require ext
$platform = $data['platform'] ?? [];
$exts = array_merge($exts, $this->filterExtensions($platform));
// get require-dev ext
if ($include_dev) {
$platform = $data['platform-dev'] ?? [];
$exts = array_merge($exts, $this->filterExtensions($platform));
}
return $exts;
}
private function outputExtensions(array $extensions): void
{
if (!$this->getOption('no-spc-filter')) {
$extensions = $this->parseExtensionList($extensions);
}
switch ($this->getOption('format')) {
case 'json':
$this->output->writeln(json_encode($extensions, JSON_PRETTY_PRINT));
break;
case 'text':
$this->output->writeln(implode(',', $extensions));
break;
default:
$this->output->writeln('<info>Required PHP extensions' . ($this->getOption('no-dev') ? ' (without dev)' : '') . ':</info>');
$this->output->writeln(implode(',', $extensions));
}
}
}

View File

@ -33,7 +33,17 @@ class SortConfigCommand extends BaseCommand
case 'lib':
$file = json_decode(FileSystem::readFile(ROOT_DIR . '/config/lib.json'), true);
ConfigValidator::validateLibs($file);
ksort($file);
uksort($file, function ($a, $b) use ($file) {
$type_a = $file[$a]['type'] ?? 'lib';
$type_b = $file[$b]['type'] ?? 'lib';
$type_order = ['root', 'target', 'package', 'lib'];
// compare type first
if ($type_a !== $type_b) {
return array_search($type_a, $type_order) <=> array_search($type_b, $type_order);
}
// compare name
return $a <=> $b;
});
if (!file_put_contents(ROOT_DIR . '/config/lib.json', json_encode($file, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n")) {
$this->output->writeln('<error>Write file lib.json failed!</error>');
return static::FAILURE;

View File

@ -73,7 +73,7 @@ class Config
if (!isset(self::$lib[$name])) {
throw new WrongUsageException('lib [' . $name . '] is not supported yet');
}
$supported_sys_based = ['static-libs', 'headers', 'lib-depends', 'lib-suggests', 'frameworks'];
$supported_sys_based = ['static-libs', 'headers', 'lib-depends', 'lib-suggests', 'frameworks', 'bin'];
if ($key !== null && in_array($key, $supported_sys_based)) {
$m_key = match (PHP_OS_FAMILY) {
'Windows' => ['-windows', '-win', ''],

View File

@ -53,13 +53,45 @@ class ConfigValidator
*/
public static function validateLibs(mixed $data, array $source_data = []): void
{
is_array($data) || throw new ValidationException('lib.json is broken');
// check if it is an array
if (!is_array($data)) {
throw new ValidationException('lib.json is broken');
}
// check each lib
foreach ($data as $name => $lib) {
isset($lib['source']) || throw new ValidationException("lib {$name} does not assign any source");
is_string($lib['source']) || throw new ValidationException("lib {$name} source must be string");
empty($source_data) || isset($source_data[$lib['source']]) || throw new ValidationException("lib {$name} assigns an invalid source: {$lib['source']}");
!isset($lib['lib-depends']) || !is_assoc_array($lib['lib-depends']) || throw new ValidationException("lib {$name} dependencies must be a list");
!isset($lib['lib-suggests']) || !is_assoc_array($lib['lib-suggests']) || throw new ValidationException("lib {$name} suggested dependencies must be a list");
// check if lib is an assoc array
if (!is_assoc_array($lib)) {
throw new ValidationException("lib {$name} is not an object");
}
// check if lib has valid type
if (!in_array($lib['type'] ?? 'lib', ['lib', 'package', 'target', 'root'])) {
throw new ValidationException("lib {$name} type is invalid");
}
// check if lib and package has source
if (in_array($lib['type'] ?? 'lib', ['lib', 'package']) && !isset($lib['source'])) {
throw new ValidationException("lib {$name} does not assign any source");
}
// check if source is valid
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 [lib-depends|lib-suggests|static-libs][-windows|-unix|-macos|-linux] are valid list array
$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");
}
}
// 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");
}
}
}

View File

@ -33,7 +33,7 @@ class DependencyUtil
$ext_suggests = array_map(fn ($x) => "ext@{$x}", $ext_suggests);
// merge ext-depends with lib-depends
$lib_depends = Config::getExt($ext_name, 'lib-depends', []);
$depends = array_merge($ext_depends, $lib_depends);
$depends = array_merge($ext_depends, $lib_depends, ['php']);
// merge ext-suggests with lib-suggests
$lib_suggests = Config::getExt($ext_name, 'lib-suggests', []);
$suggests = array_merge($ext_suggests, $lib_suggests);
@ -44,7 +44,7 @@ class DependencyUtil
}
foreach ($libs as $lib_name => $lib) {
$dep_list[$lib_name] = [
'depends' => Config::getLib($lib_name, 'lib-depends', []),
'depends' => array_merge(Config::getLib($lib_name, 'lib-depends', []), ['lib-base']),
'suggests' => Config::getLib($lib_name, 'lib-suggests', []),
];
}
@ -210,6 +210,9 @@ class DependencyUtil
}
$visited[$lib_name] = true;
// 遍历该依赖的所有依赖(此处的 getLib 如果检测到当前库不存在的话,会抛出异常)
if (!isset($dep_list[$lib_name])) {
throw new WrongUsageException("{$lib_name} not exist !");
}
foreach ($dep_list[$lib_name]['depends'] as $dep) {
self::visitPlatDeps($dep, $dep_list, $visited, $sorted);
}

View File

@ -57,10 +57,6 @@ class GlobalEnvManager
self::putenv("SPC_LINUX_DEFAULT_CXX={$arch}-linux-musl-g++");
self::putenv("SPC_LINUX_DEFAULT_AR={$arch}-linux-musl-ar");
}
self::putenv("SPC_PHP_DEFAULT_LD_LIBRARY_PATH_CMD=LD_LIBRARY_PATH=/usr/local/musl/{$arch}-linux-musl/lib");
if (getenv('SPC_NO_MUSL_PATH') !== 'yes') {
self::putenv("PATH=/usr/local/musl/bin:/usr/local/musl/{$arch}-linux-musl/bin:" . getenv('PATH'));
}
}
// Init env.ini file, read order:
@ -112,6 +108,11 @@ class GlobalEnvManager
'BSD' => self::applyConfig($ini['freebsd']),
default => null,
};
if (PHP_OS_FAMILY === 'Linux' && !filter_var(getenv('SPC_NO_MUSL_PATH'), FILTER_VALIDATE_BOOLEAN)) {
self::putenv("SPC_PHP_DEFAULT_LD_LIBRARY_PATH_CMD=LD_LIBRARY_PATH=/usr/local/musl/{$arch}-linux-musl/lib");
self::putenv("PATH=/usr/local/musl/bin:/usr/local/musl/{$arch}-linux-musl/bin:" . getenv('PATH'));
}
}
public static function putenv(string $val): void

View File

@ -70,6 +70,9 @@ class LicenseDumper
}
foreach ($this->libs as $lib) {
if (Config::getLib($lib, 'type', 'lib') !== 'lib') {
continue;
}
$source_name = Config::getLib($lib, 'source');
foreach ($this->getSourceLicenses($source_name) as $index => $license) {
$result = file_put_contents("{$target_dir}/lib_{$lib}_{$index}.txt", $license);

View File

@ -0,0 +1,5 @@
<?php
declare(strict_types=1);
assert(function_exists('OpenTelemetry\Instrumentation\hook'));

View File

@ -20,6 +20,14 @@ function is_assoc_array(mixed $array): bool
return is_array($array) && (!empty($array) && array_keys($array) !== range(0, count($array) - 1));
}
/**
* Judge if an array is a list
*/
function is_list_array(mixed $array): bool
{
return is_array($array) && (empty($array) || array_keys($array) === range(0, count($array) - 1));
}
/**
* Return a logger instance
*/

View File

@ -13,14 +13,13 @@ declare(strict_types=1);
// test php version
$test_php_version = [
// '8.1',
// '8.2',
// '8.3',
'8.3',
'8.4',
];
// test os (macos-13, macos-14, ubuntu-latest, windows-latest are available)
$test_os = [
// 'macos-13',
'macos-14',
'ubuntu-latest',
// 'windows-latest',
@ -35,12 +34,12 @@ $no_strip = false;
$upx = false;
// prefer downloading pre-built packages to speed up the build process
$prefer_pre_built = true;
$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' => 'dio',
'Windows' => 'dio',
'Linux', 'Darwin' => 'imap,openssl,zlib,memcache',
'Windows' => 'gettext',
};
// If you want to test lib-suggests feature with extension, add them below (comma separated, example `libwebp,libavif`).
@ -162,6 +161,8 @@ if ($argv[1] === 'download_cmd') {
} else {
passthru('./bin/spc ' . $build_cmd . ' --build-embed', $retcode);
}
} else {
$retcode = 0;
}
exit($retcode);

View File

@ -71,7 +71,7 @@ class ExtensionTest extends TestCase
public function testRunCliCheckWindows()
{
if (is_unix()) {
$this->markTestIncomplete('This test is for Windows only');
$this->markTestSkipped('This test is for Windows only');
} else {
$this->extension->runCliCheckWindows();
$this->assertTrue(true);

View File

@ -15,7 +15,7 @@ class SystemUtilTest extends TestCase
public static function setUpBeforeClass(): void
{
if (PHP_OS_FAMILY !== 'Linux') {
self::markTestIncomplete('This test is only for Linux');
self::markTestSkipped('This test is only for Linux');
}
}

View File

@ -15,7 +15,7 @@ class SystemUtilTest extends TestCase
public static function setUpBeforeClass(): void
{
if (PHP_OS_FAMILY !== 'Darwin') {
self::markTestIncomplete('This test is only for macOS');
self::markTestSkipped('This test is only for macOS');
}
}

View File

@ -26,7 +26,7 @@ class UnixSystemUtilTest extends TestCase
default => null,
};
if ($util_class === null) {
self::markTestIncomplete('This test is only for Unix');
self::markTestSkipped('This test is only for Unix');
}
$this->util = new $util_class();
}

View File

@ -29,6 +29,8 @@ final class DependencyUtilTest extends TestCase
],
];
Config::$lib = [
'lib-base' => ['type' => 'root'],
'php' => ['type' => 'root'],
'libaaa' => [
'source' => 'test1',
'static-libs' => ['libaaa.a'],

View File

@ -34,6 +34,8 @@ final class LicenseDumperTest extends TestCase
public function testDumpWithSingleLicense(): void
{
Config::$lib = [
'lib-base' => ['type' => 'root'],
'php' => ['type' => 'root'],
'fake_lib' => [
'source' => 'fake_lib',
],
@ -57,6 +59,8 @@ final class LicenseDumperTest extends TestCase
public function testDumpWithMultipleLicenses(): void
{
Config::$lib = [
'lib-base' => ['type' => 'root'],
'php' => ['type' => 'root'],
'fake_lib' => [
'source' => 'fake_lib',
],