Compare commits

...

74 Commits

Author SHA1 Message Date
crazywhalecc
097ecd3fb0 test build spc 2024-02-19 13:43:58 +08:00
crazywhalecc
b9359759dd update README 2024-02-19 13:25:47 +08:00
crazywhalecc
50fe366c42 restore strip line 2024-02-19 13:25:47 +08:00
crazywhalecc
9dd89e6b02 add --with-upx-pack command 2024-02-19 13:25:47 +08:00
crazywhalecc
c03220d1ee fix source extract 2024-02-19 13:25:47 +08:00
crazywhalecc
9db843ab66 fix source extract 2024-02-19 13:25:47 +08:00
Jerry Ma
304973d9bc Update FUNDING.yml 2024-02-19 09:33:54 +08:00
Jerry Ma
ab386f820c Update FUNDING.yml 2024-02-19 09:29:45 +08:00
crazywhalecc
4adf1f5e2e add musl-toolchain pkg for aarch64 2024-02-18 16:22:17 +08:00
crazywhalecc
b4ae87585c treat https://github.com/crazywhalecc/static-php-cli/issues/249 as feature 2024-02-18 16:22:17 +08:00
crazywhalecc
ae3298472d upx use ghrel 2024-02-18 16:22:17 +08:00
crazywhalecc
d241cb993e fix 2024-02-18 16:22:17 +08:00
crazywhalecc
8376122634 sort pkg.json 2024-02-18 16:22:17 +08:00
crazywhalecc
a30e054d7d Add package management 2024-02-18 16:22:17 +08:00
Jerry Ma
78f4317660 Merge pull request #341 from crazywhalecc/no-ansi-build
Apply default `--no-ansi` output
2024-02-18 13:54:27 +08:00
crazywhalecc
3ac3dab6c8 apply default no-ansi output 2024-02-18 09:44:04 +08:00
Jerry Ma
1a7e436ee1 Merge pull request #339 from crazywhalecc/ext/gettext
Add gettext support
2024-02-17 01:31:10 +08:00
crazywhalecc
f11b36ab3c Merge remote-tracking branch 'origin/main' into ext/gettext 2024-02-17 00:47:40 +08:00
Jerry Ma
e2ef195a84 Update release-build.yml 2024-02-17 00:44:03 +08:00
crazywhalecc
645e2a9fc2 update to beta 3 2024-02-17 00:37:09 +08:00
crazywhalecc
d59b8457c6 update composer 2024-02-17 00:33:23 +08:00
crazywhalecc
a2d1262cbf update composer.lock 2024-02-17 00:25:25 +08:00
Jerry Ma
2f9a1e8601 Update release-build.yml 2024-02-17 00:09:46 +08:00
Jerry Ma
b239f60fe4 Update release-build.yml 2024-02-17 00:09:32 +08:00
Jerry Ma
983521e225 Update release-build.yml 2024-02-17 00:07:30 +08:00
Jerry Ma
aeed04a5ec Update release-build.yml 2024-02-17 00:04:02 +08:00
crazywhalecc
faed569e8a fix gettext build for linux 2024-02-16 23:28:14 +08:00
crazywhalecc
49ddb3ec13 fix gettext build 2024-02-16 23:04:58 +08:00
crazywhalecc
71c0387ab0 remove tune flags, add debug output for extension sanity check 2024-02-16 20:17:34 +08:00
crazywhalecc
e5d2d5e689 Merge branch 'main' into ext/gettext
# Conflicts:
#	config/ext.json
#	src/globals/test-extensions.php
2024-02-16 19:42:18 +08:00
Jerry Ma
be2394b39b Merge pull request #329 from crazywhalecc/suggest-cmd
add with-suggested-libs and with-suggested-exts
2024-02-16 19:36:54 +08:00
crazywhalecc
158298b96c Merge branch 'main' into suggest-cmd
# Conflicts:
#	src/globals/test-extensions.php
2024-02-16 18:58:11 +08:00
crazywhalecc
ffa84f8b91 remove unused exts 2024-02-16 18:57:44 +08:00
crazywhalecc
0954ddcc96 refactor some terminal outputs 2024-02-16 18:57:32 +08:00
crazywhalecc
d9bd96af71 add dependency util tests 2024-02-16 18:57:12 +08:00
crazywhalecc
2649dcd05c add BuilderBase::getPHPVersionFromArchive 2024-02-16 18:56:59 +08:00
crazywhalecc
939db75268 refactor DependencyUtil, use for-libs and for-sources instead of by 2024-02-16 18:56:33 +08:00
crazywhalecc
227bf73870 libxml2 use zlib prefix 2024-02-16 01:45:14 +08:00
crazywhalecc
05e3898e7a add gettext support 2024-02-16 01:28:10 +08:00
Jerry Ma
9777c9aa93 Fix openpty bug for linux (#337)
* fix openpty bug for linux

* add event test

* add download retry for download command

* use test token

* use test token

* use test token

* use test token

* re-fix this bug
2024-02-14 00:49:58 +08:00
Viktor Szépe
b8d8461e61 Fix badges in README (#333)
* Fix badges in README

* Remove Discord badge

* Fix badges in README-zh
2024-02-12 19:59:51 +08:00
Viktor Szépe
b977543c72 Update README.md 2024-02-12 19:12:43 +08:00
Viktor Szépe
4d87cd11cc Update README-zh.md 2024-02-12 19:12:43 +08:00
Viktor Szépe
957daf0547 Update README.md 2024-02-12 19:12:43 +08:00
Viktor Szépe
3dae904122 Update README-zh.md 2024-02-12 19:12:43 +08:00
Viktor Szépe
e835196972 Add language badges to README 2024-02-12 19:12:43 +08:00
crazywhalecc
52ed0e2cee add glibtoolize check for macos 2024-02-07 01:19:54 +08:00
crazywhalecc
1e898d271d add with-suggested-libs and with-suggested-exts 2024-02-06 16:06:09 +08:00
crazywhalecc
39754cde59 add micro patcher for php84 2024-02-06 15:56:47 +08:00
Peter Kokot
a6f7b938e1 Fix PHP version ID 2024-02-05 20:05:54 +08:00
Peter Kokot
0ad501af9a Patch for PHP >= 8.4
FIBER_ASSEMBLER and FIBER_ASM_ARCH Makefile variables in Windows build
system PHP 8.4 have been removed in favor of the PHP_ASSEMBLER and
FIBER_ASM_ABI.
2024-02-05 20:05:54 +08:00
crazywhalecc
73035067e3 fix swoole-hook 2024-02-04 18:00:35 +08:00
crazywhalecc
421b3830b6 fix mbregex test for micro 2024-02-04 18:00:35 +08:00
crazywhalecc
59dcb905fe add test 2024-02-04 18:00:35 +08:00
crazywhalecc
d222190fc7 fix curl bug #327 2024-02-04 18:00:35 +08:00
crazywhalecc
06e9864d19 fix swoole hook check 2024-02-04 18:00:35 +08:00
crazywhalecc
0b1a321615 change with-micro-ext-test to without 2024-02-04 18:00:35 +08:00
crazywhalecc
9ee7d7769a add arm macos test 2024-02-04 11:22:53 +08:00
crazywhalecc
2440a65d8e fix windows test 2024-02-04 11:22:53 +08:00
crazywhalecc
839931d65f add static-php-cli.version ini for php-src 2024-02-04 11:22:53 +08:00
Jerry Ma
2591b48abe Update README-zh.md 2024-02-02 09:45:55 +08:00
Jerry Ma
2549597871 Update README.md 2024-02-02 09:44:37 +08:00
Jerry Ma
2064172bed Update build-macos-aarch64.yml 2024-01-31 11:58:56 +08:00
Jerry Ma
dc19d0c61d Create build-macos-aarch64.yml 2024-01-31 11:26:40 +08:00
crazywhalecc
05c6dc6dab add more tests 2024-01-29 09:55:20 +08:00
Jerry Ma
4b653bc293 Update README-zh.md 2024-01-11 10:08:32 +08:00
Jerry Ma
1c3476b8e9 Update README.md 2024-01-11 10:08:02 +08:00
Jerry Ma
61ca501c2a Update README.md 2024-01-11 09:51:48 +08:00
crazywhalecc
d916feec62 test bulk 2024-01-11 09:48:28 +08:00
crazywhalecc
0dcecf6ca4 fix windows mbstring 2024-01-11 09:48:28 +08:00
crazywhalecc
a04deb458f fix test build 2024-01-11 09:48:28 +08:00
crazywhalecc
34f810571e fix phpunit test 2024-01-11 09:48:28 +08:00
crazywhalecc
5ab4d140d5 fix phpstan 2024-01-11 09:48:28 +08:00
crazywhalecc
104778d17a add windows support 2024-01-11 09:48:28 +08:00
84 changed files with 3418 additions and 901 deletions

View File

@@ -0,0 +1,127 @@
name: CI on arm64 macOS
on:
workflow_dispatch:
inputs:
version:
required: true
description: php version to compile
default: '8.2'
type: choice
options:
- '8.3'
- '8.2'
- '8.1'
- '8.0'
- '7.4'
build-cli:
description: build cli binary
default: true
type: boolean
build-micro:
description: build phpmicro binary
type: boolean
build-fpm:
description: build fpm binary
type: boolean
extensions:
description: extensions to compile (comma separated)
required: true
type: string
debug:
type: boolean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
build:
name: build ${{ inputs.version }} on macOS arm64
runs-on: macos-14
steps:
- uses: actions/checkout@v3
# Install macOS missing packages and mark os suffix
- run: |
brew install automake gzip
echo "SPC_BUILD_OS=macos" >> $GITHUB_ENV
- name: "Setup PHP"
uses: shivammathur/setup-php@v2
with:
php-version: 8.1
tools: pecl, composer
extensions: curl, openssl, mbstring, tokenizer
ini-values: memory_limit=-1
# Cache composer dependencies
- id: cache-composer-deps
uses: actions/cache@v3
with:
path: vendor
key: composer-dependencies
# If there's no Composer cache, install dependencies
- if: steps.cache-composer-deps.outputs.cache-hit != 'true'
run: composer update --no-dev --classmap-authoritative
# Cache downloaded source
- id: cache-download
uses: actions/cache@v3
with:
path: downloads
key: php-${{ inputs.version }}-dependencies-${{ inputs.extensions }}
# With or without debug
- if: inputs.debug == true
run: echo "SPC_BUILD_DEBUG=--debug" >> $GITHUB_ENV
# With target select: cli, micro or both
- if: ${{ inputs.build-cli == true }}
run: echo "SPC_BUILD_CLI=--build-cli" >> $GITHUB_ENV
- if: ${{ inputs.build-micro == true }}
run: echo "SPC_BUILD_MICRO=--build-micro" >> $GITHUB_ENV
- if: ${{ inputs.build-fpm == true }}
run: echo "SPC_BUILD_FPM=--build-fpm" >> $GITHUB_ENV
# If there's no dependencies cache, fetch sources, with or without debug
- if: steps.cache-download.outputs.cache-hit != 'true'
run: ./bin/spc download --with-php=${{ inputs.version }} --for-extensions=${{ inputs.extensions }} ${{ env.SPC_BUILD_DEBUG }}
# Run build command
- run: ./bin/spc build ${{ inputs.extensions }} ${{ env.SPC_BUILD_DEBUG }} ${{ env.SPC_BUILD_CLI }} ${{ env.SPC_BUILD_MICRO }} ${{ env.SPC_BUILD_FPM }}
# Upload cli executable
- if: ${{ inputs.build-cli == true }}
uses: actions/upload-artifact@v3
with:
name: php-${{ inputs.version }}-${{ env.SPC_BUILD_OS }}
path: buildroot/bin/php
# Upload micro self-extracted executable
- if: ${{ inputs.build-micro == true }}
uses: actions/upload-artifact@v3
with:
name: micro-${{ inputs.version }}-${{ env.SPC_BUILD_OS }}
path: buildroot/bin/micro.sfx
# Upload fpm executable
- if: ${{ inputs.build-fpm == true }}
uses: actions/upload-artifact@v3
with:
name: php-fpm-${{ inputs.version }}-${{ env.SPC_BUILD_OS }}
path: buildroot/bin/php-fpm
# Upload extensions metadata
- uses: actions/upload-artifact@v3
with:
name: license-files
path: buildroot/license/
- uses: actions/upload-artifact@v3
with:
name: build-meta
path: |
buildroot/build-extensions.json
buildroot/build-libraries.json

View File

@@ -15,14 +15,15 @@ jobs:
strategy:
matrix:
php-version:
- "8.1"
- "8.2"
micro-version:
- "8.1.26"
- "8.2.16"
operating-system:
- "linux-x86_64"
- "macos-x86_64"
- "linux-aarch64"
- "macos-aarch64"
- "windows-x64"
steps:
- name: "Checkout"
uses: "actions/checkout@v4"
@@ -59,31 +60,51 @@ jobs:
- name: "Download minimal combination"
run: |
curl https://dl.static-php.dev/static-php-cli/minimal/php-${{ matrix.micro-version }}-micro-${{ matrix.operating-system }}.tar.gz -o tmp.tgz
tar -zxvf tmp.tgz
if [ "${{ matrix.operating-system }}" = "windows-x64" ]; then
curl https://dl.static-php.dev/static-php-cli/windows/spc-min/php-${{ matrix.micro-version }}-micro-win.zip -o tmp.zip
unzip tmp.zip
else
curl https://dl.static-php.dev/static-php-cli/minimal/php-${{ matrix.micro-version }}-micro-${{ matrix.operating-system }}.tar.gz -o tmp.tgz
tar -zxvf tmp.tgz
fi
- name: "Generate Executable"
run: |
cat micro.sfx spc.phar > spc
chmod +x spc
if [ "${{ matrix.operating-system }}" = "windows-x64" ]; then
cat micro.sfx spc.phar > spc.exe
else
cat micro.sfx spc.phar > spc
chmod +x spc
fi
- name: "Archive Executable"
run: |
tar -czf spc-${{ matrix.operating-system }}.tar.gz spc
echo "filename=spc-${{ matrix.operating-system }}.tar.gz" >> $GITHUB_ENV
echo "OS=${{ matrix.operating-system }}" >> $GITHUB_ENV
if [ "${{ matrix.operating-system }}" == "linux-x86_64" ]; then
./spc dev:extensions
if [ "${{ matrix.operating-system }}" != "windows-x64" ]; then
tar -czf spc-${{ matrix.operating-system }}.tar.gz spc
echo "filename=spc-${{ matrix.operating-system }}.tar.gz" >> $GITHUB_ENV
echo "OS=${{ matrix.operating-system }}" >> $GITHUB_ENV
if [ "${{ matrix.operating-system }}" == "linux-x86_64" ]; then
./spc dev:extensions
fi
else
echo "filename=spc-${{ matrix.operating-system }}.exe" >> $GITHUB_ENV
echo "OS=${{ matrix.operating-system }}" >> $GITHUB_ENV
fi
- name: "Copy file"
run: "mkdir dist/ && cp ${{ env.filename }} dist/ && cp spc dist/spc-$OS"
run: |
if [ "${{ matrix.operating-system }}" != "windows-x64" ]; then
mkdir dist/ && cp ${{ env.filename }} dist/ && cp spc dist/spc-$OS
else
mkdir dist/ && cp spc.exe dist/${{ env.filename }}
echo "SUFFIX=.exe" >> $GITHUB_ENV
fi
- name: upload binaries to release
uses: softprops/action-gh-release@v1
if: ${{startsWith(github.ref, 'refs/tags/') }}
with:
files: ${{ env.filename }}
files: dist/${{ env.filename }}
- name: "Deploy to Self-Hosted Server"
if: github.repository == 'crazywhalecc/static-php-cli'
@@ -100,5 +121,5 @@ jobs:
- name: "Upload Artifact"
uses: actions/upload-artifact@v3
with:
path: spc
name: spc-${{ matrix.operating-system }}
path: spc${{ env.SUFFIX }}
name: spc-${{ matrix.operating-system }}${{ env.SUFFIX }}

View File

@@ -114,6 +114,8 @@ jobs:
os:
- ubuntu-latest
- macos-latest
- windows-latest
- macos-14
fail-fast: false
steps:
- name: "Checkout"
@@ -127,6 +129,13 @@ jobs:
extensions: curl, openssl, mbstring
ini-values: memory_limit=-1
- name: "Use test token if exists"
if: matrix.os != 'windows-latest'
run: |
if [ "${{ secrets.TEST_GH_TOKEN }}" != "" ]; then
echo "GITHUB_TOKEN=${{ secrets.TEST_GH_TOKEN }}" >> $GITHUB_ENV
fi
- name: "Cache Composer packages"
id: composer-cache
uses: actions/cache@v3
@@ -150,7 +159,12 @@ jobs:
run: bin/spc doctor --auto-fix
- name: "Run Build Tests (download)"
run: bin/spc download --for-extensions="$(php src/globals/test-extensions.php extensions)" --with-php=${{ matrix.php }} --debug
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
retry_on: error
command: bin/spc download --for-extensions="$(php src/globals/test-extensions.php extensions)" --with-php=${{ matrix.php }} --debug
- name: "Run Build Tests (build)"
run: bin/spc build $(php src/globals/test-extensions.php cmd) --build-cli --build-micro --build-fpm --debug
run: bin/spc build "$(php src/globals/test-extensions.php extensions)" $(php src/globals/test-extensions.php libs_cmd) --build-cli --build-micro --build-fpm --debug

3
.gitignore vendored
View File

@@ -16,6 +16,9 @@ docker/source/
# default source build root directory
/buildroot/
# default package root directory
/pkgroot/
# tools cache files
.php-cs-fixer.cache
.phpunit.result.cache

View File

@@ -1,10 +1,11 @@
# static-php-cli
[![Version](https://img.shields.io/packagist/v/crazywhalecc/static-php-cli?include_prereleases&label=Release&style=flat-square)]()
[![](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://img.shields.io/badge/Extension%20Counter-75+-yellow.svg?style=flat-square)]([https://static-php.dev/](https://static-php.dev/en/guide/extensions.html))
[![](https://dcbadge.vercel.app/api/server/RNpegEYW?style=flat-square&compact=true&theme=default-inverted)](https://discord.gg/RNpegEYW)
[![English readme](https://img.shields.io/badge/README-English%20%F0%9F%87%AC%F0%9F%87%A7-moccasin?style=flat-square)](README.md)
[![Chinese readme](https://img.shields.io/badge/README-%E4%B8%AD%E6%96%87%20%F0%9F%87%A8%F0%9F%87%B3-moccasin?style=flat-square)](README-zh.md)
[![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 解释器的工具,支持众多流行扩展。
@@ -12,9 +13,6 @@
**static-php-cli**也支持将 PHP 代码和 PHP 运行时打包为一个文件并运行。
- [README - English](./README.md)
- [README - 中文](./README-zh.md)
## 特性
static-php-cli简称 `spc`)有许多特性:
@@ -27,6 +25,7 @@ static-php-cli简称 `spc`)有许多特性:
- :books: 自带编译依赖管理
- 📦 提供由自身编译的独立 `spc` 二进制(使用 spc 和 [box](https://github.com/box-project/box) 构建)
- :fire: 支持大量 [扩展](https://static-php.dev/zh/guide/extensions.html)
- :floppy_disk: 整合 UPX 工具(减小二进制文件体积)
**静态 php-cli:**
@@ -52,7 +51,7 @@ static-php-cli简称 `spc`)有许多特性:
### 编译环境需求
- PHP >= 8.1spc 自身是用 PHP 写的
- PHP >= 8.1这是 spc 自身需要的版本,不是支持的构建版本
- 扩展:`mbstring,pcntl,posix,tokenizer,phar`
- 系统安装了 `curl``git`
@@ -63,7 +62,7 @@ static-php-cli简称 `spc`)有许多特性:
| | x86_64 | aarch64 |
|---------|----------------------|----------------------|
| macOS | :octocat: :computer: | :computer: |
| macOS | :octocat: :computer: | :octocat: :computer: |
| Linux | :octocat: :computer: | :octocat: :computer: |
| Windows | | |
| FreeBSD | :computer: | :computer: |
@@ -250,18 +249,17 @@ bin/spc micro:combine my-app.phar -I "memory_limit=4G" -I "disable_functions=sys
## 赞助本项目
你可以在 [我的个人赞助页](https://github.com/crazywhalecc/crazywhalecc/blob/master/FUNDING.md) 支持我和我的项目。
你可以在 [我的个人赞助页](https://github.com/crazywhalecc/crazywhalecc/blob/master/FUNDING.md) 支持我和我的项目。你捐赠的一部分将会被用于维护 **static-php.dev** 服务器。
## 开源协议
本项目依据旧版本惯例采用 MIT License 开源,部分扩展的集成编译命令参考或修改自以下项目:
本项目采用 MIT License 许可开源,下面是类似的项目:
- [dixyes/lwmbs](https://github.com/dixyes/lwmbs)(木兰宽松许可证)
- [swoole/swoole-cli](https://github.com/swoole/swoole-cli)Apache 2.0 LICENSE、SWOOLE-CLI LICENSE
- [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 授权许可。
因本项目的特殊性,使用项目编译过程中会使用很多其他开源项目,例如 curl、protobuf 等,它们都有各自的开源协议。
请在编译完成后,使用命令 `bin/spc dump-license` 导出项目使用项目的开源协议,并遵守对应项目的 LICENSE。
## 进阶
本项目重构分支为模块化编写。如果你对本项目感兴趣,想加入开发,可以参照文档的 [贡献指南](https://static-php.dev) 贡献代码或文档。

View File

@@ -1,10 +1,11 @@
# static-php-cli
[![Version](https://img.shields.io/packagist/v/crazywhalecc/static-php-cli?include_prereleases&label=Release&style=flat-square)]()
[![](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://img.shields.io/badge/Extension%20Counter-75+-yellow.svg?style=flat-square)]([https://static-php.dev/](https://static-php.dev/en/guide/extensions.html))
[![](https://dcbadge.vercel.app/api/server/RNpegEYW?style=flat-square&compact=true&theme=default-inverted)](https://discord.gg/RNpegEYW)
[![Chinese readme](https://img.shields.io/badge/README-%E4%B8%AD%E6%96%87%20%F0%9F%87%A8%F0%9F%87%B3-moccasin?style=flat-square)](README-zh.md)
[![English readme](https://img.shields.io/badge/README-English%20%F0%9F%87%AC%F0%9F%87%A7-moccasin?style=flat-square)](README.md)
[![Releases](https://img.shields.io/packagist/v/crazywhalecc/static-php-cli?include_prereleases&label=Release&style=flat-square)](https://github.com/crazywhalecc/static-php-cli/releases)
[![CI](https://img.shields.io/github/actions/workflow/status/crazywhalecc/static-php-cli/tests.yml?branch=main&label=Build%20Test&style=flat-square)](https://github.com/crazywhalecc/static-php-cli/actions/workflows/tests.yml)
[![License](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](https://github.com/crazywhalecc/static-php-cli/blob/main/LICENSE)
[![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.
@@ -14,9 +15,6 @@ Static PHP built by **static-php-cli** supports `cli`, `fpm`, `embed` and `micro
**static-php-cli** also has the ability to package PHP projects
along with the PHP interpreter into one single executable file.
- [README - English](./README.md)
- [README - 中文](./README-zh.md)
## Features
static-php-cli (you can call it `spc`) has a lot of features:
@@ -24,11 +22,12 @@ 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)
- :zap: `Linux`, `macOS`, `FreeBSD`, [`Windows (WIP)`](https://github.com/crazywhalecc/static-php-cli/pull/301) support
- :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)
**Single-file standalone php-cli:**
@@ -58,7 +57,7 @@ which can be downloaded directly according to your needs.
### Compilation Requirements
- PHP >= 8.1 (static-php-cli is written in PHP)
- PHP >= 8.1 (This is the version required by spc itself, not the build version)
- Extension: `mbstring,pcntl,posix,tokenizer,phar`
- Supported OS with `curl` and `git` installed
@@ -70,9 +69,9 @@ Here is the supported OS and arch, where :octocat: represents support for GitHub
| | x86_64 | aarch64 |
|---------|----------------------|----------------------|
| macOS | :octocat: :computer: | :computer: |
| macOS | :octocat: :computer: | :octocat: :computer: |
| Linux | :octocat: :computer: | :octocat: :computer: |
| Windows | | |
| Windows | :computer: | |
| FreeBSD | :computer: | :computer: |
Currently supported PHP versions for compilation are: `7.3`, `7.4`, `8.0`, `8.1`, `8.2`, `8.3`.
@@ -274,7 +273,7 @@ Now there is a [static-php](https://github.com/static-php) organization, which i
## Sponsor this project
You can sponsor my project on [this page](https://github.com/crazywhalecc/crazywhalecc/blob/master/FUNDING.md).
You can sponsor my project on [this page](https://github.com/crazywhalecc/crazywhalecc/blob/master/FUNDING.md). A portion of your donation will be used to maintain the **static-php.dev** server.
## Open-Source License
@@ -287,6 +286,9 @@ These are similar projects:
- [dixyes/lwmbs](https://github.com/dixyes/lwmbs)
- [swoole/swoole-cli](https://github.com/swoole/swoole-cli)
The project uses some code from [dixyes/lwmbs](https://github.com/dixyes/lwmbs), such as windows static build target and libiconv support.
lwmbs is licensed under the [Mulan PSL 2](http://license.coscl.org.cn/MulanPSL2).
Due to the special nature of this project,
many other open source projects such as curl and protobuf will be used during the project compilation process,
and they all have their own open source licenses.

View File

@@ -28,7 +28,7 @@ function RemoveFromPath {
$currentPath = [System.Environment]::GetEnvironmentVariable('Path', 'User')
if ($currentPath -like "*$pathToRemove*") {
$newPath = $currentPath -replace [regex]::Escape($pathToRemove), ''
$newPath = $currentPath -replace [regex]::Escape(';' + $pathToRemove), ''
[System.Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
Write-Host "Removed Path '$pathToRemove'"
} else {
@@ -92,7 +92,7 @@ if (-not(Test-Path "runtime\composer.phar"))
# create runtime\composer.ps1
$ComposerContent = '
$WorkingDir = (Split-Path -Parent $MyInvocation.MyCommand.Definition)
Start-Process ($WorkingDir + "\php.exe") ($WorkingDir + "\composer.phar " + $args) -NoNewWindow -Wait
& ($WorkingDir + "\php.exe") (Join-Path $WorkingDir "\composer.phar") @args
'
$ComposerContent | Set-Content -Path 'runtime\composer.ps1' -Encoding UTF8

View File

@@ -21,4 +21,5 @@ try {
(new ConsoleApplication())->run();
} catch (Exception $e) {
ExceptionHandler::getInstance()->handle($e);
exit(1);
}

View File

@@ -1,4 +1,4 @@
$PHP_Exec = "runtime\php.exe"
$PHP_Exec = ".\runtime\php.exe"
if (-not(Test-Path $PHP_Exec)) {
$PHP_Exec = Get-Command php.exe -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Definition
@@ -8,5 +8,4 @@ if (-not(Test-Path $PHP_Exec)) {
}
}
$phpArgs = "bin\spc " + $args
Start-Process $PHP_Exec -ArgumentList $phpArgs -NoNewWindow -Wait
& "$PHP_Exec" ("bin/spc") @args

View File

@@ -14,6 +14,5 @@
"vendor/zhamao"
],
"git-commit-short": "git_commit_short",
"metadata": "ConsoleApplication::VERSION",
"output": "spc.phar"
}

View File

@@ -19,10 +19,10 @@
"captainhook/captainhook": "^5.10",
"captainhook/plugin-composer": "^5.3",
"friendsofphp/php-cs-fixer": "^3.25",
"humbug/box": "^4.3",
"humbug/box": "^4.5",
"nunomaduro/collision": "^7.8",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^10.3"
"phpunit/phpunit": "^10.3 || ^9"
},
"autoload": {
"psr-4": {

940
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -95,7 +95,7 @@
},
"gettext": {
"type": "builtin",
"arg-type": "with",
"arg-type": "with-prefix",
"lib-depends": [
"gettext"
]
@@ -106,7 +106,8 @@
"source": "ext-glfw",
"lib-depends": [
"glfw"
]
],
"lib-depends-windows": []
},
"gmp": {
"type": "builtin",
@@ -238,6 +239,7 @@
"openssl": {
"type": "builtin",
"arg-type": "custom",
"arg-type-windows": "with",
"lib-depends": [
"openssl",
"zlib"
@@ -311,13 +313,6 @@
"type": "external",
"source": "protobuf"
},
"pspell": {
"type": "builtin",
"arg-type": "with",
"lib-depends": [
"aspell"
]
},
"rar": {
"type": "external",
"source": "rar",
@@ -369,13 +364,6 @@
"apcu"
]
},
"snmp": {
"type": "builtin",
"arg-type": "with",
"lib-depends": [
"net-snmp"
]
},
"soap": {
"type": "builtin",
"arg-type": "custom",

View File

@@ -57,9 +57,7 @@
"brotli",
"nghttp2",
"zstd",
"openssl",
"idn2",
"psl"
"openssl"
],
"frameworks": [
"CoreFoundation",
@@ -85,6 +83,22 @@
"brotli"
]
},
"gettext": {
"source": "gettext",
"static-libs-unix": [
"libintl.a"
],
"lib-depends": [
"libiconv"
],
"lib-suggests": [
"ncurses",
"libxml2"
],
"frameworks": [
"CoreFoundation"
]
},
"glfw": {
"source": "ext-glfw",
"static-libs-unix": [

46
config/pkg.json Normal file
View File

@@ -0,0 +1,46 @@
{
"musl-toolchain-aarch64-linux": {
"type": "url",
"url": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/aarch64-musl-toolchain.tgz"
},
"musl-toolchain-x86_64-linux": {
"type": "url",
"url": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/x86_64-musl-toolchain.tgz"
},
"nasm-x86_64-win": {
"type": "url",
"url": "https://www.nasm.us/pub/nasm/releasebuilds/2.16.01/win64/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"
}
},
"strawberry-perl-x86_64-win": {
"type": "url",
"url": "https://github.com/StrawberryPerl/Perl-Dist-Strawberry/releases/download/SP_5380_5361/strawberry-perl-5.38.0.1-64bit-portable.zip"
},
"upx-aarch64-linux": {
"type": "ghrel",
"repo": "upx/upx",
"match": "upx.+-arm64_linux\\.tar\\.xz",
"extract-files": {
"upx": "{pkg_root_path}/bin/upx"
}
},
"upx-x86_64-linux": {
"type": "ghrel",
"repo": "upx/upx",
"match": "upx.+-amd64_linux\\.tar\\.xz",
"extract-files": {
"upx": "{pkg_root_path}/bin/upx"
}
},
"upx-x86_64-win": {
"type": "ghrel",
"repo": "upx/upx",
"match": "upx.+-win64\\.zip",
"extract-files": {
"upx-*-win64/upx.exe": "{pkg_root_path}/bin/upx.exe"
}
}
}

View File

@@ -129,6 +129,15 @@
"path": "LICENSE.TXT"
}
},
"gettext": {
"type": "filelist",
"url": "https://ftp.gnu.org/pub/gnu/gettext/",
"regex": "/href=\"(?<file>gettext-(?<version>[^\"]+)\\.tar\\.xz)\"/",
"license": {
"type": "file",
"path": "COPYING"
}
},
"gmp": {
"type": "ghtagtar",
"repo": "alisw/GMP",

View File

@@ -14,4 +14,5 @@ parameters:
excludePaths:
analyseAndScan:
- ./src/globals/tests/swoole.php
- ./src/globals/tests/swoole.phpt
- ./src/globals/tests/swoole.phpt
- ./src/globals/test-extensions.php

View File

@@ -6,6 +6,7 @@ namespace SPC;
use SPC\command\BuildCliCommand;
use SPC\command\BuildLibsCommand;
use SPC\command\DeleteDownloadCommand;
use SPC\command\dev\AllExtCommand;
use SPC\command\dev\PhpVerCommand;
use SPC\command\dev\SortConfigCommand;
@@ -13,6 +14,7 @@ use SPC\command\DoctorCommand;
use SPC\command\DownloadCommand;
use SPC\command\DumpLicenseCommand;
use SPC\command\ExtractCommand;
use SPC\command\InstallPkgCommand;
use SPC\command\MicroCombineCommand;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\HelpCommand;
@@ -23,7 +25,7 @@ use Symfony\Component\Console\Command\ListCommand;
*/
final class ConsoleApplication extends Application
{
public const VERSION = '2.0.1';
public const VERSION = '2.1.0-beta.4';
public function __construct()
{
@@ -35,6 +37,8 @@ final class ConsoleApplication extends Application
new BuildLibsCommand(),
new DoctorCommand(),
new DownloadCommand(),
new InstallPkgCommand(),
new DeleteDownloadCommand(),
new DumpLicenseCommand(),
new ExtractCommand(),
new MicroCombineCommand(),

View File

@@ -9,10 +9,8 @@ use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException;
use SPC\store\Config;
use SPC\store\FileSystem;
use SPC\store\SourceExtractor;
use SPC\store\SourceManager;
use SPC\util\CustomExt;
use SPC\util\DependencyUtil;
abstract class BuilderBase
{
@@ -43,64 +41,7 @@ abstract class BuilderBase
* @throws WrongUsageException
* @internal
*/
public function buildLibs(array $sorted_libraries): void
{
// search all supported libs
$support_lib_list = [];
$classes = FileSystem::getClassesPsr4(
ROOT_DIR . '/src/SPC/builder/' . osfamily2dir() . '/library',
'SPC\\builder\\' . osfamily2dir() . '\\library'
);
foreach ($classes as $class) {
if (defined($class . '::NAME') && $class::NAME !== 'unknown' && Config::getLib($class::NAME) !== null) {
$support_lib_list[$class::NAME] = $class;
}
}
// if no libs specified, compile all supported libs
if ($sorted_libraries === [] && $this->isLibsOnly()) {
$libraries = array_keys($support_lib_list);
$sorted_libraries = DependencyUtil::getLibsByDeps($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 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!');
}
$lib = new ($support_lib_list[$library])($this);
$this->addLib($lib);
}
// calculate and check dependencies
foreach ($this->libs as $lib) {
$lib->calcDependency();
}
// patch point
$this->emitPatchPoint('before-libs-extract');
// extract sources
SourceExtractor::initSource(libs: $sorted_libraries);
$this->emitPatchPoint('after-libs-extract');
// build all libs
foreach ($this->libs as $lib) {
match ($lib->tryBuild($this->getOption('rebuild', false))) {
BUILD_STATUS_OK => logger()->info('lib [' . $lib::NAME . '] build success'),
BUILD_STATUS_ALREADY => logger()->notice('lib [' . $lib::NAME . '] already built'),
BUILD_STATUS_FAILED => logger()->error('lib [' . $lib::NAME . '] build failed'),
default => logger()->warning('lib [' . $lib::NAME . '] build status unknown'),
};
}
}
abstract public function buildLibs(array $sorted_libraries);
/**
* Add library to build.
@@ -203,15 +144,15 @@ abstract class BuilderBase
{
CustomExt::loadCustomExt();
$this->emitPatchPoint('before-php-extract');
SourceExtractor::initSource(sources: ['php-src']);
SourceManager::initSource(sources: ['php-src']);
$this->emitPatchPoint('after-php-extract');
if ($this->getPHPVersionID() >= 80000) {
$this->emitPatchPoint('before-micro-extract');
SourceExtractor::initSource(sources: ['micro']);
SourceManager::initSource(sources: ['micro']);
$this->emitPatchPoint('after-micro-extract');
}
$this->emitPatchPoint('before-exts-extract');
SourceExtractor::initSource(exts: $extensions);
SourceManager::initSource(exts: $extensions);
$this->emitPatchPoint('after-exts-extract');
foreach ($extensions as $extension) {
$class = CustomExt::getExtClass($extension);
@@ -242,6 +183,7 @@ abstract class BuilderBase
{
$ret = [];
foreach ($this->exts as $ext) {
logger()->info($ext->getName() . ' is using ' . $ext->getConfigureArg());
$ret[] = trim($ext->getConfigureArg());
}
logger()->debug('Using configure: ' . implode(' ', $ret));
@@ -289,6 +231,30 @@ abstract class BuilderBase
throw new RuntimeException('PHP version file format is malformed, please remove it and download again');
}
/**
* Get PHP version from archive file name.
*
* @param null|string $file php-*.*.*.tar.gz filename, read from lockfile if empty
*/
public function getPHPVersionFromArchive(?string $file = null): false|string
{
if ($file === null) {
$lock = file_exists(DOWNLOAD_PATH . '/.lock.json') ? file_get_contents(DOWNLOAD_PATH . '/.lock.json') : false;
if ($lock === false) {
return false;
}
$lock = json_decode($lock, true);
$file = $lock['php-src']['filename'] ?? null;
if ($file === null) {
return false;
}
}
if (preg_match('/php-(\d+\.\d+\.\d+)/', $file, $match)) {
return $match[1];
}
return false;
}
/**
* Get build type name string to display.
*
@@ -417,4 +383,22 @@ abstract class BuilderBase
);
}
}
/**
* Generate micro extension test php code.
*/
protected function generateMicroExtTests(): string
{
$php = "<?php\n\necho '[micro-test-start]' . PHP_EOL;\n";
foreach ($this->getExts() as $ext) {
$ext_name = $ext->getDistName();
if (!empty($ext_name)) {
$php .= "echo 'Running micro with {$ext_name} test' . PHP_EOL;\n";
$php .= "assert(extension_loaded('{$ext_name}'));\n\n";
}
}
$php .= "echo '[micro-test-end]';\n";
return $php;
}
}

View File

@@ -7,6 +7,7 @@ namespace SPC\builder;
use SPC\builder\freebsd\BSDBuilder;
use SPC\builder\linux\LinuxBuilder;
use SPC\builder\macos\MacOSBuilder;
use SPC\builder\windows\WindowsBuilder;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException;
@@ -27,11 +28,7 @@ class BuilderProvider
public static function makeBuilderByInput(InputInterface $input): BuilderBase
{
self::$builder = match (PHP_OS_FAMILY) {
// 'Windows' => new WindowsBuilder(
// binary_sdk_dir: $input->getOption('with-sdk-binary-dir'),
// vs_ver: $input->getOption('vs-ver'),
// arch: $input->getOption('arch'),
// ),
'Windows' => new WindowsBuilder($input->getOptions()),
'Darwin' => new MacOSBuilder($input->getOptions()),
'Linux' => new LinuxBuilder($input->getOptions()),
'BSD' => new BSDBuilder($input->getOptions()),

View File

@@ -8,6 +8,7 @@ use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException;
use SPC\store\Config;
use SPC\store\FileSystem;
class Extension
{
@@ -42,7 +43,7 @@ class Extension
$arg = $this->getEnableArg();
switch (PHP_OS_FAMILY) {
case 'Windows':
$arg = $this->getWindowsConfigureArg();
$arg .= $this->getWindowsConfigureArg();
break;
case 'Darwin':
case 'Linux':
@@ -164,14 +165,13 @@ 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/tests/{extension_name}.php
* If check failed, throw RuntimeException
*
* @throws RuntimeException
*/
public function runCliCheck(): void
public function runCliCheckUnix(): void
{
// Run compile check if build target is cli
// If you need to run some check, overwrite this or add your assert in src/globals/tests/{extension_name}.php
// If check failed, throw RuntimeException
[$ret] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php --ri "' . $this->getDistName() . '"', false);
if ($ret !== 0) {
throw new RuntimeException('extension ' . $this->getName() . ' failed compile check: php-cli returned ' . $ret);
@@ -185,7 +185,38 @@ class Extension
file_get_contents(ROOT_DIR . '/src/globals/tests/' . $this->getName() . '.php')
);
[$ret] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -r "' . trim($test) . '"');
[$ret, $out] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -r "' . trim($test) . '"');
if ($ret !== 0) {
if ($this->builder->getOption('debug')) {
var_dump($out);
}
throw new RuntimeException('extension ' . $this->getName() . ' failed sanity check');
}
}
}
/**
* @throws RuntimeException
*/
public function runCliCheckWindows(): void
{
// Run compile check if build target is cli
// If you need to run some check, overwrite this or add your assert in src/globals/tests/{extension_name}.php
// If check failed, throw RuntimeException
[$ret] = cmd()->execWithResult(BUILD_ROOT_PATH . '/bin/php.exe --ri "' . $this->getDistName() . '"', false);
if ($ret !== 0) {
throw new RuntimeException('extension ' . $this->getName() . ' failed compile check: php-cli returned ' . $ret);
}
if (file_exists(FileSystem::convertPath(ROOT_DIR . '/src/globals/tests/' . $this->getName() . '.php'))) {
// Trim additional content & escape special characters to allow inline usage
$test = str_replace(
['<?php', 'declare(strict_types=1);', "\n", '"', '$'],
['', '', '', '\"', '\$'],
file_get_contents(FileSystem::convertPath(ROOT_DIR . '/src/globals/tests/' . $this->getName() . '.php'))
);
[$ret] = cmd()->execWithResult(BUILD_ROOT_PATH . '/bin/php.exe -r "' . trim($test) . '"');
if ($ret !== 0) {
throw new RuntimeException('extension ' . $this->getName() . ' failed sanity check');
}

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace SPC\builder\extension;
use SPC\builder\Extension;
use SPC\builder\macos\MacOSBuilder;
use SPC\exception\FileSystemException;
use SPC\store\FileSystem;
use SPC\util\CustomExt;
@@ -34,4 +35,16 @@ class event extends Extension
FileSystem::replaceFileRegex(SOURCE_PATH . '/php-src/configure', '/-levent_openssl/', $this->getLibFilesString());
return true;
}
/**
* @throws FileSystemException
*/
public function patchBeforeMake(): bool
{
// Prevent event extension compile error on macOS
if ($this->builder instanceof MacOSBuilder) {
FileSystem::replaceFileRegex(SOURCE_PATH . '/php-src/main/php_config.h', '/^#define HAVE_OPENPTY 1$/m', '');
}
return true;
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace SPC\builder\extension;
use SPC\builder\Extension;
use SPC\builder\macos\MacOSBuilder;
use SPC\exception\FileSystemException;
use SPC\exception\WrongUsageException;
use SPC\store\FileSystem;
use SPC\util\CustomExt;
#[CustomExt('gettext')]
class gettext extends Extension
{
/**
* @throws FileSystemException
*/
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');
}
return true;
}
/**
* @throws WrongUsageException
* @throws FileSystemException
*/
public function patchBeforeConfigure(): bool
{
if ($this->builder instanceof MacOSBuilder) {
$frameworks = ' ' . $this->builder->getFrameworks(true) . ' ';
FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/configure', '-lintl', $this->getLibFilesString() . $frameworks);
}
return true;
}
}

View File

@@ -34,4 +34,9 @@ class glfw extends Extension
{
return '--enable-glfw --with-glfw-dir=' . BUILD_ROOT_PATH;
}
public function getWindowsConfigureArg(): string
{
return '--enable-glfw=static';
}
}

View File

@@ -11,6 +11,11 @@ use SPC\util\CustomExt;
#[CustomExt('mbregex')]
class mbregex extends Extension
{
public function getDistName(): string
{
return 'mbstring';
}
public function getConfigureArg(): string
{
return '';
@@ -19,7 +24,7 @@ class mbregex extends Extension
/**
* mbregex is not an extension, we need to overwrite the default check.
*/
public function runCliCheck(): void
public function runCliCheckUnix(): void
{
[$ret] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php --ri "mbstring" | grep regex', false);
if ($ret !== 0) {

View File

@@ -10,7 +10,7 @@ use SPC\util\CustomExt;
#[CustomExt('mbstring')]
class mbstring extends Extension
{
public function getUnixConfigureArg(): string
public function getConfigureArg(): string
{
$arg = '--enable-mbstring';
if ($this->builder->getExt('mbregex') === null) {

View File

@@ -11,7 +11,7 @@ use SPC\util\CustomExt;
#[CustomExt('password-argon2')]
class password_argon2 extends Extension
{
public function runCliCheck(): void
public function runCliCheckUnix(): void
{
[$ret] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -r "assert(defined(\'PASSWORD_ARGON2I\'));"');
if ($ret !== 0) {

View File

@@ -11,6 +11,11 @@ use SPC\util\CustomExt;
#[CustomExt('swoole-hook-mysql')]
class swoole_hook_mysql extends Extension
{
public function getDistName(): string
{
return 'swoole';
}
public function getUnixConfigureArg(): string
{
// pdo_mysql doesn't need to be disabled
@@ -18,7 +23,7 @@ class swoole_hook_mysql extends Extension
return '';
}
public function runCliCheck(): void
public function runCliCheckUnix(): void
{
// skip if not enable swoole
if ($this->builder->getExt('swoole') === null) {

View File

@@ -12,6 +12,11 @@ use SPC\util\CustomExt;
#[CustomExt('swoole-hook-pgsql')]
class swoole_hook_pgsql extends Extension
{
public function getDistName(): string
{
return 'swoole';
}
public function getUnixConfigureArg(): string
{
// pdo_pgsql need to be disabled
@@ -22,7 +27,7 @@ class swoole_hook_pgsql extends Extension
return '--enable-swoole-pgsql';
}
public function runCliCheck(): void
public function runCliCheckUnix(): void
{
// skip if not enable swoole
if ($this->builder->getExt('swoole') === null) {

View File

@@ -12,6 +12,11 @@ use SPC\util\CustomExt;
#[CustomExt('swoole-hook-sqlite')]
class swoole_hook_sqlite extends Extension
{
public function getDistName(): string
{
return 'swoole';
}
public function getUnixConfigureArg(): string
{
// pdo_pgsql need to be disabled
@@ -22,7 +27,7 @@ class swoole_hook_sqlite extends Extension
return '--enable-swoole-sqlite';
}
public function runCliCheck(): void
public function runCliCheckUnix(): void
{
// skip if not enable swoole
if ($this->builder->getExt('swoole') === null) {

View File

@@ -4,19 +4,15 @@ declare(strict_types=1);
namespace SPC\builder\freebsd;
use SPC\builder\BuilderBase;
use SPC\builder\traits\UnixBuilderTrait;
use SPC\builder\unix\UnixBuilderBase;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException;
use SPC\store\FileSystem;
use SPC\store\SourcePatcher;
class BSDBuilder extends BuilderBase
class BSDBuilder extends UnixBuilderBase
{
/** Unix compatible builder methods */
use UnixBuilderTrait;
/** @var bool Micro patch phar flag */
private bool $phar_patched = false;

View File

@@ -4,20 +4,16 @@ declare(strict_types=1);
namespace SPC\builder\linux;
use SPC\builder\BuilderBase;
use SPC\builder\linux\library\LinuxLibraryBase;
use SPC\builder\traits\UnixBuilderTrait;
use SPC\builder\unix\UnixBuilderBase;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException;
use SPC\store\FileSystem;
use SPC\store\SourcePatcher;
class LinuxBuilder extends BuilderBase
class LinuxBuilder extends UnixBuilderBase
{
/** Unix compatible builder methods */
use UnixBuilderTrait;
/** @var array Tune cflags */
public array $tune_c_flags;
@@ -173,6 +169,27 @@ class LinuxBuilder extends BuilderBase
$enableMicro = ($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO;
$enableEmbed = ($build_target & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED;
// upx pack and strip for micro
if ($this->getOption('with-upx-pack', false)) {
FileSystem::replaceFileRegex(
SOURCE_PATH . '/php-src/sapi/micro/Makefile.frag',
'/POST_MICRO_BUILD_COMMANDS=.*/',
'POST_MICRO_BUILD_COMMANDS=\$(STRIP) \$(MICRO_STRIP_FLAGS) \$(SAPI_MICRO_PATH) && ' . $this->getOption('upx-exec') . ' --best \$(SAPI_MICRO_PATH)',
);
} elseif (!$this->getOption('no-strip', false)) {
FileSystem::replaceFileRegex(
SOURCE_PATH . '/php-src/sapi/micro/Makefile.frag',
'/POST_MICRO_BUILD_COMMANDS=.*/',
'POST_MICRO_BUILD_COMMANDS=true',
);
} else {
FileSystem::replaceFileRegex(
SOURCE_PATH . '/php-src/sapi/micro/Makefile.frag',
'/POST_MICRO_BUILD_COMMANDS=.*/',
'POST_MICRO_BUILD_COMMANDS=\$(STRIP) \$(MICRO_STRIP_FLAGS) \$(SAPI_MICRO_PATH)',
);
}
shell()->cd(SOURCE_PATH . '/php-src')
->exec(
"{$this->getOption('ld_library_path')} " .
@@ -242,6 +259,10 @@ class LinuxBuilder extends BuilderBase
if (!$this->getOption('no-strip', false)) {
shell()->cd(SOURCE_PATH . '/php-src/sapi/cli')->exec('strip --strip-all php');
} elseif ($this->getOption('with-upx-pack')) {
shell()->cd(SOURCE_PATH . '/php-src/sapi/cli')
->exec('strip --strip-all php')
->exec($this->getOption('upx-exec') . ' --best php');
}
$this->deployBinary(BUILD_TARGET_CLI);
@@ -271,10 +292,6 @@ class LinuxBuilder extends BuilderBase
->exec('sed -i "s|//lib|/lib|g" Makefile')
->exec("make -j{$this->concurrency} {$vars} micro");
if (!$this->getOption('no-strip', false)) {
shell()->cd(SOURCE_PATH . '/php-src/sapi/micro')->exec('strip --strip-all micro.sfx');
}
$this->deployBinary(BUILD_TARGET_MICRO);
if ($this->phar_patched) {
@@ -297,6 +314,10 @@ class LinuxBuilder extends BuilderBase
if (!$this->getOption('no-strip', false)) {
shell()->cd(SOURCE_PATH . '/php-src/sapi/fpm')->exec('strip --strip-all php-fpm');
} elseif ($this->getOption('with-upx-pack')) {
shell()->cd(SOURCE_PATH . '/php-src/sapi/fpm')
->exec('strip --strip-all php-fpm')
->exec($this->getOption('upx-exec') . ' --best php-fpm');
}
$this->deployBinary(BUILD_TARGET_FPM);

View File

@@ -110,11 +110,7 @@ class SystemUtil
public static function getTuneCFlags(string $arch): array
{
return match ($arch) {
'x86_64' => [
'-march=corei7',
'-mtune=core-avx2',
],
'arm64', 'aarch64' => [],
'x86_64', 'arm64', 'aarch64' => [],
default => throw new RuntimeException('unsupported arch: ' . $arch),
};
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace SPC\builder\linux\library;
class gettext extends LinuxLibraryBase
{
use \SPC\builder\unix\library\gettext;
public const NAME = 'gettext';
}

View File

@@ -18,7 +18,7 @@ class libxml2 extends LinuxLibraryBase
*/
public function build(): void
{
$enable_zlib = $this->builder->getLib('zlib') ? 'ON' : 'OFF';
$enable_zlib = $this->builder->getLib('zlib') ? ('ON -DZLIB_LIBRARY=' . BUILD_LIB_PATH . '/libz.a -DZLIB_INCLUDE_DIR=' . BUILD_INCLUDE_PATH) : 'OFF';
$enable_icu = $this->builder->getLib('icu') ? 'ON' : 'OFF';
$enable_xz = $this->builder->getLib('xz') ? 'ON' : 'OFF';

View File

@@ -4,20 +4,16 @@ declare(strict_types=1);
namespace SPC\builder\macos;
use SPC\builder\BuilderBase;
use SPC\builder\macos\library\MacOSLibraryBase;
use SPC\builder\traits\UnixBuilderTrait;
use SPC\builder\unix\UnixBuilderBase;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException;
use SPC\store\FileSystem;
use SPC\store\SourcePatcher;
class MacOSBuilder extends BuilderBase
class MacOSBuilder extends UnixBuilderBase
{
/** Unix compatible builder methods */
use UnixBuilderTrait;
/** @var bool Micro patch phar flag */
private bool $phar_patched = false;

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace SPC\builder\macos\library;
class gettext extends MacOSLibraryBase
{
use \SPC\builder\unix\library\gettext;
public const NAME = 'gettext';
}

View File

@@ -18,7 +18,7 @@ class libxml2 extends MacOSLibraryBase
*/
protected function build(): void
{
$enable_zlib = $this->builder->getLib('zlib') ? 'ON' : 'OFF';
$enable_zlib = $this->builder->getLib('zlib') ? ('ON -DZLIB_LIBRARY=' . BUILD_LIB_PATH . '/libz.a -DZLIB_INCLUDE_DIR=' . BUILD_INCLUDE_PATH) : 'OFF';
$enable_icu = $this->builder->getLib('icu') ? 'ON' : 'OFF';
$enable_xz = $this->builder->getLib('xz') ? 'ON' : 'OFF';

View File

@@ -1,7 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\builder\traits;
trait LibraryTrait {}

View File

@@ -12,8 +12,6 @@ use SPC\store\FileSystem;
trait UnixLibraryTrait
{
use LibraryTrait;
/**
* @throws RuntimeException
* @throws FileSystemException

View File

@@ -2,15 +2,19 @@
declare(strict_types=1);
namespace SPC\builder\traits;
namespace SPC\builder\unix;
use SPC\builder\BuilderBase;
use SPC\builder\linux\LinuxBuilder;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException;
use SPC\store\Config;
use SPC\store\FileSystem;
use SPC\store\SourceManager;
use SPC\util\DependencyUtil;
trait UnixBuilderTrait
abstract class UnixBuilderBase extends BuilderBase
{
/** @var string cflags */
public string $arch_c_flags;
@@ -86,6 +90,65 @@ trait UnixBuilderTrait
return $extra;
}
public function buildLibs(array $sorted_libraries): void
{
// search all supported libs
$support_lib_list = [];
$classes = FileSystem::getClassesPsr4(
ROOT_DIR . '/src/SPC/builder/' . osfamily2dir() . '/library',
'SPC\\builder\\' . osfamily2dir() . '\\library'
);
foreach ($classes as $class) {
if (defined($class . '::NAME') && $class::NAME !== 'unknown' && Config::getLib($class::NAME) !== null) {
$support_lib_list[$class::NAME] = $class;
}
}
// if no libs specified, compile all supported libs
if ($sorted_libraries === [] && $this->isLibsOnly()) {
$libraries = array_keys($support_lib_list);
$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 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!');
}
$lib = new ($support_lib_list[$library])($this);
$this->addLib($lib);
}
// calculate and check dependencies
foreach ($this->libs as $lib) {
$lib->calcDependency();
}
// patch point
$this->emitPatchPoint('before-libs-extract');
// extract sources
SourceManager::initSource(libs: $sorted_libraries);
$this->emitPatchPoint('after-libs-extract');
// build all libs
foreach ($this->libs as $lib) {
match ($lib->tryBuild($this->getOption('rebuild', false))) {
BUILD_STATUS_OK => logger()->info('lib [' . $lib::NAME . '] build success'),
BUILD_STATUS_ALREADY => logger()->notice('lib [' . $lib::NAME . '] already built'),
BUILD_STATUS_FAILED => logger()->error('lib [' . $lib::NAME . '] build failed'),
default => logger()->warning('lib [' . $lib::NAME . '] build status unknown'),
};
}
}
/**
* Sanity check after build complete
*
@@ -103,7 +166,7 @@ trait UnixBuilderTrait
foreach ($this->exts as $ext) {
logger()->debug('testing ext: ' . $ext->getName());
$ext->runCliCheck();
$ext->runCliCheckUnix();
}
}
@@ -115,12 +178,17 @@ trait UnixBuilderTrait
file_put_contents(
SOURCE_PATH . '/hello.exe',
file_get_contents(SOURCE_PATH . '/php-src/sapi/micro/micro.sfx') .
'<?php echo "hello";'
($this->getOption('without-micro-ext-test') ? '<?php echo "[micro-test-start][micro-test-end]";' : $this->generateMicroExtTests())
);
chmod(SOURCE_PATH . '/hello.exe', 0755);
[$ret, $output2] = shell()->execWithResult(SOURCE_PATH . '/hello.exe');
if ($ret !== 0 || trim($out = implode('', $output2)) !== 'hello') {
throw new RuntimeException('micro failed sanity check, ret[' . $ret . '], out[' . ($out ?? 'NULL') . ']');
$raw_out = trim(implode('', $output2));
$condition[0] = $ret === 0;
$condition[1] = str_starts_with($raw_out, '[micro-test-start]') && str_ends_with($raw_out, '[micro-test-end]');
foreach ($condition as $k => $v) {
if (!$v) {
throw new RuntimeException("micro failed sanity check with condition[{$k}], ret[{$ret}], out[{$raw_out}]");
}
}
}
}

View File

@@ -52,7 +52,7 @@ trait curl
// compile
shell()->cd($this->source_dir . '/build')
->exec('sed -i.save s@\${CMAKE_C_IMPLICIT_LINK_LIBRARIES}@@ ../CMakeLists.txt')
->exec("cmake {$this->builder->makeCmakeArgs()} -DBUILD_SHARED_LIBS=OFF -DBUILD_CURL_EXE=OFF {$extra} ..")
->exec("cmake {$this->builder->makeCmakeArgs()} -DBUILD_SHARED_LIBS=OFF -DBUILD_CURL_EXE=OFF -DBUILD_LIBCURL_DOCS=OFF {$extra} ..")
->exec("make -j{$this->builder->concurrency}")
->exec('make install DESTDIR=' . BUILD_ROOT_PATH);
// patch pkgconf

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace SPC\builder\unix\library;
trait gettext
{
protected function build(): void
{
$extra = $this->builder->getLib('ncurses') ? ('--with-libncurses-prefix=' . BUILD_ROOT_PATH . ' ') : '';
$extra .= $this->builder->getLib('libxml2') ? ('--with-libxml2-prefix=' . BUILD_ROOT_PATH . ' ') : '';
shell()->cd($this->source_dir)
->exec(
'./configure ' .
'--enable-static ' .
'--disable-shared ' .
'--disable-java ' .
'--disable-c+ ' .
$extra .
'--with-libiconv-prefix=' . BUILD_ROOT_PATH . ' ' .
'--prefix=' . BUILD_ROOT_PATH
)
->exec('make clean')
->exec("make -j{$this->builder->concurrency}")
->exec('make install');
}
}

View File

@@ -4,17 +4,25 @@ declare(strict_types=1);
namespace SPC\builder\windows;
use SPC\exception\FileSystemException;
use SPC\store\FileSystem;
class SystemUtil
{
/**
* @param string $name 命令名称
* @param array $paths 寻找的目标路径(如果不传入,则使用环境变量 PATH
* @return null|string 找到了返回命令路径,找不到返回 null
* Find windows program using executable name.
*
* @param string $name command name (xxx.exe)
* @param array $paths search path (default use env path)
* @return null|string null if not found, string is absolute path
*/
public static function findCommand(string $name, array $paths = []): ?string
public static function findCommand(string $name, array $paths = [], bool $include_sdk_bin = false): ?string
{
if (!$paths) {
$paths = explode(PATH_SEPARATOR, getenv('Path'));
if ($include_sdk_bin) {
$paths[] = PHP_SDK_PATH . '\bin';
}
}
foreach ($paths as $path) {
if (file_exists($path . DIRECTORY_SEPARATOR . $name)) {
@@ -23,4 +31,74 @@ class SystemUtil
}
return null;
}
/**
* Find Visual Studio installation.
*
* @return array<string, string>|false False if not installed, array contains 'version' and 'dir'
*/
public static function findVisualStudio(): array|false
{
$check_path = [
'C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe' => 'vs17',
'C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe' => 'vs17',
'C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\MSBuild.exe' => 'vs17',
'C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\MSBuild.exe' => 'vs16',
'C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin\MSBuild.exe' => 'vs16',
'C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\MSBuild.exe' => 'vs16',
];
foreach ($check_path as $path => $vs_version) {
if (file_exists($path)) {
$vs_ver = $vs_version;
$d_dir = dirname($path, 4);
return [
'version' => $vs_ver,
'dir' => $d_dir,
];
}
}
return false;
}
/**
* Get CPU count for concurrency.
*/
public static function getCpuCount(): int
{
$result = f_exec('echo %NUMBER_OF_PROCESSORS%', $out, $code);
if ($code !== 0 || !$result) {
return 1;
}
return intval($result);
}
/**
* Create CMake toolchain file.
*
* @param null|string $cflags CFLAGS for cmake, default use '/MT /Os /Ob1 /DNDEBUG /D_ACRTIMP= /D_CRTIMP='
* @param null|string $ldflags LDFLAGS for cmake, default use '/nodefaultlib:msvcrt /nodefaultlib:msvcrtd /defaultlib:libcmt'
* @throws FileSystemException
*/
public static function makeCmakeToolchainFile(?string $cflags = null, ?string $ldflags = null): string
{
if ($cflags === null) {
$cflags = '/MT /Os /Ob1 /DNDEBUG /D_ACRTIMP= /D_CRTIMP=';
}
if ($ldflags === null) {
$ldflags = '/nodefaultlib:msvcrt /nodefaultlib:msvcrtd /defaultlib:libcmt';
}
$buildroot = str_replace('\\', '\\\\', BUILD_ROOT_PATH);
$toolchain = <<<CMAKE
set(CMAKE_SYSTEM_NAME Windows)
SET(CMAKE_C_FLAGS "{$cflags}")
SET(CMAKE_C_FLAGS_DEBUG "{$cflags}")
SET(CMAKE_CXX_FLAGS "{$cflags}")
SET(CMAKE_CXX_FLAGS_DEBUG "{$cflags}")
SET(CMAKE_EXE_LINKER_FLAGS "{$ldflags}")
SET(CMAKE_FIND_ROOT_PATH "{$buildroot}")
SET(CMAKE_MSVC_RUNTIME_LIBRARY MultiThreaded)
CMAKE;
FileSystem::writeFile(SOURCE_PATH . '\toolchain.cmake', $toolchain);
return realpath(SOURCE_PATH . '\toolchain.cmake');
}
}

View File

@@ -0,0 +1,364 @@
<?php
declare(strict_types=1);
namespace SPC\builder\windows;
use SPC\builder\BuilderBase;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException;
use SPC\store\Config;
use SPC\store\FileSystem;
use SPC\store\SourceManager;
use SPC\store\SourcePatcher;
use SPC\util\DependencyUtil;
class WindowsBuilder extends BuilderBase
{
/** @var string cmake toolchain file */
public string $cmake_toolchain_file;
public string $sdk_prefix;
private bool $zts;
/** @var bool Micro patch phar flag */
private bool $phar_patched = false;
/**
* @throws FileSystemException
*/
public function __construct(array $options = [])
{
$this->options = $options;
// ---------- set necessary options ----------
// set sdk (require visual studio 16 or 17)
$vs = SystemUtil::findVisualStudio()['version'];
$this->sdk_prefix = PHP_SDK_PATH . "\\phpsdk-{$vs}-x64.bat -t";
// set zts
$this->zts = $this->getOption('enable-zts', false);
// set concurrency
$this->concurrency = SystemUtil::getCpuCount();
// make cmake toolchain
$this->cmake_toolchain_file = SystemUtil::makeCmakeToolchainFile();
}
/**
* @throws RuntimeException
* @throws WrongUsageException
* @throws FileSystemException
*/
public function buildPHP(int $build_target = BUILD_TARGET_NONE): void
{
// ---------- Update extra-libs ----------
$extra_libs = $this->getOption('extra-libs', '');
$extra_libs .= (empty($extra_libs) ? '' : ' ') . implode(' ', $this->getAllStaticLibFiles());
$this->setOption('extra-libs', $extra_libs);
$enableCli = ($build_target & BUILD_TARGET_CLI) === BUILD_TARGET_CLI;
$enableFpm = ($build_target & BUILD_TARGET_FPM) === BUILD_TARGET_FPM;
$enableMicro = ($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO;
$enableEmbed = ($build_target & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED;
SourcePatcher::patchBeforeBuildconf($this);
cmd()->cd(SOURCE_PATH . '\php-src')->exec("{$this->sdk_prefix} buildconf.bat");
SourcePatcher::patchBeforeConfigure($this);
$zts = $this->zts ? '--enable-zts=yes ' : '--enable-zts=no ';
// with-upx-pack for phpmicro
$makefile = FileSystem::convertPath(SOURCE_PATH . '/php-src/sapi/micro/Makefile.frag.w32');
if ($this->getOption('with-upx-pack', false)) {
if (!file_exists($makefile . '.originfile')) {
copy($makefile, $makefile . '.originfile');
FileSystem::replaceFileStr($makefile, '$(MICRO_SFX):', "_MICRO_UPX = {$this->getOption('upx-exec')} --best $(MICRO_SFX)\n$(MICRO_SFX):");
FileSystem::replaceFileStr($makefile, '@$(_MICRO_MT)', "@$(_MICRO_MT)\n\t@$(_MICRO_UPX)");
}
} elseif (file_exists($makefile . '.originfile')) {
copy($makefile . '.originfile', $makefile);
unlink($makefile . '.originfile');
}
cmd()->cd(SOURCE_PATH . '\php-src')
->exec(
"{$this->sdk_prefix} configure.bat --task-args \"" .
'--disable-all ' .
'--disable-cgi ' .
'--with-php-build=' . BUILD_ROOT_PATH . ' ' .
'--with-extra-includes=' . BUILD_INCLUDE_PATH . ' ' .
'--with-extra-libs=' . BUILD_LIB_PATH . ' ' .
($enableCli ? '--enable-cli=yes ' : '--enable-cli=no ') .
($enableMicro ? '--enable-micro=yes ' : '--enable-micro=no ') .
($enableEmbed ? '--enable-embed=yes ' : '--enable-embed=no ') .
"{$this->makeExtensionArgs()} " .
$zts .
'"'
);
SourcePatcher::patchBeforeMake($this);
$this->cleanMake();
if ($enableCli) {
logger()->info('building cli');
$this->buildCli();
}
if ($enableFpm) {
logger()->warning('Windows does not support fpm SAPI, I will skip it.');
}
if ($enableMicro) {
logger()->info('building micro');
$this->buildMicro();
}
if ($enableEmbed) {
logger()->warning('Windows does not currently support embed SAPI.');
// logger()->info('building embed');
$this->buildEmbed();
}
$this->sanityCheck($build_target);
}
/**
* @throws FileSystemException
* @throws RuntimeException
*/
public function buildCli(): void
{
SourcePatcher::patchWindowsCLITarget();
// add nmake wrapper
FileSystem::writeFile(SOURCE_PATH . '\php-src\nmake_cli_wrapper.bat', "nmake /nologo LIBS_CLI=\"{$this->getOption('extra-libs')} ws2_32.lib shell32.lib\" EXTRA_LD_FLAGS_PROGRAM= %*");
cmd()->cd(SOURCE_PATH . '\php-src')->exec("{$this->sdk_prefix} nmake_cli_wrapper.bat --task-args php.exe");
$this->deployBinary(BUILD_TARGET_CLI);
}
public function buildEmbed(): void
{
// TODO: add embed support for windows
/*
FileSystem::writeFile(SOURCE_PATH . '\php-src\nmake_embed_wrapper.bat', 'nmake /nologo %*');
cmd()->cd(SOURCE_PATH . '\php-src')
->exec("{$this->sdk_prefix} nmake_embed_wrapper.bat --task-args php8embed.lib");
*/
}
/**
* @throws FileSystemException
* @throws RuntimeException
* @throws WrongUsageException
*/
public function buildMicro(): void
{
// workaround for fiber (originally from https://github.com/dixyes/lwmbs/blob/master/windows/MicroBuild.php)
$makefile = FileSystem::readFile(SOURCE_PATH . '\php-src\Makefile');
if ($this->getPHPVersionID() >= 80200 && str_contains($makefile, 'FIBER_ASM_ARCH')) {
$makefile .= "\r\n" . '$(MICRO_SFX): $(BUILD_DIR)\Zend\jump_$(FIBER_ASM_ARCH)_ms_pe_masm.obj $(BUILD_DIR)\Zend\make_$(FIBER_ASM_ARCH)_ms_pe_masm.obj' . "\r\n\r\n";
} elseif ($this->getPHPVersionID() >= 80400 && str_contains($makefile, 'FIBER_ASM_ABI')) {
$makefile .= "\r\n" . '$(MICRO_SFX): $(BUILD_DIR)\Zend\jump_$(FIBER_ASM_ABI).obj $(BUILD_DIR)\Zend\make_$(FIBER_ASM_ABI).obj' . "\r\n\r\n";
}
FileSystem::writeFile(SOURCE_PATH . '\php-src\Makefile', $makefile);
// add nmake wrapper
$fake_cli = $this->getOption('with-micro-fake-cli', false) ? ' /DPHP_MICRO_FAKE_CLI" ' : '';
$wrapper = "nmake /nologo LIBS_MICRO=\"{$this->getOption('extra-libs')} ws2_32.lib shell32.lib\" CFLAGS_MICRO=\"/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1{$fake_cli}\" %*";
FileSystem::writeFile(SOURCE_PATH . '\php-src\nmake_micro_wrapper.bat', $wrapper);
// phar patch for micro
if ($this->getExt('phar')) {
$this->phar_patched = true;
SourcePatcher::patchMicro(['phar']);
}
cmd()->cd(SOURCE_PATH . '\php-src')->exec("{$this->sdk_prefix} nmake_micro_wrapper.bat --task-args micro");
if ($this->phar_patched) {
SourcePatcher::patchMicro(['phar'], true);
}
$this->deployBinary(BUILD_TARGET_MICRO);
}
public function buildLibs(array $sorted_libraries): void
{
// search all supported libs
$support_lib_list = [];
$classes = FileSystem::getClassesPsr4(
ROOT_DIR . '\src\SPC\builder\\' . osfamily2dir() . '\\library',
'SPC\\builder\\' . osfamily2dir() . '\\library'
);
foreach ($classes as $class) {
if (defined($class . '::NAME') && $class::NAME !== 'unknown' && Config::getLib($class::NAME) !== null) {
$support_lib_list[$class::NAME] = $class;
}
}
// if no libs specified, compile all supported libs
if ($sorted_libraries === [] && $this->isLibsOnly()) {
$libraries = array_keys($support_lib_list);
$sorted_libraries = DependencyUtil::getLibs($libraries);
}
// add lib object for builder
foreach ($sorted_libraries as $library) {
// 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!');
}
$lib = new ($support_lib_list[$library])($this);
$this->addLib($lib);
}
// calculate and check dependencies
foreach ($this->libs as $lib) {
$lib->calcDependency();
}
// extract sources
SourceManager::initSource(libs: $sorted_libraries);
// build all libs
foreach ($this->libs as $lib) {
match ($lib->tryBuild($this->getOption('rebuild', false))) {
BUILD_STATUS_OK => logger()->info('lib [' . $lib::NAME . '] build success'),
BUILD_STATUS_ALREADY => logger()->notice('lib [' . $lib::NAME . '] already built'),
BUILD_STATUS_FAILED => logger()->error('lib [' . $lib::NAME . '] build failed'),
default => logger()->warning('lib [' . $lib::NAME . '] build status unknown'),
};
}
}
/**
* @throws FileSystemException
* @throws RuntimeException
*/
public function cleanMake(): void
{
FileSystem::writeFile(SOURCE_PATH . '\php-src\nmake_clean_wrapper.bat', 'nmake /nologo %*');
cmd()->cd(SOURCE_PATH . '\php-src')->exec("{$this->sdk_prefix} nmake_clean_wrapper.bat --task-args \"clean\"");
}
/**
* Run extension and PHP cli and micro check
*
* @throws RuntimeException
*/
public function sanityCheck(mixed $build_target): void
{
// 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\";"');
if ($ret !== 0 || trim(implode('', $output)) !== 'hello') {
throw new RuntimeException('cli failed sanity check');
}
foreach ($this->exts as $ext) {
logger()->debug('testing ext: ' . $ext->getName());
$ext->runCliCheckWindows();
}
}
// sanity check for phpmicro
if (($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO) {
if (file_exists(SOURCE_PATH . '\hello.exe')) {
@unlink(SOURCE_PATH . '\hello.exe');
}
file_put_contents(
SOURCE_PATH . '\hello.exe',
file_get_contents(BUILD_ROOT_PATH . '\bin\micro.sfx') .
($this->getOption('without-micro-ext-test') ? '<?php echo "[micro-test-start][micro-test-end]";' : $this->generateMicroExtTests())
);
chmod(SOURCE_PATH . '\hello.exe', 0755);
[$ret, $output2] = cmd()->execWithResult(SOURCE_PATH . '\hello.exe');
$raw_out = trim(implode('', $output2));
$condition[0] = $ret === 0;
$condition[1] = str_starts_with($raw_out, '[micro-test-start]') && str_ends_with($raw_out, '[micro-test-end]');
foreach ($condition as $k => $v) {
if (!$v) {
throw new RuntimeException("micro failed sanity check with condition[{$k}], ret[{$ret}], out[{$raw_out}]");
}
}
}
}
/**
* 将编译好的二进制文件发布到 buildroot
*
* @param int $type 发布类型
* @throws RuntimeException
* @throws FileSystemException
*/
public function deployBinary(int $type): bool
{
$ts = $this->zts ? '_TS' : '';
$src = match ($type) {
BUILD_TARGET_CLI => SOURCE_PATH . "\\php-src\\x64\\Release{$ts}\\php.exe",
BUILD_TARGET_MICRO => SOURCE_PATH . "\\php-src\\x64\\Release{$ts}\\micro.sfx",
default => throw new RuntimeException('Deployment does not accept type ' . $type),
};
// with-upx-pack for cli
if ($this->getOption('with-upx-pack', false) && $type === BUILD_TARGET_CLI) {
cmd()->exec($this->getOption('upx-exec') . ' --best ' . escapeshellarg($src));
}
logger()->info('Deploying ' . $this->getBuildTypeName($type) . ' file');
FileSystem::createDir(BUILD_ROOT_PATH . '\bin');
cmd()->exec('copy ' . escapeshellarg($src) . ' ' . escapeshellarg(BUILD_ROOT_PATH . '\bin\\'));
return true;
}
/**
* @throws WrongUsageException
* @throws FileSystemException
*/
public function getAllStaticLibFiles(): array
{
$libs = [];
// reorder libs
foreach ($this->libs as $lib) {
foreach ($lib->getDependencies() as $dep) {
$libs[] = $dep;
}
$libs[] = $lib;
}
$libFiles = [];
$libNames = [];
// merge libs
foreach ($libs as $lib) {
if (!in_array($lib::NAME, $libNames, true)) {
$libNames[] = $lib::NAME;
array_unshift($libFiles, ...$lib->getStaticLibs());
}
}
return $libFiles;
}
/**
* Generate command wrapper prefix for php-sdk internal commands.
*
* @param string $internal_cmd command in php-sdk-tools\bin
* @return string Example: C:\php-sdk-tools\phpsdk-vs17-x64.bat -t source\cmake_wrapper.bat --task-args
*/
public function makeSimpleWrapper(string $internal_cmd): string
{
$wrapper_bat = SOURCE_PATH . '\\' . crc32($internal_cmd) . '_wrapper.bat';
if (!file_exists($wrapper_bat)) {
file_put_contents($wrapper_bat, $internal_cmd . ' %*');
}
return "{$this->sdk_prefix} {$wrapper_bat} --task-args";
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace SPC\builder\windows\library;
use SPC\builder\BuilderBase;
use SPC\builder\LibraryBase;
use SPC\builder\windows\WindowsBuilder;
use SPC\exception\FileSystemException;
use SPC\store\FileSystem;
abstract class WindowsLibraryBase extends LibraryBase
{
public function __construct(protected WindowsBuilder $builder)
{
parent::__construct();
}
public function getBuilder(): BuilderBase
{
return $this->builder;
}
/**
* Create a nmake wrapper file.
*
* @param string $content nmake wrapper content
* @param string $default_filename default nmake wrapper filename
* @throws FileSystemException
*/
public function makeNmakeWrapper(string $content, string $default_filename = ''): string
{
if ($default_filename === '') {
$default_filename = $this->source_dir . '\nmake_wrapper.bat';
}
FileSystem::writeFile($default_filename, $content);
return 'nmake_wrapper.bat';
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace SPC\builder\windows\library;
use SPC\builder\windows\SystemUtil;
use SPC\exception\RuntimeException;
use SPC\store\FileSystem;
class openssl extends WindowsLibraryBase
{
public const NAME = 'openssl';
protected function build(): void
{
$perl_path_native = PKG_ROOT_PATH . '\strawberry-perl-' . arch2gnu(php_uname('m')) . '-win\perl\bin\perl.exe';
$perl = file_exists($perl_path_native) ? ($perl_path_native) : SystemUtil::findCommand('perl.exe');
if ($perl === null) {
throw new RuntimeException('You need to install perl first! (easiest way is using static-php-cli command "doctor")');
}
cmd()->cd($this->source_dir)
->execWithWrapper(
$this->builder->makeSimpleWrapper($perl),
'Configure zlib VC-WIN64A ' .
'no-shared ' .
'--prefix=' . quote(BUILD_ROOT_PATH) . ' ' .
'--with-zlib-lib=' . quote(BUILD_LIB_PATH) . ' ' .
'--with-zlib-include=' . quote(BUILD_INCLUDE_PATH) . ' ' .
'--release ' .
'no-legacy '
);
// patch zlib
FileSystem::replaceFileStr($this->source_dir . '\Makefile', 'ZLIB1', 'zlibstatic.lib');
// patch debug: https://stackoverflow.com/questions/18486243/how-do-i-build-openssl-statically-linked-against-windows-runtime
FileSystem::replaceFileStr($this->source_dir . '\Makefile', '/debug', '/incremental:no /opt:icf /dynamicbase /nxcompat /ltcg /nodefaultlib:msvcrt');
cmd()->cd($this->source_dir)->execWithWrapper(
$this->builder->makeSimpleWrapper('nmake'),
'install_dev ' .
'CNF_LDFLAGS="/NODEFAULTLIB:kernel32.lib /NODEFAULTLIB:msvcrt /NODEFAULTLIB:msvcrtd /DEFAULTLIB:libcmt /LIBPATH:' . BUILD_LIB_PATH . ' zlibstatic.lib"'
);
copy($this->source_dir . '\ms\applink.c', BUILD_INCLUDE_PATH . '\openssl\applink.c');
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace SPC\builder\windows\library;
use SPC\store\FileSystem;
class zlib extends WindowsLibraryBase
{
public const NAME = 'zlib';
protected function build(): void
{
// reset cmake
FileSystem::resetDir($this->source_dir . '\build');
// start build
cmd()->cd($this->source_dir)
->execWithWrapper(
$this->builder->makeSimpleWrapper('cmake'),
'-B build ' .
'-A x64 ' .
"-DCMAKE_TOOLCHAIN_FILE={$this->builder->cmake_toolchain_file} " .
'-DCMAKE_BUILD_TYPE=Release ' .
'-DBUILD_SHARED_LIBS=OFF ' .
'-DSKIP_INSTALL_FILES=ON ' .
'-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' '
)
->execWithWrapper(
$this->builder->makeSimpleWrapper('cmake'),
"--build build --config Release --target install -j{$this->builder->concurrency}"
);
copy(BUILD_LIB_PATH . '\zlibstatic.lib', BUILD_LIB_PATH . '\zlib_a.lib');
unlink(BUILD_ROOT_PATH . '\bin\zlib.dll');
unlink(BUILD_LIB_PATH . '\zlib.lib');
}
}

View File

@@ -56,11 +56,6 @@ abstract class BaseCommand extends Command
// 如果 return false 则错误会继续递交给 PHP 标准错误处理
return true;
}, E_ALL | E_STRICT);
if ($input->getOption('debug')) {
global $ob_logger;
$ob_logger = new ConsoleLogger(LogLevel::DEBUG);
define('DEBUG_MODE', true);
}
$version = ConsoleApplication::VERSION;
if (!$this->no_motd) {
echo " _ _ _ _
@@ -83,6 +78,14 @@ abstract class BaseCommand extends Command
$this->input = $input;
$this->output = $output;
global $ob_logger;
if ($input->getOption('debug')) {
$ob_logger = new ConsoleLogger(LogLevel::DEBUG, decorated: !$input->getOption('no-ansi'));
define('DEBUG_MODE', true);
} else {
$ob_logger = new ConsoleLogger(decorated: !$input->getOption('no-ansi'));
}
// windows fallback
Prompt::fallbackWhen(PHP_OS_FAMILY === 'Windows');
ConfirmPrompt::fallbackUsing(function (ConfirmPrompt $prompt) use ($input, $output) {

View File

@@ -23,19 +23,22 @@ class BuildCliCommand extends BuildCommand
{
$this->addArgument('extensions', InputArgument::REQUIRED, 'The extensions will be compiled, comma separated');
$this->addOption('with-libs', null, InputOption::VALUE_REQUIRED, 'add additional libraries, comma separated', '');
$this->addOption('build-micro', null, null, 'build micro');
$this->addOption('build-cli', null, null, 'build cli');
$this->addOption('build-fpm', null, null, 'build fpm');
$this->addOption('build-embed', null, null, 'build embed');
$this->addOption('build-all', null, null, 'build cli, micro, fpm, embed');
$this->addOption('build-micro', null, null, 'Build micro SAPI');
$this->addOption('build-cli', null, null, 'Build cli SAPI');
$this->addOption('build-fpm', null, null, 'Build fpm SAPI');
$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-hardcoded-ini', 'I', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Patch PHP source code, inject hardcoded INI');
$this->addOption('with-micro-fake-cli', null, null, 'Enable phpmicro fake cli');
$this->addOption('with-micro-fake-cli', null, null, 'Let phpmicro\'s PHP_SAPI use "cli" instead of "micro"');
$this->addOption('with-suggested-libs', 'L', null, 'Build with suggested libs for selected exts and libs');
$this->addOption('with-suggested-exts', 'E', null, 'Build with suggested extensions for selected exts');
$this->addOption('with-added-patch', 'P', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Inject patch script outside');
$this->addOption('without-micro-ext-test', null, null, 'Disable phpmicro with extension test code');
$this->addOption('with-upx-pack', null, null, 'Compress / pack binary using UPX tool (linux/windows only)');
}
public function handle(): int
@@ -57,11 +60,37 @@ class BuildCliCommand extends BuildCommand
$this->output->writeln("<comment>\t--build-all\tBuild all SAPI: cli, micro, fpm, embed</comment>");
return static::FAILURE;
}
if ($rule === BUILD_TARGET_ALL) {
logger()->warning('--build-all option makes `--no-strip` always true, be aware!');
}
// Check upx
$suffix = PHP_OS_FAMILY === 'Windows' ? '.exe' : '';
if ($this->getOption('with-upx-pack')) {
// only available for linux for now
if (!in_array(PHP_OS_FAMILY, ['Linux', 'Windows'])) {
logger()->error('UPX is only available on Linux and Windows!');
return static::FAILURE;
}
// need to install this manually
if (!file_exists(PKG_ROOT_PATH . '/bin/upx' . $suffix)) {
global $argv;
logger()->error('upx does not exist, please install it first:');
logger()->error('');
logger()->error("\t" . $argv[0] . ' install-pkg upx');
logger()->error('');
return static::FAILURE;
}
// exclusive with no-strip
if ($this->getOption('no-strip')) {
logger()->warning('--with-upx-pack conflicts with --no-strip, --no-strip won\'t work!');
}
}
try {
// create builder
$builder = BuilderProvider::makeBuilderByInput($this->input);
// calculate dependencies
[$extensions, $libraries, $not_included] = DependencyUtil::getExtLibsByDeps($extensions, $libraries);
$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);
// print info
$indent_texts = [
@@ -75,12 +104,27 @@ class BuildCliCommand extends BuildCommand
if (!empty($this->input->getOption('with-hardcoded-ini'))) {
$indent_texts['Hardcoded INI'] = $this->input->getOption('with-hardcoded-ini');
}
$this->printFormatInfo($indent_texts);
if ($this->input->getOption('disable-opcache-jit')) {
$indent_texts['Opcache JIT'] = 'disabled';
}
if ($this->input->getOption('with-upx-pack') && in_array(PHP_OS_FAMILY, ['Linux', 'Windows'])) {
$indent_texts['UPX Pack'] = 'enabled';
$builder->setOption('upx-exec', FileSystem::convertPath(PKG_ROOT_PATH . '/bin/upx' . $suffix));
}
try {
$ver = $builder->getPHPVersion();
$indent_texts['PHP Version'] = $ver;
} catch (\Throwable) {
if (($ver = $builder->getPHPVersionFromArchive()) !== false) {
$indent_texts['PHP Version'] = $ver;
}
}
if (!empty($not_included)) {
logger()->warning('Some extensions will be enabled due to dependencies: ' . implode(',', $not_included));
$indent_texts['Extra Exts (' . count($not_included) . ')'] = implode(', ', $not_included);
}
logger()->info('Build will start after 2s ...');
$this->printFormatInfo($indent_texts);
logger()->notice('Build will start after 2s ...');
sleep(2);
if ($this->input->getOption('with-clean')) {
@@ -103,6 +147,9 @@ class BuildCliCommand extends BuildCommand
SourcePatcher::patchHardcodedINI($custom_ini);
}
// add static-php-cli.version to main.c, in order to debug php failure more easily
SourcePatcher::patchSPCVersionToPHP($this->getApplication()->getVersion());
// start to build
$builder->buildPHP($rule);
@@ -120,13 +167,17 @@ class BuildCliCommand extends BuildCommand
$fixed = ' (host system)';
}
if (($rule & BUILD_TARGET_CLI) === BUILD_TARGET_CLI) {
logger()->info('Static php binary path' . $fixed . ': ' . $build_root_path . '/bin/php');
$win_suffix = PHP_OS_FAMILY === 'Windows' ? '.exe' : '';
$path = FileSystem::convertPath("{$build_root_path}/bin/php{$win_suffix}");
logger()->info("Static php binary path{$fixed}: {$path}");
}
if (($rule & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO) {
logger()->info('phpmicro binary path' . $fixed . ': ' . $build_root_path . '/bin/micro.sfx');
$path = FileSystem::convertPath("{$build_root_path}/bin/micro.sfx");
logger()->info("phpmicro binary path{$fixed}: {$path}");
}
if (($rule & BUILD_TARGET_FPM) === BUILD_TARGET_FPM) {
logger()->info('Static php-fpm binary path' . $fixed . ': ' . $build_root_path . '/bin/php-fpm');
if (($rule & BUILD_TARGET_FPM) === BUILD_TARGET_FPM && PHP_OS_FAMILY !== 'Windows') {
$path = FileSystem::convertPath("{$build_root_path}/bin/php-fpm");
logger()->info("Static php-fpm binary path{$fixed}: {$path}");
}
// export metadata
@@ -135,7 +186,8 @@ class BuildCliCommand extends BuildCommand
// export licenses
$dumper = new LicenseDumper();
$dumper->addExts($extensions)->addLibs($libraries)->addSources(['php-src'])->dump(BUILD_ROOT_PATH . '/license');
logger()->info('License path' . $fixed . ': ' . $build_root_path . '/license/');
$path = FileSystem::convertPath("{$build_root_path}/license/");
logger()->info("License path{$fixed}: {$path}");
return static::SUCCESS;
} catch (WrongUsageException $e) {
// WrongUsageException is not an exception, it's a user error, so we just print the error message

View File

@@ -60,7 +60,7 @@ class BuildLibsCommand extends BuildCommand
// 只编译 library 的情况下,标记
$builder->setLibsOnly();
// 编译和检查库完整
$libraries = DependencyUtil::getLibsByDeps($libraries);
$libraries = DependencyUtil::getLibs($libraries);
$builder->buildLibs($libraries);
$time = round(microtime(true) - START_TIME, 3);

View File

@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace SPC\command;
use SPC\exception\DownloaderException;
use SPC\exception\FileSystemException;
use SPC\exception\WrongUsageException;
use SPC\store\FileSystem;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand('del-download', 'Remove locked download source or package using name', ['delete-download', 'del-down'])]
class DeleteDownloadCommand extends BaseCommand
{
public function configure(): void
{
$this->addArgument('sources', InputArgument::REQUIRED, 'The sources/packages will be deleted, comma separated');
$this->addOption('all', 'A', null, 'Delete all downloaded and locked sources/packages');
}
public function initialize(InputInterface $input, OutputInterface $output): void
{
if ($input->getOption('all')) {
$input->setArgument('sources', '');
}
parent::initialize($input, $output);
}
/**
* @throws FileSystemException
*/
public function handle(): int
{
try {
// get source list that will be downloaded
$sources = array_map('trim', array_filter(explode(',', $this->getArgument('sources'))));
if (empty($sources)) {
logger()->notice('Removing downloads/ directory ...');
FileSystem::removeDir(DOWNLOAD_PATH);
logger()->info('Removed downloads/ dir!');
return static::SUCCESS;
}
$chosen_sources = $sources;
$lock = json_decode(FileSystem::readFile(DOWNLOAD_PATH . '/.lock.json'), true) ?? [];
foreach ($chosen_sources as $source) {
$source = trim($source);
if (!isset($lock[$source])) {
logger()->warning("Source/Package [{$source}] not locked or not downloaded, skipped.");
continue;
}
// remove download file/dir if exists
if ($lock[$source]['source_type'] === 'archive') {
if (file_exists($path = FileSystem::convertPath(DOWNLOAD_PATH . '/' . $lock[$source]['filename']))) {
logger()->info('Deleting file ' . $path);
unlink($path);
} else {
logger()->warning("Source/Package [{$source}] file not found, skip deleting file.");
}
} else {
if (is_dir($path = FileSystem::convertPath(DOWNLOAD_PATH . '/' . $lock[$source]['dirname']))) {
logger()->info('Deleting dir ' . $path);
FileSystem::removeDir($path);
} else {
logger()->warning("Source/Package [{$source}] directory not found, skip deleting dir.");
}
}
// remove locked sources
unset($lock[$source]);
}
FileSystem::writeFile(DOWNLOAD_PATH . '/.lock.json', json_encode($lock, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
logger()->info('Delete success!');
return static::SUCCESS;
} catch (DownloaderException $e) {
logger()->error($e->getMessage());
return static::FAILURE;
} catch (WrongUsageException $e) {
logger()->critical($e->getMessage());
return static::FAILURE;
}
}
}

View File

@@ -116,6 +116,7 @@ class DownloadCommand extends BaseCommand
// get source list that will be downloaded
$sources = array_map('trim', array_filter(explode(',', $this->getArgument('sources'))));
if (empty($sources)) {
logger()->notice('Downloading with --all option will take more times to download, we recommend you to download with --for-extensions option !');
$sources = array_keys(Config::getSources());
}
}
@@ -213,7 +214,7 @@ class DownloadCommand extends BaseCommand
*/
private function calculateSourcesByExt(array $extensions, bool $include_suggests = true): array
{
[$extensions, $libraries] = $include_suggests ? DependencyUtil::getAllExtLibsByDeps($extensions) : DependencyUtil::getExtLibsByDeps($extensions);
[$extensions, $libraries] = $include_suggests ? DependencyUtil::getExtsAndLibs($extensions, [], true, true) : DependencyUtil::getExtsAndLibs($extensions);
$sources = [];
foreach ($extensions as $extension) {
if (Config::getExt($extension, 'type') === 'external') {

View File

@@ -22,8 +22,8 @@ class DumpLicenseCommand extends BaseCommand
{
$this->addOption('for-extensions', null, InputOption::VALUE_REQUIRED, 'Dump by extensions and related libraries', null);
$this->addOption('without-php', null, InputOption::VALUE_NONE, 'Dump without php-src');
$this->addOption('by-libs', null, InputOption::VALUE_REQUIRED, 'Dump by libraries', null);
$this->addOption('by-sources', null, InputOption::VALUE_REQUIRED, 'Dump by original sources (source.json)', null);
$this->addOption('for-libs', null, InputOption::VALUE_REQUIRED, 'Dump by libraries', null);
$this->addOption('for-sources', null, InputOption::VALUE_REQUIRED, 'Dump by original sources (source.json)', null);
$this->addOption('dump-dir', null, InputOption::VALUE_REQUIRED, 'Change dump directory', BUILD_ROOT_PATH . '/license');
}
@@ -39,7 +39,7 @@ class DumpLicenseCommand extends BaseCommand
// 从参数中获取要编译的 extensions并转换为数组
$extensions = array_map('trim', array_filter(explode(',', $this->getOption('for-extensions'))));
// 根据提供的扩展列表获取依赖库列表并编译
[$extensions, $libraries] = DependencyUtil::getExtLibsByDeps($extensions);
[$extensions, $libraries] = DependencyUtil::getExtsAndLibs($extensions);
$dumper->addExts($extensions);
$dumper->addLibs($libraries);
if (!$this->getOption('without-php')) {
@@ -52,22 +52,22 @@ class DumpLicenseCommand extends BaseCommand
$this->output->writeln('Dump target dir: ' . $this->getOption('dump-dir'));
return static::SUCCESS;
}
if ($this->getOption('by-libs') !== null) {
$libraries = array_map('trim', array_filter(explode(',', $this->getOption('by-libs'))));
$libraries = DependencyUtil::getLibsByDeps($libraries);
if ($this->getOption('for-libs') !== null) {
$libraries = array_map('trim', array_filter(explode(',', $this->getOption('for-libs'))));
$libraries = DependencyUtil::getLibs($libraries);
$dumper->addLibs($libraries);
$dumper->dump($this->getOption('dump-dir'));
$this->output->writeln('Dump target dir: ' . $this->getOption('dump-dir'));
return static::SUCCESS;
}
if ($this->getOption('by-sources') !== null) {
$sources = array_map('trim', array_filter(explode(',', $this->getOption('by-sources'))));
if ($this->getOption('for-sources') !== null) {
$sources = array_map('trim', array_filter(explode(',', $this->getOption('for-sources'))));
$dumper->addSources($sources);
$dumper->dump($this->getOption('dump-dir'));
$this->output->writeln('Dump target dir: ' . $this->getOption('dump-dir'));
return static::SUCCESS;
}
$this->output->writeln('You must use one of "--for-extensions=", "--by-libs=", "--by-sources=" to dump');
$this->output->writeln('You must use one of "--for-extensions=", "--for-libs=", "--for-sources=" to dump');
return static::FAILURE;
}
}

View File

@@ -8,11 +8,11 @@ use SPC\builder\traits\UnixSystemUtilTrait;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException;
use SPC\store\SourceExtractor;
use SPC\store\SourceManager;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
#[AsCommand('extract', 'Extract required sources')]
#[AsCommand('extract', 'Extract required sources', ['extract-source'])]
class ExtractCommand extends BaseCommand
{
use UnixSystemUtilTrait;
@@ -34,7 +34,7 @@ class ExtractCommand extends BaseCommand
$this->output->writeln('<error>sources cannot be empty, at least contain one !</error>');
return static::FAILURE;
}
SourceExtractor::initSource(sources: $sources);
SourceManager::initSource(sources: $sources);
logger()->info('Extract done !');
return static::SUCCESS;
}

View File

@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace SPC\command;
use SPC\builder\traits\UnixSystemUtilTrait;
use SPC\exception\DownloaderException;
use SPC\exception\FileSystemException;
use SPC\exception\WrongUsageException;
use SPC\store\Config;
use SPC\store\PackageManager;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand('install-pkg', 'Install additional packages', ['i', 'install-package'])]
class InstallPkgCommand extends BaseCommand
{
use UnixSystemUtilTrait;
public function configure(): void
{
$this->addArgument('packages', InputArgument::REQUIRED, 'The packages will be installed, comma separated');
$this->addOption('shallow-clone', null, null, 'Clone shallow');
$this->addOption('custom-url', 'U', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Specify custom source download url, e.g "php-src:https://downloads.php.net/~eric/php-8.3.0beta1.tar.gz"');
}
/**
* @throws FileSystemException
*/
public function handle(): int
{
try {
// Use shallow-clone can reduce git resource download
if ($this->getOption('shallow-clone')) {
define('GIT_SHALLOW_CLONE', true);
}
// Process -U options
$custom_urls = [];
foreach ($this->input->getOption('custom-url') as $value) {
[$pkg_name, $url] = explode(':', $value, 2);
$custom_urls[$pkg_name] = $url;
}
$chosen_pkgs = array_map('trim', array_filter(explode(',', $this->getArgument('packages'))));
// Download them
f_mkdir(DOWNLOAD_PATH);
$ni = 0;
$cnt = count($chosen_pkgs);
foreach ($chosen_pkgs as $pkg) {
++$ni;
if (isset($custom_urls[$pkg])) {
$config = Config::getPkg($pkg);
$new_config = [
'type' => 'url',
'url' => $custom_urls[$pkg],
];
if (isset($config['extract'])) {
$new_config['extract'] = $config['extract'];
}
if (isset($config['filename'])) {
$new_config['filename'] = $config['filename'];
}
logger()->info("Installing source {$pkg} from custom url [{$ni}/{$cnt}]");
PackageManager::installPackage($pkg, $new_config);
} else {
logger()->info("Fetching package {$pkg} [{$ni}/{$cnt}]");
PackageManager::installPackage($pkg, Config::getPkg($pkg));
}
}
$time = round(microtime(true) - START_TIME, 3);
logger()->info('Install packages complete, used ' . $time . ' s !');
return static::SUCCESS;
} catch (DownloaderException $e) {
logger()->error($e->getMessage());
return static::FAILURE;
} catch (WrongUsageException $e) {
logger()->critical($e->getMessage());
return static::FAILURE;
}
}
}

View File

@@ -87,6 +87,10 @@ class MicroCombineCommand extends BaseCommand
// 8. Combine !
$output = FileSystem::isRelativePath($output) ? (WORKING_DIR . '/' . $output) : $output;
$file_target = file_get_contents($micro_file) . $ini_part . file_get_contents($file);
if (PHP_OS_FAMILY === 'Windows' && !str_ends_with(strtolower($output), '.exe')) {
$output .= '.exe';
}
$output = FileSystem::convertPath($output);
$result = file_put_contents($output, $file_target);
if ($result === false) {
$this->output->writeln('<error>Combine failed.</error>');

View File

@@ -15,8 +15,6 @@ use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Style\SymfonyStyle;
use function Laravel\Prompts\table;
#[AsCommand('dev:extensions', 'Helper command that lists available extension details', ['list-ext'])]
class AllExtCommand extends BaseCommand
{
@@ -61,7 +59,7 @@ class AllExtCommand extends BaseCommand
}
try {
[, $libraries, $not_included] = DependencyUtil::getExtLibsByDeps([$extension]);
[, $libraries, $not_included] = DependencyUtil::getExtsAndLibs([$extension]);
} catch (WrongUsageException) {
$libraries = $not_included = [];
}
@@ -88,7 +86,8 @@ class AllExtCommand extends BaseCommand
if ($data === []) {
$style->warning('Unknown extension selected: ' . implode(',', $extensions));
} else {
table($columns, $data);
$func = PHP_OS_FAMILY === 'Windows' ? [$style, 'table'] : '\Laravel\Prompts\table';
call_user_func($func, $columns, $data);
}
return static::SUCCESS;

View File

@@ -57,6 +57,15 @@ class SortConfigCommand extends BaseCommand
return static::FAILURE;
}
break;
case 'pkg':
$file = json_decode(FileSystem::readFile(ROOT_DIR . '/config/pkg.json'), true);
ConfigValidator::validatePkgs($file);
ksort($file);
if (!file_put_contents(ROOT_DIR . '/config/pkg.json', json_encode($file, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n")) {
$this->output->writeln('<error>Write file pkg.json failed!</error>');
return static::FAILURE;
}
break;
default:
$this->output->writeln("<error>invalid config name: {$name}</error>");
return 1;

View File

@@ -57,7 +57,7 @@ class BSDToolCheckList
$prefix = '';
}
try {
shell(true)->exec("ASSUME_ALWAYS_YES=yes {$prefix} pkg install -y " . implode(' ', $missing));
shell(true)->exec("ASSUME_ALWAYS_YES=yes {$prefix}pkg install -y " . implode(' ', $missing));
} catch (RuntimeException) {
return false;
}

View File

@@ -14,6 +14,7 @@ use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException;
use SPC\store\Downloader;
use SPC\store\FileSystem;
use SPC\store\PackageManager;
class LinuxMuslCheck
{
@@ -84,7 +85,6 @@ class LinuxMuslCheck
/** @noinspection PhpUnused */
/**
* @throws DownloaderException
* @throws FileSystemException
* @throws WrongUsageException
*/
@@ -98,15 +98,10 @@ class LinuxMuslCheck
logger()->warning('Current user is not root, using sudo for running command');
}
$arch = arch2gnu(php_uname('m'));
$musl_compile_source = [
'type' => 'url',
'url' => "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/{$arch}-musl-toolchain.tgz",
];
logger()->info('Downloading ' . $musl_compile_source['url']);
Downloader::downloadSource('musl-compile', $musl_compile_source);
logger()->info('Extracting musl-cross');
FileSystem::extractSource('musl-compile', DOWNLOAD_PATH . "/{$arch}-musl-toolchain.tgz");
shell()->exec($prefix . 'cp -rf ' . SOURCE_PATH . '/musl-compile/* /usr/local/musl');
PackageManager::installPackage("musl-toolchain-{$arch}-linux");
$pkg_root = PKG_ROOT_PATH . "/musl-toolchain-{$arch}-linux";
shell()->exec("{$prefix}cp -rf {$pkg_root}/* /usr/local/musl");
FileSystem::removeDir($pkg_root);
return true;
} catch (RuntimeException) {
return false;

View File

@@ -31,6 +31,7 @@ class MacOSToolCheckList
'gzip',
'bzip2',
'cmake',
'glibtoolize',
];
#[AsCheckItem('if homebrew has installed', limit_os: 'Darwin', level: 998)]
@@ -72,8 +73,14 @@ class MacOSToolCheckList
#[AsFixItem('build-tools')]
public function fixBuildTools(array $missing): bool
{
$replacement = [
'glibtoolize' => 'libtool',
];
foreach ($missing as $cmd) {
try {
if (isset($replacement[$cmd])) {
$cmd = $replacement[$cmd];
}
shell(true)->exec('brew install --formula ' . escapeshellarg($cmd));
} catch (RuntimeException) {
return false;

View File

@@ -9,10 +9,22 @@ use SPC\doctor\AsCheckItem;
use SPC\doctor\AsFixItem;
use SPC\doctor\CheckResult;
use SPC\exception\RuntimeException;
use SPC\store\FileSystem;
use SPC\store\PackageManager;
class WindowsToolCheckList
{
#[AsCheckItem('if git are installed', limit_os: 'Windows', level: 999)]
#[AsCheckItem('if Visual Studio are installed', limit_os: 'Windows', level: 999)]
public function checkVS(): ?CheckResult
{
$vs_ver = SystemUtil::findVisualStudio();
if ($vs_ver === false) {
return CheckResult::fail('Visual Studio not installed, please install VS 2022/2019.');
}
return CheckResult::ok($vs_ver['version'] . ' ' . $vs_ver['dir']);
}
#[AsCheckItem('if git are installed', limit_os: 'Windows', level: 998)]
public function checkGit(): ?CheckResult
{
if (SystemUtil::findCommand('git.exe') === null) {
@@ -22,7 +34,7 @@ class WindowsToolCheckList
return CheckResult::ok();
}
#[AsCheckItem('if php-sdk-binary-tools are downloaded', limit_os: 'Windows', level: 998)]
#[AsCheckItem('if php-sdk-binary-tools are downloaded', limit_os: 'Windows', level: 997)]
public function checkSDK(): ?CheckResult
{
if (!file_exists(PHP_SDK_PATH . DIRECTORY_SEPARATOR . 'phpsdk-starter.bat')) {
@@ -31,14 +43,64 @@ class WindowsToolCheckList
return CheckResult::ok(PHP_SDK_PATH);
}
#[AsCheckItem('if git associated command exists', limit_os: 'Windows', level: 996)]
public function checkGitPatch(): ?CheckResult
{
if (($path = SystemUtil::findCommand('patch.exe')) === null) {
return CheckResult::fail('Git patch (minGW command) not found in path. You need to add "C:\Program Files\Git\usr\bin" in Path.');
}
return CheckResult::ok();
}
#[AsCheckItem('if nasm installed', limit_os: 'Windows', level: 995)]
public function checkNasm(): ?CheckResult
{
if (SystemUtil::findCommand('nasm.exe', include_sdk_bin: true) === null) {
return CheckResult::fail('nasm.exe not found in path.', 'install-nasm');
}
return CheckResult::ok();
}
#[AsCheckItem('if perl(strawberry) installed', limit_os: 'Windows', level: 994)]
public function checkPerl(): ?CheckResult
{
$arch = arch2gnu(php_uname('m'));
if (file_exists(PKG_ROOT_PATH . '\strawberry-perl-' . $arch . '-win\perl\bin\perl.exe')) {
return CheckResult::ok(PKG_ROOT_PATH . '\strawberry-perl-' . $arch . '-win\perl\bin\perl.exe');
}
if (($path = SystemUtil::findCommand('perl.exe')) === null) {
return CheckResult::fail('perl not found in path.', 'install-perl');
}
if (!str_contains(implode('', cmd()->execWithResult(quote($path) . ' -v')[1]), 'MSWin32')) {
return CheckResult::fail($path . ' is not built for msvc.', 'install-perl');
}
return CheckResult::ok();
}
#[AsFixItem('install-php-sdk')]
public function installPhpSdk(): bool
{
try {
cmd(true)->exec('git clone https://github.com/php/php-sdk-binary-tools.git ' . PHP_SDK_PATH);
FileSystem::removeDir(PHP_SDK_PATH);
cmd(true)->exec('git.exe clone --depth 1 https://github.com/php/php-sdk-binary-tools.git ' . PHP_SDK_PATH);
} catch (RuntimeException) {
return false;
}
return true;
}
#[AsFixItem('install-nasm')]
public function installNasm(): bool
{
PackageManager::installPackage('nasm-x86_64-win');
return true;
}
#[AsFixItem('install-perl')]
public function installPerl(): bool
{
$arch = arch2gnu(php_uname('m'));
PackageManager::installPackage("strawberry-perl-{$arch}-win");
return true;
}
}

View File

@@ -12,6 +12,8 @@ use SPC\exception\WrongUsageException;
*/
class Config
{
public static ?array $pkg = null;
public static ?array $source = null;
public static ?array $lib = null;
@@ -31,6 +33,19 @@ class Config
return self::$source[$name] ?? null;
}
/**
* Read pkg from pkg.json
*
* @throws FileSystemException
*/
public static function getPkg(string $name): ?array
{
if (self::$pkg === null) {
self::$pkg = FileSystem::loadConfigArray('pkg');
}
return self::$pkg[$name] ?? null;
}
/**
* 根据不同的操作系统分别选择不同的 lib 库依赖项
* 如果 key 为 null那么直接返回整个 meta。

View File

@@ -168,14 +168,15 @@ class Downloader
public static function downloadFile(string $name, string $url, string $filename, ?string $move_path = null): void
{
logger()->debug("Downloading {$url}");
pcntl_signal(SIGINT, function () use ($filename) {
if (file_exists(DOWNLOAD_PATH . '/' . $filename)) {
$cancel_func = function () use ($filename) {
if (file_exists(FileSystem::convertPath(DOWNLOAD_PATH . '/' . $filename))) {
logger()->warning('Deleting download file: ' . $filename);
unlink(DOWNLOAD_PATH . '/' . $filename);
unlink(FileSystem::convertPath(DOWNLOAD_PATH . '/' . $filename));
}
});
self::curlDown(url: $url, path: DOWNLOAD_PATH . "/{$filename}");
pcntl_signal(SIGINT, SIG_IGN);
};
self::registerCancelEvent($cancel_func);
self::curlDown(url: $url, path: FileSystem::convertPath(DOWNLOAD_PATH . "/{$filename}"));
self::unregisterCancelEvent();
logger()->debug("Locking {$filename}");
self::lockSource($name, ['source_type' => 'archive', 'filename' => $filename, 'move_path' => $move_path]);
}
@@ -187,7 +188,7 @@ class Downloader
*/
public static function lockSource(string $name, array $data): void
{
if (!file_exists(DOWNLOAD_PATH . '/.lock.json')) {
if (!file_exists(FileSystem::convertPath(DOWNLOAD_PATH . '/.lock.json'))) {
$lock = [];
} else {
$lock = json_decode(FileSystem::readFile(DOWNLOAD_PATH . '/.lock.json'), true) ?? [];
@@ -204,24 +205,25 @@ class Downloader
*/
public static function downloadGit(string $name, string $url, string $branch, ?string $move_path = null): void
{
$download_path = DOWNLOAD_PATH . "/{$name}";
$download_path = FileSystem::convertPath(DOWNLOAD_PATH . "/{$name}");
if (file_exists($download_path)) {
FileSystem::removeDir($download_path);
}
logger()->debug("cloning {$name} source");
$check = !defined('DEBUG_MODE') ? ' -q' : '';
pcntl_signal(SIGINT, function () use ($download_path) {
$cancel_func = function () use ($download_path) {
if (is_dir($download_path)) {
logger()->warning('Removing path ' . $download_path);
FileSystem::removeDir($download_path);
}
});
};
self::registerCancelEvent($cancel_func);
f_passthru(
'git clone' . $check .
SPC_GIT_EXEC . ' clone' . $check .
' --config core.autocrlf=false ' .
"--branch \"{$branch}\" " . (defined('GIT_SHALLOW_CLONE') ? '--depth 1 --single-branch' : '') . " --recursive \"{$url}\" \"{$download_path}\""
);
pcntl_signal(SIGINT, SIG_IGN);
self::unregisterCancelEvent();
// Lock
logger()->debug("Locking git source {$name}");
@@ -244,6 +246,92 @@ class Downloader
}*/
}
public static function downloadPackage(string $name, ?array $pkg = null, bool $force = false): void
{
if ($pkg === null) {
$pkg = Config::getPkg($name);
}
if ($pkg === null) {
logger()->warning('Package {name} unknown. Skipping.', ['name' => $name]);
return;
}
if (!is_dir(DOWNLOAD_PATH)) {
FileSystem::createDir(DOWNLOAD_PATH);
}
// load lock file
if (!file_exists(DOWNLOAD_PATH . '/.lock.json')) {
$lock = [];
} else {
$lock = json_decode(FileSystem::readFile(DOWNLOAD_PATH . '/.lock.json'), true) ?? [];
}
// If lock file exists, skip downloading
if (isset($lock[$name]) && !$force) {
if ($lock[$name]['source_type'] === 'archive' && file_exists(DOWNLOAD_PATH . '/' . $lock[$name]['filename'])) {
logger()->notice("Package [{$name}] already downloaded: " . $lock[$name]['filename']);
return;
}
if ($lock[$name]['source_type'] === 'dir' && is_dir(DOWNLOAD_PATH . '/' . $lock[$name]['dirname'])) {
logger()->notice("Package [{$name}] already downloaded: " . $lock[$name]['dirname']);
return;
}
}
try {
switch ($pkg['type']) {
case 'bitbuckettag': // BitBucket Tag
[$url, $filename] = self::getLatestBitbucketTag($name, $pkg);
self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null);
break;
case 'ghtar': // GitHub Release (tar)
[$url, $filename] = self::getLatestGithubTarball($name, $pkg);
self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null);
break;
case 'ghtagtar': // GitHub Tag (tar)
[$url, $filename] = self::getLatestGithubTarball($name, $pkg, 'tags');
self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null);
break;
case 'ghrel': // GitHub Release (uploaded)
[$url, $filename] = self::getLatestGithubRelease($name, $pkg);
self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null);
break;
case 'filelist': // Basic File List (regex based crawler)
[$url, $filename] = self::getFromFileList($name, $pkg);
self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null);
break;
case 'url': // Direct download URL
$url = $pkg['url'];
$filename = $pkg['filename'] ?? basename($pkg['url']);
self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null);
break;
case 'git': // Git repo
self::downloadGit($name, $pkg['url'], $pkg['rev'], $pkg['extract'] ?? null);
break;
case 'custom': // Custom download method, like API-based download or other
$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();
break;
}
}
break;
default:
throw new DownloaderException('unknown source type: ' . $pkg['type']);
}
} catch (RuntimeException $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());
}
}
/**
* Download source by name and meta.
*
@@ -251,7 +339,6 @@ class Downloader
* @param null|array $source source meta info: [type, path, rev, url, filename, regex, license]
* @throws DownloaderException
* @throws FileSystemException
* @throws RuntimeException
*/
public static function downloadSource(string $name, ?array $source = null, bool $force = false): void
{
@@ -359,12 +446,12 @@ class Downloader
};
$headerArg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $headers));
$cmd = "curl -sfSL {$methodArg} {$headerArg} \"{$url}\"";
$cmd = SPC_CURL_EXEC . " -sfSL {$methodArg} {$headerArg} \"{$url}\"";
if (getenv('CACHE_API_EXEC') === 'yes') {
if (!file_exists(DOWNLOAD_PATH . '/.curl_exec_cache')) {
if (!file_exists(FileSystem::convertPath(DOWNLOAD_PATH . '/.curl_exec_cache'))) {
$cache = [];
} else {
$cache = json_decode(file_get_contents(DOWNLOAD_PATH . '/.curl_exec_cache'), true);
$cache = json_decode(file_get_contents(FileSystem::convertPath(DOWNLOAD_PATH . '/.curl_exec_cache')), true);
}
if (isset($cache[$cmd]) && $cache[$cmd]['expire'] >= time()) {
return $cache[$cmd]['cache'];
@@ -375,7 +462,7 @@ class Downloader
}
$cache[$cmd]['cache'] = implode("\n", $output);
$cache[$cmd]['expire'] = time() + 3600;
file_put_contents(DOWNLOAD_PATH . '/.curl_exec_cache', json_encode($cache));
file_put_contents(FileSystem::convertPath(DOWNLOAD_PATH . '/.curl_exec_cache'), json_encode($cache));
return $cache[$cmd]['cache'];
}
f_exec($cmd, $output, $ret);
@@ -404,7 +491,35 @@ class Downloader
};
$headerArg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $headers));
$check = !defined('DEBUG_MODE') ? 's' : '#';
$cmd = "curl -{$check}fSL -o \"{$path}\" {$methodArg} {$headerArg} \"{$url}\"";
$cmd = SPC_CURL_EXEC . " -{$check}fSL -o \"{$path}\" {$methodArg} {$headerArg} \"{$url}\"";
f_passthru($cmd);
}
/**
* Register CTRL+C event for different OS.
*
* @param callable $callback callback function
*/
private static function registerCancelEvent(callable $callback): void
{
if (PHP_OS_FAMILY === 'Windows') {
sapi_windows_set_ctrl_handler($callback);
} elseif (extension_loaded('pcntl')) {
pcntl_signal(SIGINT, $callback);
} else {
logger()->debug('You have not enabled `pcntl` extension, cannot prevent download file corruption when Ctrl+C');
}
}
/**
* Unegister CTRL+C event for different OS.
*/
private static function unregisterCancelEvent(): void
{
if (PHP_OS_FAMILY === 'Windows') {
sapi_windows_set_ctrl_handler(null);
} elseif (extension_loaded('pcntl')) {
pcntl_signal(SIGINT, SIG_IGN);
}
}
}

View File

@@ -14,13 +14,13 @@ class FileSystem
/**
* @throws FileSystemException
*/
public static function loadConfigArray(string $config): array
public static function loadConfigArray(string $config, ?string $config_dir = null): array
{
$whitelist = ['ext', 'lib', 'source'];
$whitelist = ['ext', 'lib', 'source', 'pkg'];
if (!in_array($config, $whitelist)) {
throw new FileSystemException('Reading ' . $config . '.json is not allowed');
}
$tries = [
$tries = $config_dir !== null ? [FileSystem::convertPath($config_dir . '/' . $config . '.json')] : [
WORKING_DIR . '/config/' . $config . '.json',
ROOT_DIR . '/config/' . $config . '.json',
];
@@ -138,6 +138,37 @@ class FileSystem
}
}
/**
* @throws RuntimeException
* @throws FileSystemException
*/
public static function extractPackage(string $name, string $filename, ?string $extract_path = null): void
{
if ($extract_path !== null) {
// replace
$extract_path = self::replacePathVariable($extract_path);
$extract_path = self::isRelativePath($extract_path) ? (WORKING_DIR . '/' . $extract_path) : $extract_path;
} else {
$extract_path = PKG_ROOT_PATH . '/' . $name;
}
logger()->info("extracting {$name} package to {$extract_path} ...");
$target = self::convertPath($extract_path);
if (!is_dir($dir = dirname($target))) {
self::createDir($dir);
}
try {
self::extractArchive($filename, $target);
} catch (RuntimeException $e) {
if (PHP_OS_FAMILY === 'Windows') {
f_passthru('rmdir /s /q ' . $target);
} else {
f_passthru('rm -rf ' . $target);
}
throw new FileSystemException('Cannot extract package ' . $name, $e->getCode(), $e);
}
}
/**
* 解压缩下载的资源包到 source 目录
*
@@ -152,95 +183,24 @@ class FileSystem
if (self::$_extract_hook === []) {
SourcePatcher::init();
}
if (!is_dir(SOURCE_PATH)) {
self::createDir(SOURCE_PATH);
}
if ($move_path !== null) {
$move_path = SOURCE_PATH . '/' . $move_path;
}
logger()->info("extracting {$name} source to " . ($move_path ?? SOURCE_PATH . "/{$name}") . ' ...');
logger()->info("extracting {$name} source to " . ($move_path ?? (SOURCE_PATH . "/{$name}")) . ' ...');
$target = self::convertPath($move_path ?? (SOURCE_PATH . "/{$name}"));
if (!is_dir($dir = dirname($target))) {
self::createDir($dir);
}
try {
$target = $move_path ?? (SOURCE_PATH . "/{$name}");
// Git source, just move
if (is_dir($filename)) {
self::copyDir($filename, $target);
self::emitSourceExtractHook($name);
return;
}
if (in_array(PHP_OS_FAMILY, ['Darwin', 'Linux', 'BSD'])) {
if (f_mkdir(directory: $target, recursive: true) !== true) {
throw new FileSystemException('create ' . $name . 'source dir failed');
}
switch (self::extname($filename)) {
case 'xz':
case 'txz':
f_passthru("tar -xf {$filename} -C {$target} --strip-components 1");
// f_passthru("cat {$filename} | xz -d | tar -x -C " . SOURCE_PATH . "/{$name} --strip-components 1");
break;
case 'gz':
case 'tgz':
f_passthru("tar -xzf {$filename} -C {$target} --strip-components 1");
break;
case 'bz2':
f_passthru("tar -xjf {$filename} -C {$target} --strip-components 1");
break;
case 'zip':
f_passthru("unzip {$filename} -d {$target}");
break;
// case 'zstd':
// case 'zst':
// passthru('cat ' . $filename . ' | zstd -d | tar -x -C ".SOURCE_PATH . "/' . $name . ' --strip-components 1', $ret);
// break;
case 'tar':
f_passthru("tar -xf {$filename} -C {$target} --strip-components 1");
break;
default:
throw new FileSystemException('unknown archive format: ' . $filename);
}
} elseif (PHP_OS_FAMILY === 'Windows') {
// find 7z
$_7zExe = self::findCommandPath('7z', [
'C:\Program Files\7-Zip-Zstandard',
'C:\Program Files (x86)\7-Zip-Zstandard',
'C:\Program Files\7-Zip',
'C:\Program Files (x86)\7-Zip',
]);
if (!$_7zExe) {
throw new FileSystemException('windows needs 7z to unpack');
}
f_mkdir(SOURCE_PATH . "/{$name}", recursive: true);
switch (self::extname($filename)) {
case 'zstd':
case 'zst':
if (!str_contains($_7zExe, 'Zstandard')) {
throw new FileSystemException("zstd is not supported: {$filename}");
}
// no break
case 'xz':
case 'txz':
case 'gz':
case 'tgz':
case 'bz2':
f_passthru("\"{$_7zExe}\" x -so {$filename} | tar -f - -x -C {$target} --strip-components 1");
break;
case 'tar':
f_passthru("tar -xf {$filename} -C {$target} --strip-components 1");
break;
case 'zip':
f_passthru("\"{$_7zExe}\" x {$filename} -o{$target}");
break;
default:
throw new FileSystemException("unknown archive format: {$filename}");
}
}
self::extractArchive($filename, $target);
self::emitSourceExtractHook($name);
} catch (RuntimeException $e) {
if (PHP_OS_FAMILY === 'Windows') {
f_passthru('rmdir /s /q ' . SOURCE_PATH . "/{$name}");
f_passthru('rmdir /s /q ' . $target);
} else {
f_passthru('rm -r ' . SOURCE_PATH . "/{$name}");
f_passthru('rm -rf ' . $target);
}
throw new FileSystemException('Cannot extract source ' . $name, $e->getCode(), $e);
throw new FileSystemException('Cannot extract source ' . $name . ': ' . $e->getMessage(), $e->getCode(), $e);
}
}
@@ -270,8 +230,12 @@ class FileSystem
{
$dir = self::convertPath($dir);
// 不是目录不扫,直接 false 处理
if (!file_exists($dir)) {
logger()->debug('Scan dir failed, no such file or directory.');
return false;
}
if (!is_dir($dir)) {
logger()->warning('Scan dir failed, no such directory.');
logger()->warning('Scan dir failed, not directory.');
return false;
}
logger()->debug('scanning directory ' . $dir);
@@ -360,8 +324,12 @@ class FileSystem
$dir = FileSystem::convertPath($dir);
logger()->debug('Removing path recursively: "' . $dir . '"');
// 不是目录不扫,直接 false 处理
if (!file_exists($dir)) {
logger()->debug('Scan dir failed, no such file or directory.');
return false;
}
if (!is_dir($dir)) {
logger()->warning('Scan dir failed, no such directory.');
logger()->warning('Scan dir failed, not directory.');
return false;
}
logger()->debug('scanning directory ' . $dir);
@@ -398,7 +366,7 @@ class FileSystem
public static function createDir(string $path): void
{
if (!is_dir($path) && !f_mkdir($path, 0755, true) && !is_dir($path)) {
throw new FileSystemException(sprintf('无法建立目录:%s', $path));
throw new FileSystemException(sprintf('Unable to create dir: %s', $path));
}
}
@@ -408,7 +376,7 @@ class FileSystem
*/
public static function writeFile(string $path, mixed $content, ...$args): bool|int|string
{
$dir = pathinfo($path, PATHINFO_DIRNAME);
$dir = pathinfo(self::convertPath($path), PATHINFO_DIRNAME);
if (!is_dir($dir) && !mkdir($dir, 0755, true)) {
throw new FileSystemException('Write file failed, cannot create parent directory: ' . $dir);
}
@@ -446,6 +414,54 @@ class FileSystem
return strlen($path) > 0 && $path[0] !== '/';
}
public static function replacePathVariable(string $path): string
{
$replacement = [
'{pkg_root_path}' => PKG_ROOT_PATH,
'{php_sdk_path}' => defined('PHP_SDK_PATH') ? PHP_SDK_PATH : WORKING_DIR . '/php-sdk-binary-tools',
'{working_dir}' => WORKING_DIR,
'{download_path}' => DOWNLOAD_PATH,
'{source_path}' => SOURCE_PATH,
];
return str_replace(array_keys($replacement), array_values($replacement), $path);
}
/**
* @throws RuntimeException
* @throws FileSystemException
*/
private static function extractArchive(string $filename, string $target): void
{
// Git source, just move
if (is_dir(self::convertPath($filename))) {
self::copyDir(self::convertPath($filename), $target);
return;
}
// Create base dir
if (f_mkdir(directory: $target, recursive: true) !== true) {
throw new FileSystemException('create ' . $target . ' dir failed');
}
if (in_array(PHP_OS_FAMILY, ['Darwin', 'Linux', 'BSD'])) {
match (self::extname($filename)) {
'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}"),
default => throw new FileSystemException('unknown archive format: ' . $filename),
};
} elseif (PHP_OS_FAMILY === 'Windows') {
// use php-sdk-binary-tools/bin/7za.exe
$_7z = self::convertPath(PHP_SDK_PATH . '/bin/7za.exe');
match (self::extname($filename)) {
'tar' => f_passthru("tar -xf {$filename} -C {$target} --strip-components 1"),
'xz', 'txz', 'gz', 'tgz', 'bz2' => f_passthru("\"{$_7z}\" x -so {$filename} | tar -f - -x -C {$target} --strip-components 1"),
'zip' => f_passthru("\"{$_7z}\" x {$filename} -o{$target} -y"),
default => throw new FileSystemException("unknown archive format: {$filename}"),
};
}
}
/**
* @throws FileSystemException
*/

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace SPC\store;
use SPC\exception\FileSystemException;
use SPC\exception\WrongUsageException;
class PackageManager
{
public static function installPackage(string $pkg_name, ?array $config = null, bool $force = false): void
{
if ($config === null) {
$config = Config::getPkg($pkg_name);
}
if ($config === null) {
$arch = arch2gnu(php_uname('m'));
$os = match (PHP_OS_FAMILY) {
'Linux' => 'linux',
'Windows' => 'win',
'BSD' => 'freebsd',
'Darwin' => 'macos',
default => throw new WrongUsageException('Unsupported OS!'),
};
$config = Config::getPkg("{$pkg_name}-{$arch}-{$os}");
}
if ($config === null) {
throw new WrongUsageException("Package [{$pkg_name}] does not exist, please check the name and correct it !");
}
// Download package
Downloader::downloadPackage($pkg_name, $config, $force);
// After download, read lock file name
$lock = json_decode(FileSystem::readFile(DOWNLOAD_PATH . '/.lock.json'), true);
$filename = DOWNLOAD_PATH . '/' . ($lock[$pkg_name]['filename'] ?? $lock[$pkg_name]['dirname']);
$extract = $lock[$pkg_name]['move_path'] === null ? (PKG_ROOT_PATH . '/' . $pkg_name) : $lock[$pkg_name]['move_path'];
FileSystem::extractPackage($pkg_name, $filename, $extract);
// if contains extract-files, we just move this file to destination, and remove extract dir
if (is_array($config['extract-files'] ?? null) && is_assoc_array($config['extract-files'])) {
$scandir = FileSystem::scanDirFiles($extract, true, true);
foreach ($config['extract-files'] as $file => $target) {
$target = FileSystem::convertPath(FileSystem::replacePathVariable($target));
if (!is_dir($dir = dirname($target))) {
f_mkdir($dir, 0755, true);
}
logger()->debug("Moving package [{$pkg_name}] file {$file} to {$target}");
// match pattern, needs to scan dir
$file = FileSystem::convertPath($file);
$found = false;
foreach ($scandir as $item) {
if (match_pattern($file, $item)) {
$file = $item;
$found = true;
break;
}
}
if ($found === false) {
throw new FileSystemException('Unable to find extract-files item: ' . $file);
}
rename(FileSystem::convertPath($extract . '/' . $file), $target);
}
FileSystem::removeDir($extract);
}
}
}

View File

@@ -8,7 +8,7 @@ use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException;
class SourceExtractor
class SourceManager
{
/**
* @throws WrongUsageException

View File

@@ -6,6 +6,7 @@ namespace SPC\store;
use SPC\builder\BuilderBase;
use SPC\builder\linux\LinuxBuilder;
use SPC\builder\unix\UnixBuilderBase;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
@@ -31,6 +32,20 @@ class SourcePatcher
logger()->info('Extension [' . $ext->getName() . '] patched before buildconf');
}
}
// patch windows php 8.1 bug
if (PHP_OS_FAMILY === 'Windows' && $builder->getPHPVersionID() >= 80100 && $builder->getPHPVersionID() < 80200) {
logger()->info('Patching PHP 8.1 windows Fiber bug');
FileSystem::replaceFileStr(
SOURCE_PATH . '\php-src\win32\build\config.w32',
"ADD_FLAG('LDFLAGS', '$(BUILD_DIR)\\\\Zend\\\\jump_' + FIBER_ASM_ARCH + '_ms_pe_masm.obj');",
"ADD_FLAG('ASM_OBJS', '$(BUILD_DIR)\\\\Zend\\\\jump_' + FIBER_ASM_ARCH + '_ms_pe_masm.obj $(BUILD_DIR)\\\\Zend\\\\make_' + FIBER_ASM_ARCH + '_ms_pe_masm.obj');"
);
FileSystem::replaceFileStr(
SOURCE_PATH . '\php-src\win32\build\config.w32',
"ADD_FLAG('LDFLAGS', '$(BUILD_DIR)\\\\Zend\\\\make_' + FIBER_ASM_ARCH + '_ms_pe_masm.obj');",
''
);
}
}
/**
@@ -91,7 +106,7 @@ class SourcePatcher
}
$patch_list = $list ?? $default;
$patches = [];
$serial = ['80', '81', '82', '83'];
$serial = ['80', '81', '82', '83', '84'];
foreach ($patch_list as $patchName) {
if (file_exists(SOURCE_PATH . "/php-src/sapi/micro/patches/{$patchName}.patch")) {
$patches[] = "sapi/micro/patches/{$patchName}.patch";
@@ -181,9 +196,9 @@ class SourcePatcher
FileSystem::replaceFileRegex(SOURCE_PATH . '/php-src/main/php_config.h', '/^#define HAVE_STRLCPY 1$/m', '');
FileSystem::replaceFileRegex(SOURCE_PATH . '/php-src/main/php_config.h', '/^#define HAVE_STRLCAT 1$/m', '');
}
FileSystem::replaceFileRegex(SOURCE_PATH . '/php-src/main/php_config.h', '/^#define HAVE_OPENPTY 1$/m', '');
FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/Makefile', 'install-micro', '');
if ($builder instanceof UnixBuilderBase) {
FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/Makefile', 'install-micro', '');
}
// call extension patch before make
foreach ($builder->getExts() as $ext) {
@@ -252,4 +267,48 @@ class SourcePatcher
@unlink($embed_c_bak);
return $result;
}
/**
* Patch cli SAPI Makefile for Windows.
*
* @throws FileSystemException
* @throws RuntimeException
*/
public static function patchWindowsCLITarget(): void
{
// search Makefile code line contains "$(BUILD_DIR)\php.exe:"
$content = FileSystem::readFile(SOURCE_PATH . '/php-src/Makefile');
$lines = explode("\r\n", $content);
$line_num = 0;
$found = false;
foreach ($lines as $v) {
if (strpos($v, '$(BUILD_DIR)\php.exe:') !== false) {
$found = $line_num;
break;
}
++$line_num;
}
if ($found === false) {
throw new RuntimeException('Cannot patch windows CLI Makefile!');
}
$lines[$line_num] = '$(BUILD_DIR)\php.exe: generated_files $(DEPS_CLI) $(PHP_GLOBAL_OBJS) $(CLI_GLOBAL_OBJS) $(STATIC_EXT_OBJS) $(ASM_OBJS) $(BUILD_DIR)\php.exe.res $(BUILD_DIR)\php.exe.manifest';
$lines[$line_num + 1] = "\t" . '"$(LINK)" /nologo $(PHP_GLOBAL_OBJS_RESP) $(CLI_GLOBAL_OBJS_RESP) $(STATIC_EXT_OBJS_RESP) $(STATIC_EXT_LIBS) $(ASM_OBJS) $(LIBS) $(LIBS_CLI) $(BUILD_DIR)\php.exe.res /out:$(BUILD_DIR)\php.exe $(LDFLAGS) $(LDFLAGS_CLI) /ltcg /nodefaultlib:msvcrt /nodefaultlib:msvcrtd /ignore:4286';
FileSystem::writeFile(SOURCE_PATH . '/php-src/Makefile', implode("\r\n", $lines));
}
/**
* Add additional `static-php-cli.version` ini value for PHP source.
*
* @throws FileSystemException
*/
public static function patchSPCVersionToPHP(string $version = 'unknown'): void
{
// detect patch
$file = FileSystem::readFile(SOURCE_PATH . '/php-src/main/main.c');
if (!str_contains($file, 'static-php-cli.version')) {
logger()->debug('Inserting static-php-cli.version to php-src');
$file = str_replace('PHP_INI_BEGIN()', "PHP_INI_BEGIN()\n\tPHP_INI_ENTRY(\"static-php-cli.version\",\t\"{$version}\",\tPHP_INI_ALL,\tNULL)", $file);
FileSystem::writeFile(SOURCE_PATH . '/php-src/main/main.c', $file);
}
}
}

View File

@@ -70,4 +70,12 @@ class ConfigValidator
{
is_array($data) || throw new ValidationException('ext.json is broken');
}
/**
* @throws ValidationException
*/
public static function validatePkgs(mixed $data): void
{
is_array($data) || throw new ValidationException('pkg.json is broken');
}
}

View File

@@ -5,90 +5,164 @@ declare(strict_types=1);
namespace SPC\util;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException;
use SPC\store\Config;
/**
* 依赖处理工具类,包含处理扩展、库的依赖列表顺序等
* Dependency processing tool class, including processing extensions, library dependency list order, etc.
*/
class DependencyUtil
{
/**
* Obtain the dependent lib list according to the required ext list, and sort according to the dependency
* Convert platform extensions to library dependencies and suggestions.
*
* @param array $exts extensions list
* @param array $additional_libs List of additional libraries to add to activate the extra library features triggered by lib-suggests
* @return array Returns an array containing three arrays, [extensions, libraries, not included extensions]
* @throws WrongUsageException
* @throws RuntimeException
* @throws FileSystemException
*/
public static function getExtLibsByDeps(array $exts, array $additional_libs = []): array
public static function platExtToLibs(): array
{
$sorted = [];
$visited = [];
$not_included_exts = [];
foreach ($exts as $ext) {
if (!isset($visited[$ext])) {
self::visitExtDeps($ext, $visited, $sorted);
}
$exts = Config::getExts();
$libs = Config::getLibs();
$dep_list = [];
foreach ($exts as $ext_name => $ext) {
// convert ext-depends value to ext@xxx
$ext_depends = Config::getExt($ext_name, 'ext-depends', []);
$ext_depends = array_map(fn ($x) => "ext@{$x}", $ext_depends);
// convert ext-suggests value to ext@xxx
$ext_suggests = Config::getExt($ext_name, 'ext-suggests', []);
$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);
// merge ext-suggests with lib-suggests
$lib_suggests = Config::getExt($ext_name, 'lib-suggests', []);
$suggests = array_merge($ext_suggests, $lib_suggests);
$dep_list["ext@{$ext_name}"] = [
'depends' => $depends,
'suggests' => $suggests,
];
}
$sorted_suggests = [];
$visited_suggests = [];
$final = [];
foreach ($exts as $ext) {
if (!isset($visited_suggests[$ext])) {
self::visitExtAllDeps($ext, $visited_suggests, $sorted_suggests);
}
foreach ($libs as $lib_name => $lib) {
$dep_list[$lib_name] = [
'depends' => Config::getLib($lib_name, 'lib-depends', []),
'suggests' => Config::getLib($lib_name, 'lib-suggests', []),
];
}
foreach ($sorted_suggests as $suggest) {
if (in_array($suggest, $sorted)) {
$final[] = $suggest;
}
}
$libs = $additional_libs;
foreach ($final as $ext) {
if (!in_array($ext, $exts)) {
$not_included_exts[] = $ext;
}
foreach (Config::getExt($ext, 'lib-depends', []) as $lib) {
if (!in_array($lib, $libs)) {
$libs[] = $lib;
}
}
}
return [$final, self::getLibsByDeps($libs), $not_included_exts];
// here is an array that only contains dependency map
return $dep_list;
}
/**
* 根据 lib 库的依赖关系进行一个排序,同时返回多出来的依赖列表
*
* @param array $libs 要排序的 libs 列表
* @return array 排序后的列表
* @throws FileSystemException
* @throws RuntimeException
* @throws WrongUsageException
* @throws FileSystemException
*/
public static function getLibsByDeps(array $libs): array
public static function getLibs(array $libs, bool $include_suggested_libs = false): array
{
$sorted = [];
$visited = [];
$dep_list = self::platExtToLibs();
// 遍历所有
foreach ($libs as $lib) {
if (!isset($visited[$lib])) {
self::visitLibDeps($lib, $visited, $sorted);
if ($include_suggested_libs) {
foreach ($dep_list as $name => $obj) {
foreach ($obj['suggests'] as $id => $suggest) {
if (!str_starts_with($suggest, 'ext@')) {
$dep_list[$name]['depends'][] = $suggest;
array_splice($dep_list[$name]['suggests'], $id, 1);
}
}
}
}
$final = self::doVisitPlat($libs, $dep_list);
$libs_final = [];
foreach ($final as $item) {
if (!str_starts_with($item, 'ext@')) {
$libs_final[] = $item;
}
}
return $libs_final;
}
/**
* @throws FileSystemException|WrongUsageException
*/
public static function getExtsAndLibs(array $exts, array $additional_libs = [], bool $include_suggested_exts = false, bool $include_suggested_libs = false): array
{
$dep_list = self::platExtToLibs();
// include suggested extensions
if ($include_suggested_exts) {
// check every deps suggests contains ext@
foreach ($dep_list as $name => $obj) {
foreach ($obj['suggests'] as $id => $suggest) {
if (str_starts_with($suggest, 'ext@')) {
$dep_list[$name]['depends'][] = $suggest;
array_splice($dep_list[$name]['suggests'], $id, 1);
}
}
}
}
// include suggested libraries
if ($include_suggested_libs) {
// check every deps suggests
foreach ($dep_list as $name => $obj) {
foreach ($obj['suggests'] as $id => $suggest) {
if (!str_starts_with($suggest, 'ext@')) {
$dep_list[$name]['depends'][] = $suggest;
array_splice($dep_list[$name]['suggests'], $id, 1);
}
}
}
}
// convert ext_name to ext@ext_name
$origin_exts = $exts;
$exts = array_map(fn ($x) => "ext@{$x}", $exts);
$exts = array_merge($exts, $additional_libs);
$final = self::doVisitPlat($exts, $dep_list);
// revert array
$exts_final = [];
$libs_final = [];
$not_included_final = [];
foreach ($final as $item) {
if (str_starts_with($item, 'ext@')) {
$tmp = substr($item, 4);
if (!in_array($tmp, $origin_exts)) {
$not_included_final[] = $tmp;
}
$exts_final[] = $tmp;
} else {
$libs_final[] = $item;
}
}
return [$exts_final, $libs_final, $not_included_final];
}
/**
* @throws WrongUsageException
*/
private static function doVisitPlat(array $deps, array $dep_list): array
{
// default: get extension exts and libs sorted by dep_list
$sorted = [];
$visited = [];
foreach ($deps as $ext_name) {
if (!isset($dep_list[$ext_name])) {
$ext_name = str_starts_with($ext_name, 'ext@') ? ('Extension [' . substr($ext_name, 4) . ']') : ('Library [' . $ext_name . ']');
throw new WrongUsageException("{$ext_name} not exist !");
}
if (!isset($visited[$ext_name])) {
self::visitPlatDeps($ext_name, $dep_list, $visited, $sorted);
}
}
$sorted_suggests = [];
$visited_suggests = [];
$final = [];
foreach ($libs as $lib) {
if (!isset($visited_suggests[$lib])) {
self::visitLibAllDeps($lib, $visited_suggests, $sorted_suggests);
foreach ($deps as $ext_name) {
if (!isset($visited_suggests[$ext_name])) {
self::visitPlatAllDeps($ext_name, $dep_list, $visited_suggests, $sorted_suggests);
}
}
foreach ($sorted_suggests as $suggest) {
@@ -99,49 +173,7 @@ class DependencyUtil
return $final;
}
public static function getAllExtLibsByDeps(array $exts): array
{
$sorted = [];
$visited = [];
$not_included_exts = [];
foreach ($exts as $ext) {
if (!isset($visited[$ext])) {
self::visitExtAllDeps($ext, $visited, $sorted);
}
}
$libs = [];
foreach ($sorted as $ext) {
if (!in_array($ext, $exts)) {
$not_included_exts[] = $ext;
}
foreach (array_merge(Config::getExt($ext, 'lib-depends', []), Config::getExt($ext, 'lib-suggests', [])) as $dep) {
if (!in_array($dep, $libs)) {
$libs[] = $dep;
}
}
}
return [$sorted, self::getAllLibsByDeps($libs), $not_included_exts];
}
public static function getAllLibsByDeps(array $libs): array
{
$sorted = [];
$visited = [];
foreach ($libs as $lib) {
if (!isset($visited[$lib])) {
self::visitLibAllDeps($lib, $visited, $sorted);
}
}
return $sorted;
}
/**
* @throws FileSystemException
* @throws RuntimeException
* @throws WrongUsageException
*/
private static function visitLibAllDeps(string $lib_name, array &$visited, array &$sorted): void
private static function visitPlatAllDeps(string $lib_name, array $dep_list, array &$visited, array &$sorted): void
{
// 如果已经识别到了,那就不管
if (isset($visited[$lib_name])) {
@@ -149,37 +181,13 @@ class DependencyUtil
}
$visited[$lib_name] = true;
// 遍历该依赖的所有依赖(此处的 getLib 如果检测到当前库不存在的话,会抛出异常)
foreach (array_merge(Config::getLib($lib_name, 'lib-depends', []), Config::getLib($lib_name, 'lib-suggests', [])) as $dep) {
self::visitLibDeps($dep, $visited, $sorted);
foreach (array_merge($dep_list[$lib_name]['depends'], $dep_list[$lib_name]['suggests']) as $dep) {
self::visitPlatAllDeps($dep, $dep_list, $visited, $sorted);
}
$sorted[] = $lib_name;
}
/**
* @throws RuntimeException
* @throws FileSystemException
* @throws WrongUsageException
*/
private static function visitExtAllDeps(string $ext_name, array &$visited, array &$sorted): void
{
// 如果已经识别到了,那就不管
if (isset($visited[$ext_name])) {
return;
}
$visited[$ext_name] = true;
// 遍历该依赖的所有依赖(此处的 getLib 如果检测到当前库不存在的话,会抛出异常)
foreach (array_merge(Config::getExt($ext_name, 'ext-depends', []), Config::getExt($ext_name, 'ext-suggests', [])) as $dep) {
self::visitExtDeps($dep, $visited, $sorted);
}
$sorted[] = $ext_name;
}
/**
* @throws RuntimeException
* @throws FileSystemException
* @throws WrongUsageException
*/
private static function visitLibDeps(string $lib_name, array &$visited, array &$sorted): void
private static function visitPlatDeps(string $lib_name, array $dep_list, array &$visited, array &$sorted): void
{
// 如果已经识别到了,那就不管
if (isset($visited[$lib_name])) {
@@ -187,26 +195,9 @@ class DependencyUtil
}
$visited[$lib_name] = true;
// 遍历该依赖的所有依赖(此处的 getLib 如果检测到当前库不存在的话,会抛出异常)
foreach (Config::getLib($lib_name, 'lib-depends', []) as $dep) {
self::visitLibDeps($dep, $visited, $sorted);
foreach ($dep_list[$lib_name]['depends'] as $dep) {
self::visitPlatDeps($dep, $dep_list, $visited, $sorted);
}
$sorted[] = $lib_name;
}
/**
* @throws RuntimeException
* @throws FileSystemException
* @throws WrongUsageException
*/
private static function visitExtDeps(string $ext_name, array &$visited, array &$sorted): void
{
if (isset($visited[$ext_name])) {
return;
}
$visited[$ext_name] = true;
foreach (Config::getExt($ext_name, 'ext-depends', []) as $dep) {
self::visitExtDeps($dep, $visited, $sorted);
}
$sorted[] = $ext_name;
}
}

View File

@@ -50,6 +50,11 @@ class WindowsCmd
return $this;
}
public function execWithWrapper(string $wrapper, string $args): WindowsCmd
{
return $this->exec($wrapper . ' "' . str_replace('"', '^"', $args) . '"');
}
public function execWithResult(string $cmd, bool $with_log = true): array
{
if ($with_log) {

View File

@@ -2,6 +2,7 @@
declare(strict_types=1);
use SPC\store\FileSystem;
use ZM\Logger\ConsoleLogger;
define('WORKING_DIR', getcwd());
@@ -10,12 +11,13 @@ define('ROOT_DIR', dirname(__DIR__, 2));
// CLI start time
define('START_TIME', microtime(true));
define('BUILD_ROOT_PATH', is_string($a = getenv('BUILD_ROOT_PATH')) ? $a : (WORKING_DIR . '/buildroot'));
define('SOURCE_PATH', is_string($a = getenv('SOURCE_PATH')) ? $a : (WORKING_DIR . '/source'));
define('DOWNLOAD_PATH', is_string($a = getenv('DOWNLOAD_PATH')) ? $a : (WORKING_DIR . '/downloads'));
define('BUILD_BIN_PATH', is_string($a = getenv('INSTALL_BIN_PATH')) ? $a : (BUILD_ROOT_PATH . '/bin'));
define('BUILD_LIB_PATH', is_string($a = getenv('INSTALL_LIB_PATH')) ? $a : (BUILD_ROOT_PATH . '/lib'));
define('BUILD_INCLUDE_PATH', is_string($a = getenv('INSTALL_INCLUDE_PATH')) ? $a : (BUILD_ROOT_PATH . '/include'));
define('BUILD_ROOT_PATH', FileSystem::convertPath(is_string($a = getenv('BUILD_ROOT_PATH')) ? $a : (WORKING_DIR . '/buildroot')));
define('SOURCE_PATH', FileSystem::convertPath(is_string($a = getenv('SOURCE_PATH')) ? $a : (WORKING_DIR . '/source')));
define('DOWNLOAD_PATH', FileSystem::convertPath(is_string($a = getenv('DOWNLOAD_PATH')) ? $a : (WORKING_DIR . '/downloads')));
define('PKG_ROOT_PATH', FileSystem::convertPath(is_string($a = getenv('PKG_ROOT_PATH')) ? $a : (WORKING_DIR . '/pkgroot')));
define('BUILD_BIN_PATH', FileSystem::convertPath(is_string($a = getenv('INSTALL_BIN_PATH')) ? $a : (BUILD_ROOT_PATH . '/bin')));
define('BUILD_LIB_PATH', FileSystem::convertPath(is_string($a = getenv('INSTALL_LIB_PATH')) ? $a : (BUILD_ROOT_PATH . '/lib')));
define('BUILD_INCLUDE_PATH', FileSystem::convertPath(is_string($a = getenv('INSTALL_INCLUDE_PATH')) ? $a : (BUILD_ROOT_PATH . '/include')));
define('SEPARATED_PATH', [
'/' . pathinfo(BUILD_LIB_PATH)['basename'], // lib
'/' . pathinfo(BUILD_INCLUDE_PATH)['basename'], // include
@@ -26,6 +28,10 @@ if (PHP_OS_FAMILY === 'Windows') {
define('PHP_SDK_PATH', is_string($a = getenv('PHP_SDK_PATH')) ? $a : (WORKING_DIR . DIRECTORY_SEPARATOR . 'php-sdk-binary-tools'));
}
// for windows, prevent calling Invoke-WebRequest and wsl command
const SPC_CURL_EXEC = PHP_OS_FAMILY === 'Windows' ? 'curl.exe' : 'curl';
const SPC_GIT_EXEC = PHP_OS_FAMILY === 'Windows' ? 'git.exe' : 'git';
// dangerous command
const DANGER_CMD = [
'rm',

View File

@@ -47,6 +47,13 @@ function arch2gnu(string $arch): string
};
}
function match_pattern(string $pattern, string $subject): bool
{
$pattern = str_replace(['\*', '\\\\.*'], ['.*', '\*'], preg_quote($pattern, '/'));
$pattern = '/^' . $pattern . '$/i';
return preg_match($pattern, $subject) === 1;
}
function quote(string $str, string $quote = '"'): string
{
return $quote . $str . $quote;

View File

@@ -1,5 +1,7 @@
<?php
/** @noinspection ALL */
declare(strict_types=1);
/**
@@ -10,14 +12,24 @@ declare(strict_types=1);
// --------------------------------- edit area ---------------------------------
// If you want to test your added extensions and libs, add below (comma separated, example `bcmath,openssl`).
$extensions = 'ldap';
$extensions = match (PHP_OS_FAMILY) {
'Linux', 'Darwin' => 'event,gettext',
'Windows' => 'mbstring',
};
// If you want to test lib-suggests feature with extension, add them below (comma separated, example `libwebp,libavif`).
$with_libs = '';
$with_libs = match (PHP_OS_FAMILY) {
'Linux', 'Darwin' => '',
'Windows' => '',
};
// Please change your test base combination. We recommend testing with `common`.
// You can use `common`, `bulk`, `minimal` or `none`.
$base_combination = 'minimal';
// note: combination is only available for *nix platform. Windows must use `none` combination
$base_combination = match (PHP_OS_FAMILY) {
'Linux', 'Darwin' => 'common',
'Windows' => 'none',
};
// -------------------------- code area, do not modify --------------------------
@@ -48,9 +60,16 @@ $trim_value = "\r\n \t,";
$final_extensions = trim(trim($extensions, $trim_value) . ',' . _getCombination($base_combination), $trim_value);
$final_libs = trim($with_libs, $trim_value);
if (PHP_OS_FAMILY === 'Windows') {
$final_extensions_cmd = '"' . $final_extensions . '"';
} else {
$final_extensions_cmd = $final_extensions;
}
echo match ($argv[1]) {
'extensions' => $final_extensions,
'libs' => $final_libs,
'cmd' => $final_extensions . ($final_libs === '' ? '' : (' --with-libs=' . $final_libs)),
'libs_cmd' => ($final_libs === '' ? '' : (' --with-libs="' . $final_libs . '"')),
'cmd' => $final_extensions_cmd . ($final_libs === '' ? '' : (' --with-libs="' . $final_libs . '"')),
default => '',
};

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
assert(function_exists('gettext'));
assert(function_exists('bindtextdomain'));
assert(function_exists('textdomain'));
assert(function_exists('bind_textdomain_codeset'));
if (!is_dir('locale/en_US/LC_MESSAGES/')) {
mkdir('locale/en_US/LC_MESSAGES/', 0755, true);
}
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');
setlocale(LC_ALL, 'en_US');
$domain = 'test';
bindtextdomain($domain, 'locale/');
bind_textdomain_codeset($domain, 'UTF-8');
textdomain($domain);
assert(gettext(json_decode('"\u793a\u4f8b"', true)) === 'Example');

View File

@@ -0,0 +1,6 @@
<?php
declare(strict_types=1);
assert(function_exists('openssl_digest'));
assert(openssl_digest('123456', 'md5') === 'e10adc3949ba59abbe56e057f20f883e');

View File

@@ -3,3 +3,4 @@
declare(strict_types=1);
assert(function_exists('gzcompress'));
assert(gzdecode(gzencode('aaa')) === 'aaa');

View File

@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\globals;
use PHPUnit\Framework\TestCase;
use Psr\Log\LogLevel;
use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException;
use ZM\Logger\ConsoleLogger;
/**
* @internal
*/
class GlobalFunctionsTest extends TestCase
{
private static $logger_cache;
public static function setUpBeforeClass(): void
{
global $ob_logger;
self::$logger_cache = $ob_logger;
$ob_logger = new ConsoleLogger(LogLevel::ALERT);
}
public static function tearDownAfterClass(): void
{
global $ob_logger;
$ob_logger = self::$logger_cache;
}
public function testIsAssocArray(): void
{
$this->assertTrue(is_assoc_array(['a' => 1, 'b' => 2]));
$this->assertFalse(is_assoc_array([1, 2, 3]));
}
public function testLogger(): void
{
$this->assertInstanceOf('Psr\Log\LoggerInterface', logger());
}
/**
* @throws WrongUsageException
*/
public function testArch2Gnu(): void
{
$this->assertEquals('x86_64', arch2gnu('x86_64'));
$this->assertEquals('x86_64', arch2gnu('x64'));
$this->assertEquals('x86_64', arch2gnu('amd64'));
$this->assertEquals('aarch64', arch2gnu('arm64'));
$this->assertEquals('aarch64', arch2gnu('aarch64'));
$this->expectException('SPC\exception\WrongUsageException');
arch2gnu('armv7');
}
public function testQuote(): void
{
$this->assertEquals('"hello"', quote('hello'));
$this->assertEquals("'hello'", quote('hello', "'"));
}
/**
* @throws RuntimeException
*/
public function testFPassthru(): void
{
if (PHP_OS_FAMILY === 'Windows') {
$this->markTestSkipped('Windows not support f_passthru');
}
$this->assertEquals(null, f_passthru('echo ""'));
$this->expectException('SPC\exception\RuntimeException');
f_passthru('false');
}
public function testFPutenv(): void
{
$this->assertTrue(f_putenv('SPC_TEST_ENV=1'));
$this->assertEquals('1', getenv('SPC_TEST_ENV'));
}
public function testShell(): void
{
if (PHP_OS_FAMILY === 'Windows') {
$this->markTestSkipped('Windows not support shell');
}
$shell = shell();
$this->assertInstanceOf('SPC\util\UnixShell', $shell);
$this->assertInstanceOf('SPC\util\UnixShell', $shell->cd('/'));
$this->assertInstanceOf('SPC\util\UnixShell', $shell->exec('echo ""'));
$this->assertInstanceOf('SPC\util\UnixShell', $shell->setEnv(['SPC_TEST_ENV' => '1']));
[$code, $out] = $shell->execWithResult('echo "_"');
$this->assertEquals(0, $code);
$this->assertEquals('_', implode('', $out));
$this->expectException('SPC\exception\RuntimeException');
$shell->exec('false');
}
}

View File

@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\store;
use PHPUnit\Framework\TestCase;
use SPC\exception\FileSystemException;
use SPC\exception\WrongUsageException;
use SPC\store\Config;
use SPC\store\FileSystem;
/**
* @internal
*/
class ConfigTest extends TestCase
{
/**
* @throws FileSystemException
*/
public static function setUpBeforeClass(): void
{
$testdir = WORKING_DIR . '/.configtest';
FileSystem::createDir($testdir);
FileSystem::writeFile($testdir . '/lib.json', file_get_contents(ROOT_DIR . '/config/lib.json'));
FileSystem::writeFile($testdir . '/ext.json', file_get_contents(ROOT_DIR . '/config/ext.json'));
FileSystem::writeFile($testdir . '/source.json', file_get_contents(ROOT_DIR . '/config/source.json'));
FileSystem::loadConfigArray('lib', $testdir);
FileSystem::loadConfigArray('ext', $testdir);
FileSystem::loadConfigArray('source', $testdir);
}
/**
* @throws FileSystemException
*/
public static function tearDownAfterClass(): void
{
FileSystem::removeDir(WORKING_DIR . '/.configtest');
}
/**
* @throws FileSystemException
*/
public function testGetExts()
{
$this->assertTrue(is_assoc_array(Config::getExts()));
}
/**
* @throws FileSystemException
* @throws WrongUsageException
*/
public function testGetLib()
{
$this->assertIsArray(Config::getLib('zlib'));
match (PHP_OS_FAMILY) {
'FreeBSD', 'Darwin', 'Linux' => $this->assertStringEndsWith('.a', Config::getLib('zlib', 'static-libs', [])[0]),
'Windows' => $this->assertStringEndsWith('.lib', Config::getLib('zlib', 'static-libs', [])[0]),
default => null,
};
}
/**
* @throws WrongUsageException
* @throws FileSystemException
*/
public function testGetExt()
{
$this->assertIsArray(Config::getExt('bcmath'));
$this->assertEquals('builtin', Config::getExt('bcmath', 'type'));
}
/**
* @throws FileSystemException
*/
public function testGetSources()
{
$this->assertTrue(is_assoc_array(Config::getSources()));
}
/**
* @throws FileSystemException
*/
public function testGetSource()
{
$this->assertIsArray(Config::getSource('php-src'));
}
/**
* @throws FileSystemException
*/
public function testGetLibs()
{
$this->assertTrue(is_assoc_array(Config::getLibs()));
}
}

View File

@@ -0,0 +1,213 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\store;
use PHPUnit\Framework\TestCase;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\store\FileSystem;
/**
* @internal
*/
class FileSystemTest extends TestCase
{
private const TEST_FILE_CONTENT = 'Hello! Bye!';
public static function setUpBeforeClass(): void
{
if (file_put_contents(WORKING_DIR . '/.testfile', self::TEST_FILE_CONTENT) === false) {
static::markTestSkipped('Current environment or working dir is not writable!');
}
}
public static function tearDownAfterClass(): void
{
if (file_exists(WORKING_DIR . '/.testfile')) {
unlink(WORKING_DIR . '/.testfile');
}
}
/**
* @throws FileSystemException
*/
public function testReplaceFileRegex()
{
$file = WORKING_DIR . '/.txt1';
file_put_contents($file, 'hello');
FileSystem::replaceFileRegex($file, '/ll/', '11');
$this->assertEquals('he11o', file_get_contents($file));
unlink($file);
}
public function testFindCommandPath()
{
$this->assertNull(FileSystem::findCommandPath('randomtestxxxxx'));
if (PHP_OS_FAMILY === 'Windows') {
$this->assertIsString(FileSystem::findCommandPath('explorer'));
} elseif (in_array(PHP_OS_FAMILY, ['Linux', 'Darwin', 'FreeBSD'])) {
$this->assertIsString(FileSystem::findCommandPath('uname'));
}
}
/**
* @throws FileSystemException
*/
public function testReadFile()
{
$file = WORKING_DIR . '/.testread';
file_put_contents($file, 'haha');
$content = FileSystem::readFile($file);
$this->assertEquals('haha', $content);
@unlink($file);
}
/**
* @throws FileSystemException
*/
public function testReplaceFileUser()
{
$file = WORKING_DIR . '/.txt1';
file_put_contents($file, 'hello');
FileSystem::replaceFileUser($file, function ($file) {
return str_replace('el', '55', $file);
});
$this->assertEquals('h55lo', file_get_contents($file));
unlink($file);
}
public function testExtname()
{
$this->assertEquals('exe', FileSystem::extname('/tmp/asd.exe'));
$this->assertEquals('', FileSystem::extname('/tmp/asd.'));
}
/**
* @throws \ReflectionException
* @throws FileSystemException
*/
public function testGetClassesPsr4()
{
$classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/builder/extension', 'SPC\\builder\\extension');
foreach ($classes as $class) {
$this->assertIsString($class);
new \ReflectionClass($class);
}
}
public function testConvertPath()
{
$this->assertEquals('phar://C:/pharfile.phar', FileSystem::convertPath('phar://C:/pharfile.phar'));
if (DIRECTORY_SEPARATOR === '\\') {
$this->assertEquals('C:\Windows\win.ini', FileSystem::convertPath('C:\Windows/win.ini'));
}
}
/**
* @throws FileSystemException
*/
public function testCreateDir()
{
FileSystem::createDir(WORKING_DIR . '/.testdir');
$this->assertDirectoryExists(WORKING_DIR . '/.testdir');
rmdir(WORKING_DIR . '/.testdir');
}
/**
* @throws FileSystemException
*/
public function testReplaceFileStr()
{
$file = WORKING_DIR . '/.txt1';
file_put_contents($file, 'hello');
FileSystem::replaceFileStr($file, 'el', '55');
$this->assertEquals('h55lo', file_get_contents($file));
unlink($file);
}
/**
* @throws FileSystemException
*/
public function testResetDir()
{
// prepare fake git dir to test
FileSystem::createDir(WORKING_DIR . '/.fake_down_test');
FileSystem::writeFile(WORKING_DIR . '/.fake_down_test/a.c', 'int main() { return 0; }');
FileSystem::resetDir(WORKING_DIR . '/.fake_down_test');
$this->assertFileDoesNotExist(WORKING_DIR . '/.fake_down_test/a.c');
FileSystem::removeDir(WORKING_DIR . '/.fake_down_test');
}
/**
* @throws FileSystemException
* @throws RuntimeException
*/
public function testCopyDir()
{
// prepare fake git dir to test
FileSystem::createDir(WORKING_DIR . '/.fake_down_test');
FileSystem::writeFile(WORKING_DIR . '/.fake_down_test/a.c', 'int main() { return 0; }');
FileSystem::copyDir(WORKING_DIR . '/.fake_down_test', WORKING_DIR . '/.fake_down_test2');
$this->assertDirectoryExists(WORKING_DIR . '/.fake_down_test2');
$this->assertFileExists(WORKING_DIR . '/.fake_down_test2/a.c');
FileSystem::removeDir(WORKING_DIR . '/.fake_down_test');
FileSystem::removeDir(WORKING_DIR . '/.fake_down_test2');
}
/**
* @throws FileSystemException
*/
public function testRemoveDir()
{
FileSystem::createDir(WORKING_DIR . '/.fake_down_test');
$this->assertDirectoryExists(WORKING_DIR . '/.fake_down_test');
FileSystem::removeDir(WORKING_DIR . '/.fake_down_test');
$this->assertDirectoryDoesNotExist(WORKING_DIR . '/.fake_down_test');
}
/**
* @throws FileSystemException
*/
public function testLoadConfigArray()
{
$arr = FileSystem::loadConfigArray('lib');
$this->assertArrayHasKey('zlib', $arr);
}
public function testIsRelativePath()
{
$this->assertTrue(FileSystem::isRelativePath('.'));
$this->assertTrue(FileSystem::isRelativePath('.\\sdf'));
if (DIRECTORY_SEPARATOR === '\\') {
$this->assertFalse(FileSystem::isRelativePath('C:\\asdasd/fwe\asd'));
} else {
$this->assertFalse(FileSystem::isRelativePath('/fwefwefewf'));
}
}
public function testScanDirFiles()
{
$this->assertFalse(FileSystem::scanDirFiles('wfwefewfewf'));
$files = FileSystem::scanDirFiles(ROOT_DIR . '/config', true, true);
$this->assertContains('lib.json', $files);
}
/**
* @throws FileSystemException
*/
public function testWriteFile()
{
FileSystem::writeFile(WORKING_DIR . '/.txt', 'txt');
$this->assertFileExists(WORKING_DIR . '/.txt');
$this->assertEquals('txt', FileSystem::readFile(WORKING_DIR . '/.txt'));
unlink(WORKING_DIR . '/.txt');
}
}

View File

@@ -0,0 +1,172 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\util;
use PHPUnit\Framework\TestCase;
use SPC\exception\ValidationException;
use SPC\util\ConfigValidator;
/**
* @internal
*/
class ConfigValidatorTest extends TestCase
{
public function testValidateSourceGood(): void
{
$good_source = [
'source1' => [
'type' => 'filelist',
'url' => 'https://example.com',
'regex' => '.*',
],
'source2' => [
'type' => 'git',
'url' => 'https://example.com',
'rev' => 'master',
],
'source3' => [
'type' => 'ghtagtar',
'repo' => 'aaaa/bbbb',
],
'source4' => [
'type' => 'ghtar',
'repo' => 'aaa/bbb',
'path' => 'path/to/dir',
],
'source5' => [
'type' => 'ghrel',
'repo' => 'aaa/bbb',
'match' => '.*',
],
'source6' => [
'type' => 'url',
'url' => 'https://example.com',
],
];
try {
ConfigValidator::validateSource($good_source);
$this->assertTrue(true);
} catch (ValidationException $e) {
$this->fail($e->getMessage());
}
}
public function testValidateSourceBad(): void
{
$bad_source = [
'source1' => [
'type' => 'filelist',
'url' => 'https://example.com',
// no regex
],
'source2' => [
'type' => 'git',
'url' => true, // not string
'rev' => 'master',
],
'source3' => [
'type' => 'ghtagtar',
'url' => 'aaaa/bbbb', // not repo
],
'source4' => [
'type' => 'ghtar',
'repo' => 'aaa/bbb',
'path' => true, // not string
],
'source5' => [
'type' => 'ghrel',
'repo' => 'aaa/bbb',
'match' => 1, // not string
],
'source6' => [
'type' => 'url', // no url
],
];
foreach ($bad_source as $name => $src) {
try {
ConfigValidator::validateSource([$name => $src]);
$this->fail("should throw ValidationException for source {$name}");
} catch (ValidationException) {
$this->assertTrue(true);
}
}
}
public function testValidateLibsGood(): void
{
$good_libs = [
'lib1' => [
'source' => 'source1',
],
'lib2' => [
'source' => 'source2',
'lib-depends' => [
'lib1',
],
],
'lib3' => [
'source' => 'source3',
'lib-suggests' => [
'lib1',
],
],
];
try {
ConfigValidator::validateLibs($good_libs, ['source1' => [], 'source2' => [], 'source3' => []]);
$this->assertTrue(true);
} catch (ValidationException $e) {
$this->fail($e->getMessage());
}
}
public function testValidateLibsBad(): void
{
// lib.json is broken
try {
ConfigValidator::validateLibs('not array');
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// lib source not exists
try {
ConfigValidator::validateLibs(['lib1' => ['source' => 'source3']], ['source1' => [], 'source2' => []]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// source must be string
try {
ConfigValidator::validateLibs(['lib1' => ['source' => true]], ['source1' => [], 'source2' => []]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// lib-depends must be list
try {
ConfigValidator::validateLibs(['lib1' => ['source' => 'source1', 'lib-depends' => ['a' => 'not list']]], ['source1' => [], 'source2' => []]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// lib-suggests must be list
try {
ConfigValidator::validateLibs(['lib1' => ['source' => 'source1', 'lib-suggests' => ['a' => 'not list']]], ['source1' => [], 'source2' => []]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
}
/**
* @throws ValidationException
*/
public function testValidateExts(): void
{
ConfigValidator::validateExts([]);
$this->expectException(ValidationException::class);
ConfigValidator::validateExts(null);
}
}

View File

@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\util;
use PHPUnit\Framework\TestCase;
use SPC\exception\WrongUsageException;
use SPC\store\Config;
use SPC\util\DependencyUtil;
/**
* @internal
*/
final class DependencyUtilTest extends TestCase
{
public function testGetExtLibsByDeps(): void
{
// example
Config::$source = [
'test1' => [
'type' => 'url',
'url' => 'https://pecl.php.net/get/APCu',
'filename' => 'apcu.tgz',
'license' => [
'type' => 'file',
'path' => 'LICENSE',
],
],
];
Config::$lib = [
'libaaa' => [
'source' => 'test1',
'static-libs' => ['libaaa.a'],
'lib-depends' => ['libbbb', 'libccc'],
'lib-suggests' => ['libeee'],
],
'libbbb' => [
'source' => 'test1',
'static-libs' => ['libbbb.a'],
'lib-suggests' => ['libccc'],
],
'libccc' => [
'source' => 'test1',
'static-libs' => ['libccc.a'],
],
'libeee' => [
'source' => 'test1',
'static-libs' => ['libeee.a'],
'lib-suggests' => ['libfff'],
],
'libfff' => [
'source' => 'test1',
'static-libs' => ['libfff.a'],
],
];
Config::$ext = [
'ext-a' => [
'type' => 'builtin',
'lib-depends' => ['libaaa'],
'ext-suggests' => ['ext-b'],
],
'ext-b' => [
'type' => 'builtin',
'lib-depends' => ['libeee'],
],
];
// test getExtLibsByDeps (notmal test with ext-depends and lib-depends)
[$exts, $libs, $not_included] = DependencyUtil::getExtsAndLibs(['ext-a'], include_suggested_exts: true);
$this->assertContains('libbbb', $libs);
$this->assertContains('libccc', $libs);
$this->assertContains('ext-b', $exts);
$this->assertContains('ext-b', $not_included);
// test dep order
$this->assertIsInt($b = array_search('libbbb', $libs));
$this->assertIsInt($c = array_search('libccc', $libs));
$this->assertIsInt($a = array_search('libaaa', $libs));
// libbbb, libaaa
$this->assertTrue($b < $a);
$this->assertTrue($c < $a);
$this->assertTrue($c < $b);
}
public function testNotExistExtException(): void
{
$this->expectException(WrongUsageException::class);
DependencyUtil::getExtsAndLibs(['sdsd']);
}
}

View File

@@ -13,7 +13,13 @@ use SPC\util\LicenseDumper;
*/
final class LicenseDumperTest extends TestCase
{
private const DIRECTORY = '../../var/license-dump';
private const DIRECTORY = __DIR__ . '/../../var/license-dump';
public static function tearDownAfterClass(): void
{
@rmdir(self::DIRECTORY);
@rmdir(dirname(self::DIRECTORY));
}
protected function setUp(): void
{