feat(compiler): implement embedded PHP dependencies and update release packaging

- Add embedded-files configuration option for packaging Composer vendor files and PHP fallback files
- Implement OPcache bytecode generation for embedded PHP files during build process
- Update GitHub Actions workflows to handle PHP version-specific release packaging
- Modify package.php to embed Composer runtime dependencies directly in compiler binaries
- Remove vendor directory from release packages since dependencies are now embedded
- Add comprehensive documentation for embedded-files feature in English and Chinese
- Update README files to include embedded dependencies documentation links
- Implement PHP ABI versioning in package filenames to distinguish different PHP builds
- Add verification mechanism to ensure embedded Composer runtime functions correctly
- Create detailed guides covering recommended setup, performance considerations, and build requirements
master
韩天峰 2 weeks ago
parent cbdf5d6a11
commit 43e85b8866
  1. 8
      .github/actions/unix-arm64-build/action.yml
  2. 8
      .github/workflows/linux-x64.yml
  3. 14
      .github/workflows/release.yml
  4. 15
      .github/workflows/windows-build.yml
  5. 13
      README-CN.md
  6. 18
      README.md
  7. 16
      docs/en/COMPILER_CLI.md
  8. 288
      docs/en/EMBEDDED_FILES.md
  9. 1
      docs/en/README.md
  10. 15
      docs/zh-cn/COMPILER_CLI.md
  11. 262
      docs/zh-cn/EMBEDDED_FILES.md
  12. 1
      docs/zh-cn/README.md
  13. 160
      package.php

@ -181,7 +181,7 @@ runs:
fi
- name: Package tested compiler
if: startsWith(github.ref, 'refs/tags/') && inputs.php-version == '8.5'
if: startsWith(github.ref, 'refs/tags/')
shell: bash
run: |
composer install --no-dev --prefer-dist --no-progress --classmap-authoritative
@ -190,16 +190,16 @@ runs:
./tpc --version
export TYPEPHP_PACKAGE_VERSION="${GITHUB_REF_NAME}"
php package.php
test "$(find . -maxdepth 1 -name 'tpc_v*_${{ inputs.os }}_arm64.tar.gz' -type f | wc -l)" -eq 1
test "$(find . -maxdepth 1 -name 'tpc_v*_${{ inputs.os }}_arm64_php${{ inputs.php-version }}.*-zts.tar.gz' -type f | wc -l)" -eq 1
- name: Upload release package
if: startsWith(github.ref, 'refs/tags/') && inputs.php-version == '8.5'
if: startsWith(github.ref, 'refs/tags/')
uses: actions/upload-artifact@v4
with:
name: release-${{ inputs.os }}-arm64-php-${{ inputs.php-version }}-zts
if-no-files-found: error
retention-days: 1
path: tpc_v*_${{ inputs.os }}_arm64.tar.gz
path: tpc_v*_${{ inputs.os }}_arm64_php${{ inputs.php-version }}.*-zts.tar.gz
- name: Upload platform build outputs
if: always()

@ -509,7 +509,7 @@ jobs:
path: build/phpt-metrics
- name: Package tested Linux compiler
if: startsWith(github.ref, 'refs/tags/') && matrix.php == '8.5'
if: startsWith(github.ref, 'refs/tags/')
shell: bash
run: |
composer install --no-dev --prefer-dist --no-progress --classmap-authoritative
@ -518,16 +518,16 @@ jobs:
./tpc --version
export TYPEPHP_PACKAGE_VERSION="${GITHUB_REF_NAME}"
php package.php
test "$(find . -maxdepth 1 -name 'tpc_v*_linux_*.tar.gz' -type f | wc -l)" -eq 1
test "$(find . -maxdepth 1 -name 'tpc_v*_linux_x64_php${{ matrix.php }}.*-zts.tar.gz' -type f | wc -l)" -eq 1
- name: Upload Linux release package
if: startsWith(github.ref, 'refs/tags/') && matrix.php == '8.5'
if: startsWith(github.ref, 'refs/tags/')
uses: actions/upload-artifact@v4
with:
name: release-linux-x64-php-${{ matrix.php }}-zts
if-no-files-found: error
retention-days: 1
path: tpc_v*_linux_*.tar.gz
path: tpc_v*_linux_x64_php${{ matrix.php }}.*-zts.tar.gz
- name: Upload PHPT failure artifacts
if: failure()

@ -104,10 +104,14 @@ jobs:
--pattern 'release-macos-arm64-*' \
--dir dist/macos-arm64
test "$(find dist -type f -name 'tpc_v*_linux_x64.tar.gz' | wc -l)" -eq 1
test "$(find dist -type f -name 'tpc_v*_linux_arm64.tar.gz' | wc -l)" -eq 1
test "$(find dist -type f -name 'tpc_v*_macos_arm64.tar.gz' | wc -l)" -eq 1
test "$(find dist -type f -name 'tpc_v*_windows_x64.zip' | wc -l)" -eq 1
for platform in linux_x64 linux_arm64 macos_arm64; do
for php_version in 8.4 8.5; do
test "$(find dist -type f -name "tpc_v*_${platform}_php${php_version}.*-zts.tar.gz" | wc -l)" -eq 1
done
done
for php_version in 8.4 8.5; do
test "$(find dist -type f -name "tpc_v*_windows_x64_php${php_version}.*-zts.zip" | wc -l)" -eq 1
done
- name: Prepare release assets
shell: bash
@ -116,7 +120,7 @@ jobs:
mkdir -p release-assets
find dist -type f \( -name '*.tar.gz' -o -name '*.zip' \) \
-exec cp '{}' release-assets/ \;
test "$(find release-assets -maxdepth 1 -type f \( -name '*.tar.gz' -o -name '*.zip' \) | wc -l)" -eq 4
test "$(find release-assets -maxdepth 1 -type f \( -name '*.tar.gz' -o -name '*.zip' \) | wc -l)" -eq 8
(
cd release-assets
sha256sum ./*.tar.gz ./*.zip > SHA256SUMS

@ -390,7 +390,7 @@ jobs:
}
- name: Package tested Windows compiler
if: startsWith(github.ref, 'refs/tags/') && matrix.php == '8.5'
if: startsWith(github.ref, 'refs/tags/')
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
@ -415,17 +415,17 @@ jobs:
throw "Windows packaging failed with exit code $LASTEXITCODE"
}
$packages = @(Get-ChildItem 'tpc_v*_windows_*.zip' -File)
$packages = @(Get-ChildItem 'tpc_v*_windows_x64_php${{ matrix.php }}.*-zts.zip' -File)
if ($packages.Count -ne 1) {
throw "Expected one Windows release package, found $($packages.Count)"
}
- name: Verify portable Windows PHP configuration
if: startsWith(github.ref, 'refs/tags/') && matrix.php == '8.5'
if: startsWith(github.ref, 'refs/tags/')
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$package = Get-ChildItem 'tpc_v*_windows_*.zip' -File | Select-Object -First 1
$package = Get-ChildItem 'tpc_v*_windows_x64_php${{ matrix.php }}.*-zts.zip' -File | Select-Object -First 1
$destination = Join-Path $env:RUNNER_TEMP ('typephp-release-check-' + [guid]::NewGuid())
Expand-Archive -LiteralPath $package.FullName -DestinationPath $destination
$roots = @(Get-ChildItem $destination -Directory)
@ -433,6 +433,9 @@ jobs:
throw "Expected one release package directory, found $($roots.Count)"
}
$root = $roots[0].FullName
if (Test-Path (Join-Path $root 'vendor')) {
throw 'Release package must not contain a vendor directory'
}
$phpExe = Join-Path $root 'php.exe'
$compilerExe = Join-Path $root 'tpc.exe'
$phpIni = Join-Path $root 'php.ini'
@ -476,13 +479,13 @@ jobs:
}
- name: Upload Windows release package
if: startsWith(github.ref, 'refs/tags/') && matrix.php == '8.5'
if: startsWith(github.ref, 'refs/tags/')
uses: actions/upload-artifact@v4
with:
name: release-windows-x64-php-${{ matrix.php }}-zts
if-no-files-found: error
retention-days: 1
path: tpc_v*_windows_*.zip
path: tpc_v*_windows_x64_php${{ matrix.php }}.*-zts.zip
- name: Upload Windows build outputs
if: always()

@ -150,10 +150,12 @@ Android `arm64-v8a`、iPhoneOS `arm64` 和 WASI 后端;具体主机能否构
[Android 原生应用示例](examples/android-native/)和
[iOS/macOS 原生应用示例](examples/apple-native/)。
原生 Release Assets 默认使用 PHP 8.5 ZTS 的最新版本构建,提供 Linux x64、Linux
ARM64、macOS ARM64 和 Windows x64 四个平台包;不提供原生 NTS 或 32 位 x86 包。
Linux 与 macOS 包包含编译器和 production Composer 依赖,Windows 包则包含完整且
匹配的 PHP/PHPX 运行时与 SDK。
Linux、macOS 和 Windows Release Assets 分别使用最新的 PHP 8.4 ZTS 和 PHP 8.5
ZTS 构建,文件名包含构建使用的完整 PHP 版本与 ZTS ABI。Linux 与 macOS 用户必须
选择与宿主机 PHP 匹配的版本;Windows x64 发布包已包含匹配的 PHP/PHPX 运行时与
SDK,用户可直接选择希望使用的内置 PHP 版本。不提供原生 NTS 或 32 位 x86 包。
Linux 与 macOS 包只包含编译器、中英文 README 和 LICENSE,production Composer
依赖已嵌入 `tpc`;Windows 包同样不再携带独立的 `vendor` 目录。
## 安装
@ -359,6 +361,8 @@ embedded-files:
`version` 用于设置 Zend 模块版本。`info` 映射可配置任意标签和值,并显示在模块
独立的 `phpinfo()` 区块中。
`embedded-files` 支持与 `sources` 相同的文件、目录及条件写法,仅在显式配置时启用。
完整的开发/发布配置、Composer autoload 接入、构建依赖、缓存规则和排错方法见
[将 PHP 依赖嵌入可执行文件](docs/zh-cn/EMBEDDED_FILES.md)。
所列文件全部打包进二进制;未通过 `sources` 成功原生编译的 PHP 文件由 OPcache
生成字节码。用于 API 声明的 `.stub.php` 文件仍保留在原始文件包中,不生成可执行字节码。
其他无法由 OPcache 编译的内嵌 PHP 文件会输出跳过日志,原始文件仍保留,但不进入
@ -780,6 +784,7 @@ GitHub Actions 会在 PHP 8.4 和 8.5 上分别运行 PHPUnit 与自举 PHPT。
## 文档
- [快速入门](docs/zh-cn/QUICKSTART.md) —— 最小编译流程
- [内嵌 PHP 依赖](docs/zh-cn/EMBEDDED_FILES.md) —— 将 Composer vendor 和运行时资源打包进可执行文件
- [变更记录](CHANGELOG.md) —— 破坏性变更与 1.0 前升级说明
- [编译模式](docs/zh-cn/COMPILATION_MODES.md) —— `bin`、`ext`、`lib`
- [编译器命令行](docs/zh-cn/COMPILER_CLI.md) —— CLI 参数与项目配置

@ -170,11 +170,15 @@ in TypePHP while keeping only a thin platform-native UI bridge. See the
[Android native app example](examples/android-native/) and the
[iOS/macOS native app example](examples/apple-native/).
Native release assets are built with the latest PHP 8.5 ZTS release. TypePHP
publishes Linux x64, Linux ARM64, macOS ARM64, and Windows x64 packages. Native
NTS and 32-bit x86 packages are not provided. Linux and macOS archives contain
the compiler and production Composer dependencies, while the Windows archive
contains the complete matching PHP/PHPX runtime and SDK.
Linux, macOS, and Windows release assets are built separately with the latest
PHP 8.4 ZTS and PHP 8.5 ZTS releases; each filename identifies the complete
build PHP version and ZTS ABI. Linux and macOS users must select the build that
matches the host PHP. Each Windows x64 archive includes its matching PHP/PHPX
runtime and SDK, so users can directly choose the bundled PHP version they want.
Native NTS and 32-bit x86 packages are not provided. Linux and macOS archives
contain only the compiler, English and Chinese READMEs, and the license;
production Composer dependencies are embedded in `tpc`. Windows archives also
omit a separate `vendor` directory.
## Installation
@ -395,6 +399,9 @@ entries so Zend can reject loading when a required PHP extension is missing.
`version` provides the Zend module version. The `info` mapping accepts arbitrary
labels and values for the module's dedicated `phpinfo()` section.
`embedded-files` accepts files or directories with the same conditional syntax.
For development/release configurations, Composer autoload setup, build
requirements, cache behavior, and troubleshooting, see
[Embedding PHP dependencies in an executable](docs/en/EMBEDDED_FILES.md).
It is opt-in for embedded binary builds: all listed files are packed into the
binary, and PHP files not successfully compiled from `sources` are stored as
OPcache bytecode. `.stub.php` API declaration files remain in the raw bundle
@ -853,6 +860,7 @@ rules and a PHPT whenever runtime output or diagnostics are observable.
## Documentation
- [Quick Start](docs/en/QUICKSTART.md) — minimal compilation flow
- [Embedded PHP dependencies](docs/en/EMBEDDED_FILES.md) — package Composer vendor and runtime resources in an executable
- [Change log](CHANGELOG.md) — breaking changes and pre-1.0 upgrade notes
- [Compilation modes](docs/en/COMPILATION_MODES.md) — `bin`, `ext`, `lib`
- [Compiler CLI](docs/en/COMPILER_CLI.md) — CLI arguments and project config

@ -187,6 +187,22 @@ extension-dependencies:
The compiler generates a `ZEND_MOD_REQUIRED` for each entry. Zend checks whether these extensions are loaded when loading the TypePHP module. This setting does not represent native link libraries; C/C++ link dependencies still use `link-libs`.
### Embedded PHP dependencies and resources
A release `mode: bin` project can package Composer vendor files, PHP fallback
files, and read-only resources through `embedded-files`:
```yaml
embedded-files:
- vendor
- resources
```
The build needs matching PHP CLI and OPcache installations. The runtime does
not need PHP CLI, OPcache, Composer installation, or a disk vendor tree. See
[Embedding PHP Dependencies in an Executable](EMBEDDED_FILES.md) for the full
workflow and limitations.
## Viewing the Authoritative Help
The command-line implementation may continue to evolve; for released versions the actual arguments are determined by the following command:

@ -0,0 +1,288 @@
# Embedding PHP Dependencies in an Executable
`embedded-files` packages Composer dependencies, PHP fallback files, and
runtime resources into a TypePHP executable. It addresses these deployment
problems:
- a third-party library cannot be compiled by TypePHP AOT;
- Composer autoload must still load that library lazily;
- the release host should not run `composer install` or carry a separate
`vendor` directory;
- read-only configuration, templates, certificates, or similar resources must
ship with the application.
During the build, TypePHP converts PHP files that were not compiled through
`sources` into OPcache opcodes and stores every selected file in the executable.
At runtime, `require`, `require_once`, and Composer autoload load PHP scripts
from the in-memory opcode table. Other read-only files are served by the
in-memory file table.
## Recommended setup
Keep `embedded-files` disabled for development so builds use the workspace
`vendor` directory directly. Reuse the common configuration through YAML
`include`, and enable embedding only for release builds.
`project.yml`:
```yaml
name: myapp
mode: bin
build-dir: build
sources:
- app.php
- src
ignore:
- src/legacy
```
`project-release.yml`:
```yaml
include: project.yml
optimize: 2
embedded-files:
- vendor
- resources/config
```
Build the release:
```bash
composer install --no-dev --classmap-authoritative
tpc project-release.yml
```
The project-root `app.php` starts Composer autoload in the usual way:
```php
<?php
function main(): void
{
require_once __DIR__ . '/vendor/autoload.php';
$application = new App\Application();
$application->run();
}
```
The resulting executable can run without the project source tree or a disk
`vendor` directory:
```bash
mkdir -p /tmp/myapp-release
cp myapp /tmp/myapp-release/
cd /tmp/myapp-release
./myapp
```
Here, standalone means that PHP CLI, Composer installation, and PHP project
files are no longer needed. The executable still has the native dependencies
selected by its build, such as `libphp`, PHPX, and other shared libraries.
Ship those dependencies in the release package or use a supported static-link
configuration.
## Relationship between `sources`, `ignore`, and `embedded-files`
The three settings have separate responsibilities:
| Setting | Purpose |
|---|---|
| `sources` | Select PHP/C/C++ files translated to C++ and native machine code. |
| `ignore` | Remove files from the `sources` scan result. |
| `embedded-files` | Package files verbatim and generate opcodes for PHP files that were not successfully compiled by AOT. |
A file may be selected by both `sources` and `embedded-files`:
- a successfully AOT-compiled PHP file uses its native implementation and
does not receive a duplicate opcode blob;
- a PHP file excluded by `ignore`, or rejected because it uses unsupported AOT
syntax, enters the opcode table;
- `ignore` does not remove files from `embedded-files`;
- `.stub.php` files remain raw API declarations and never receive executable
opcodes.
This lets the application enter `sources` while the complete `vendor` tree is
embedded. AOT-compatible code continues to run as machine code, and ZendVM
executes the remaining dependencies lazily.
```yaml
sources:
- src
- vendor/acme/optimized-package/src
embedded-files:
- vendor
```
There is no separate exclusion list for `embedded-files`. List the required
subdirectories or files instead of their common parent when some content must
not be packaged.
## Configuration syntax
The setting is explicitly enabled in a YAML project and must be a list. Each
entry may be a file, a directory, or a conditional path. Relative paths use the
outermost project YAML directory as their base.
```yaml
embedded-files:
- vendor
- resources/app.json
- path: resources/windows
if: PHP_OS_FAMILY == "Windows"
- path: resources/php85
if: PHP_VERSION_ID >= 80500
```
Directories are scanned recursively and all regular files are archived. JSON,
YAML, templates, and other non-PHP resources retain their original bytes.
Read embedded resources through their original paths:
```php
$config = file_get_contents(__DIR__ . '/resources/app.json');
```
Embedded files are read-only. Logs, caches, uploads, and databases belong in a
separate runtime data directory. Do not rely on `glob()` or directory iteration
over an embedded directory; read resources through known file paths.
## Build requirements
`embedded-files` is available only for a regular `mode: bin` build. It is not
available for `ext`, `lib`, Nano, WASI, iOS, or Android targets.
The build host needs:
1. `php` or `php.exe` matching the target `libphp`;
2. Zend OPcache loadable by that CLI;
3. the same full PHP version, ZTS/NTS mode, Debug mode, and integer width as
the final runtime.
Check the build PHP before compiling:
```bash
php -r 'var_dump(PHP_VERSION, PHP_ZTS, extension_loaded("Zend OPcache"), function_exists("opcache_compile_file"));'
```
TypePHP probes OPcache with `-n` and tries the standard extension locations.
A standalone `tpc` without the matching PHP CLI and OPcache cannot build an
`embedded-files` project.
OPcache is only the build-time serializer. The generated program does not need:
- the OPcache extension;
- PHP CLI;
- a Composer installation;
- disk copies of `vendor/autoload.php` or any other embedded file.
Embedded opcodes are tied to the complete PHP version. If the runtime PHP does
not match the PHP that generated the opcodes, the program reports the mismatch
at startup instead of executing incompatible bytecode.
## Build output and logs
A first build prints messages similar to:
```text
embedded-files: found 3012 files (2886 PHP)
Generating embedded opcodes for 2886 PHP files
Vendor opcode cache: 0 reused, 2886 to generate
Packed 3012 files and 2886 opcode blobs
```
A later build can reuse Composer vendor opcodes:
```text
Vendor opcode cache: 2886 reused, 0 to generate
Packed 3012 files and 2886 opcode blobs
```
Opcode blobs, the archive, and object files live under `build-dir/cache`.
Generated `embedded-opcodes-<name>.cc` contains only readable index code; large
file contents are not expanded into C++ arrays.
A PHP file that OPcache cannot compile and is not expected to be required is
reported as `Skipping non-executable embedded PHP file`; its original bytes are
still archived. If the application can load that file, fix the build error
instead of ignoring the message.
## Vendor opcode cache
Persistent opcode caching is enabled only for a directory named `vendor` whose
root contains `autoload.php`. Its key includes:
- the vendor root path and directory mtime;
- PHP CLI and OPcache binary signatures;
- PHP version, ZTS/Debug mode, and integer width.
Other `embedded-files` directories regenerate their opcodes on every build
because the compiler has no reliable invalidation boundary for them.
Editing an existing nested file does not necessarily change the vendor root
mtime. Use `--force` when this happens:
```bash
tpc project-release.yml --force
```
Composer `install` or `update` normally rebuilds root entries and autoload
files, but a release build should still use `--force` after manual vendor
changes or whenever a cached result is questionable.
## Performance and release guidance
Embedding a large vendor tree increases the executable size, linker input, and
process startup cost. That fixed cost is visible in PHPT, unit tests, and local
workflows that start many short-lived processes. Recommended practice:
- use `project.yml` without `embedded-files` for development and tests;
- enable it only in `project-release.yml`;
- keep the same `build-dir` to reuse opcode, archive, and object caches;
- reserve `--force` for vendor changes that did not invalidate the cache.
Composer autoload remains lazy. ZendVM executes a vendor opcode only when code
first requests the corresponding class; embedding the complete vendor tree
does not execute every PHP file during process startup.
## Frequently asked questions
### Why is Composer autoload still required?
`embedded-files` changes where files are stored and how PHP scripts are
compiled. It does not replace Composer's class map and PSR-4 rules. Require the
same `vendor/autoload.php`; the autoloader and later class files are loaded from
the executable's memory tables.
### Can only `vendor/autoload.php` be embedded?
No. The autoload file contains loading rules, while the actual package files
must also be present. Embed the complete `vendor` directory in normal projects.
### Does the runtime host need OPcache?
No. PHPX provides the decoding and execution integration linked into the
program. The OPcache extension is needed only to generate opcodes at build time.
### Does this protect PHP source code?
Do not treat it as encryption or source protection. The archive includes the
original bytes of selected files to support ordinary file reads. Someone who
can analyze the executable may still extract them.
### What if vendor code depends on another PHP extension?
`embedded-files` packages PHP files; it does not embed implementations such as
`curl` or `pdo_mysql`. Declare them through `ext-deps` or
`extension-dependencies` and provide them in the target runtime.
```yaml
ext-deps:
- curl
- pdo_mysql
```

@ -10,6 +10,7 @@ This directory contains compiler implementation, compatibility, build-mode, and
- [Compiler CLI](COMPILER_CLI.md): current CLI arguments and project configuration.
- [Compilation Modes](COMPILATION_MODES.md): binary, extension, library modes.
- [Quick Start](QUICKSTART.md): the minimal compile flow.
- [Embedding PHP Dependencies in an Executable](EMBEDDED_FILES.md): release setup, Composer autoload, build requirements, and caching for `embedded-files`.
- [Compile-time Functions](COMPILE_TIME_FUNCTIONS.md): `std::any()`, `std::ref()`, `std::expected()`, `std::unexpected()`, and keyword methods.
- [Native Types](NATIVE_TYPES.md), [High-Precision Types](HIGH_PRECISION_TYPES.md), [Std Containers](STD_CONTAINERS.md).
- [Three Object Storage and Passing Models](OBJECT_STORAGE_AND_PASSING_MODELS.md): the responsibilities, ABI, and non-substitutable boundaries of Zend Object, PHPX Box, and Native Class Object.

@ -182,6 +182,21 @@ extension-dependencies:
编译器会为每一项生成 `ZEND_MOD_REQUIRED`。Zend 在加载 TypePHP 模块时检查这些扩展是否已加载。该配置不表示原生链接库;C/C++ 链接依赖仍使用 `link-libs`。
### 内嵌 PHP 依赖和资源
发布用的 `mode: bin` 项目可以通过 `embedded-files` 将 Composer vendor、PHP
fallback 文件和只读资源打包进可执行文件:
```yaml
embedded-files:
- vendor
- resources
```
构建时需要匹配的 PHP CLI 和 OPcache;运行时不需要 PHP CLI、OPcache、Composer
安装或磁盘 vendor。完整用法和限制见
[将 PHP 依赖嵌入可执行文件](EMBEDDED_FILES.md)。
## 查看权威帮助
命令行实现可能继续演进,发布版本的实际参数以以下命令为准:

@ -0,0 +1,262 @@
# 将 PHP 依赖嵌入可执行文件
`embedded-files` 用于把 Composer 依赖、PHP fallback 文件和运行时资源打包进
TypePHP 可执行文件。它主要解决以下部署问题:
- 第三方库不支持 TypePHP AOT,无法放入 `sources` 原生编译;
- 程序仍需通过 Composer autoload 按需加载这些 PHP 类;
- 发布环境不希望执行 `composer install`,也不希望携带独立的 `vendor` 目录;
- 配置、模板、证书等只读文件也需要随程序一起发布。
启用后,TypePHP 在构建期将不能 AOT 编译的 PHP 文件转换为 OPcache opcode,
同时把配置中选中的全部文件写入可执行文件。运行时 `require`、`require_once` 和
Composer autoload 直接从内存中的 opcode 表加载 PHP 文件,普通只读文件则通过
内存文件表读取。
## 推荐用法
开发构建不启用 `embedded-files`,直接使用工作区中的 `vendor`。发布构建通过
YAML `include` 复用公共配置,并只在发布阶段嵌入依赖。
`project.yml`:
```yaml
name: myapp
mode: bin
build-dir: build
sources:
- app.php
- src
ignore:
- src/legacy
```
`project-release.yml`:
```yaml
include: project.yml
optimize: 2
embedded-files:
- vendor
- resources/config
```
构建发布版本:
```bash
composer install --no-dev --classmap-authoritative
tpc project-release.yml
```
项目根目录的 `app.php` 仍按普通 Composer 方式启动自动加载器:
```php
<?php
function main(): void
{
require_once __DIR__ . '/vendor/autoload.php';
$application = new App\Application();
$application->run();
}
```
生成的可执行文件可以移到没有项目源码和 `vendor` 目录的位置运行:
```bash
mkdir -p /tmp/myapp-release
cp myapp /tmp/myapp-release/
cd /tmp/myapp-release
./myapp
```
这里的“独立运行”是指不再需要 PHP CLI、Composer 安装和磁盘上的 PHP 项目文件。
程序仍需要构建方式所对应的 `libphp`、PHPX 及其他原生共享库;这些依赖需随发布包
提供,或使用项目支持的静态链接方式处理。
## `sources`、`ignore` 和 `embedded-files` 的关系
三个配置项承担不同职责:
| 配置 | 作用 |
|---|---|
| `sources` | 选择要翻译为 C++ 并编译为机器码的 PHP/C/C++ 文件。 |
| `ignore` | 从 `sources` 的扫描结果中排除文件。 |
| `embedded-files` | 选择要原样打包进可执行文件的文件,并为没有成功 AOT 编译的 PHP 文件生成 opcode。 |
一个文件可以同时由 `sources` 和 `embedded-files` 选中:
- 成功进入 AOT 的 PHP 文件使用原生实现,不再生成重复 opcode;
- 被 `ignore` 排除或因不支持的语法而未进入 AOT 的 PHP 文件会进入 opcode 表;
- `ignore` 不会从 `embedded-files` 中删除文件;
- `.stub.php` 只作为 API 声明原样打包,不会生成可执行 opcode。
因此可以先让应用代码进入 `sources`,再将整个 `vendor` 放入
`embedded-files`。支持 AOT 的文件继续以机器码运行,其余依赖由 ZendVM 按需执行。
```yaml
sources:
- src
- vendor/acme/optimized-package/src
embedded-files:
- vendor
```
`embedded-files` 没有单独的排除列表。若不希望打包某个子目录,应列出实际需要的
目录或文件,而不是配置整个上级目录。
## 支持的配置格式
该配置只在 YAML 项目中显式启用,值必须是列表。每一项可以是文件、目录或带条件的
路径,相对路径以最外层项目 YAML 所在目录为基准。
```yaml
embedded-files:
- vendor
- resources/app.json
- path: resources/windows
if: PHP_OS_FAMILY == "Windows"
- path: resources/php85
if: PHP_VERSION_ID >= 80500
```
目录会被递归扫描,其中的所有普通文件都会进入归档。除 PHP 文件外,JSON、YAML、
模板和其他资源也会按原始字节保存。
运行时可以使用原来的路径读取已嵌入文件:
```php
$config = file_get_contents(__DIR__ . '/resources/app.json');
```
内嵌文件是只读的。程序产生的日志、缓存、上传文件和数据库应写入单独的运行时数据
目录。不要依赖对内嵌目录执行 `glob()` 或目录遍历;应使用已知文件路径读取资源。
## 构建环境要求
`embedded-files` 仅支持普通的 `mode: bin` 构建。它不适用于 `ext`、`lib`、Nano、
WASI、iOS 或 Android 目标。
构建机必须具备:
1. 与目标 `libphp` 匹配的 `php` 或 `php.exe`;
2. 可由该 CLI 加载的 Zend OPcache 扩展;
3. 与最终运行时一致的 PHP 完整版本、ZTS/NTS、Debug 模式和整数宽度。
可以先检查构建 PHP:
```bash
php -r 'var_dump(PHP_VERSION, PHP_ZTS, extension_loaded("Zend OPcache"), function_exists("opcache_compile_file"));'
```
TypePHP 自己也会用 `-n` 探测 OPcache,并尝试加载标准位置中的扩展。仅有 `tpc`
可执行文件但没有匹配的 PHP CLI 和 OPcache 时,无法构建 `embedded-files`。
OPcache 只参与构建期序列化。运行生成的程序时:
- 不需要启用或安装 OPcache 扩展;
- 不需要 PHP CLI;
- 不需要执行 `composer install`;
- 不需要磁盘上的 `vendor/autoload.php` 或其他已嵌入文件。
二进制中的 opcode 与 PHP 完整版本绑定。若运行时 PHP 与生成 opcode 的 PHP 版本
不同,程序会在启动时报告版本不匹配,而不会继续执行不兼容的字节码。
## 构建输出和日志
首次构建会看到类似输出:
```text
embedded-files: found 3012 files (2886 PHP)
Generating embedded opcodes for 2886 PHP files
Vendor opcode cache: 0 reused, 2886 to generate
Packed 3012 files and 2886 opcode blobs
```
再次构建时,满足缓存条件的 Composer vendor opcode 会被复用:
```text
Vendor opcode cache: 2886 reused, 0 to generate
Packed 3012 files and 2886 opcode blobs
```
内部 opcode、归档和对象文件位于 `build-dir/cache`。生成的
`embedded-opcodes-<name>.cc` 只包含可读的索引代码,大块文件内容不会展开成 C++
数组。
OPcache 无法编译且预期不会被 `require` 的 PHP 文件会显示
`Skipping non-executable embedded PHP file`,文件原始内容仍会被打包。若程序实际会
加载该文件,应修复构建错误,不能忽略这条日志。
## vendor opcode 缓存
只有目录名为 `vendor` 且根目录存在 `autoload.php` 时,TypePHP 才启用持久 opcode
缓存。缓存键包含:
- vendor 根目录路径和目录 mtime;
- PHP CLI 与 OPcache 二进制签名;
- PHP 版本、ZTS/Debug 模式及整数宽度。
其他 `embedded-files` 目录每次构建都会重新生成 opcode,因为编译器无法确定它们的
可靠失效边界。
直接修改 vendor 中一个已有文件不一定改变 vendor 根目录 mtime。此时使用
`--force` 重新生成:
```bash
tpc project-release.yml --force
```
Composer `install` 或 `update` 通常会重建根目录内容和 autoload 文件,但发布流程仍应
在依赖发生人工修改或缓存结果可疑时使用 `--force`。
## 性能和发布建议
嵌入大量 vendor 文件会增大最终二进制、链接输入和进程启动成本。对于频繁启动大量
短进程的 PHPT、单元测试或本地开发,这部分固定成本会很明显。因此建议:
- 开发和测试使用不含 `embedded-files` 的 `project.yml`;
- 只在交付构建使用 `project-release.yml`;
- 保留同一个 `build-dir` 以复用 opcode、归档和对象缓存;
- 修改 vendor 内容但缓存未失效时再使用 `--force`。
Composer autoload 仍是懒加载:只有代码首次引用某个类时,对应 vendor opcode 才由
ZendVM 执行。打包整个 vendor 不会在程序启动时执行全部 PHP 文件。
## 常见问题
### 为什么配置后仍然需要 Composer autoload?
`embedded-files` 改变的是文件保存位置和 PHP 文件的编译来源,不会替代 Composer
生成的类映射与 PSR-4 规则。程序仍然 `require_once` 同一个
`vendor/autoload.php`,只是该文件和后续类文件都从二进制内存表加载。
### 能否只把 `vendor/autoload.php` 放进去?
不能。autoload 文件只包含加载规则,真正的包文件也必须存在于 `embedded-files`
中。通常应直接配置整个 `vendor`。
### 运行主机需要 OPcache 吗?
不需要。反序列化和执行支持已经随 PHPX 链入程序。OPcache 扩展只用于构建 opcode。
### 这是否能保护 PHP 源码?
不要把它当作加密或源码保护功能。为了支持普通文件读取,归档中包含所选文件的原始
字节;能够分析二进制文件的人仍可能提取这些内容。
### vendor 代码依赖其他 PHP 扩展怎么办?
`embedded-files` 只打包 PHP 文件,不会把 `curl`、`pdo_mysql` 等扩展实现打进程序。
应使用 `ext-deps` / `extension-dependencies` 声明依赖,并在目标运行时提供对应扩展。
```yaml
ext-deps:
- curl
- pdo_mysql
```

@ -10,6 +10,7 @@
- [编译器命令行](COMPILER_CLI.md):当前 CLI 参数和项目配置。
- [编译模式](COMPILATION_MODES.md):binary、extension、library 模式。
- [快速入门](QUICKSTART.md):最小编译流程。
- [将 PHP 依赖嵌入可执行文件](EMBEDDED_FILES.md):`embedded-files` 的发布配置、Composer autoload、构建依赖和缓存规则。
- [编译期函数](COMPILE_TIME_FUNCTIONS.md):`std::any()`、`std::ref()`、`std::expected()`、`std::unexpected()` 和关键词方法。
- [原生类型](NATIVE_TYPES.md)、[高精度类型](HIGH_PRECISION_TYPES.md)、[Std 容器](STD_CONTAINERS.md)。
- [三套对象存储与传递模型](OBJECT_STORAGE_AND_PASSING_MODELS.md):Zend Object、PHPX Box 与 Native Class Object 的职责、ABI 和不可替代边界。

@ -3,9 +3,9 @@
/**
* TypePHP cross-platform release packager.
*
* Windows produces a self-contained PHP/PHPX SDK package. Linux contains the
* tested ELF compiler and production Composer vendor tree, but no host native
* libraries. All platforms share version, staging, verification, and cleanup.
* Windows produces a self-contained PHP/PHPX SDK package. Linux and macOS
* contain the tested compiler plus release documentation. Composer runtime
* files are embedded in the compiler and must not be duplicated in packages.
*/
if (!chdir(__DIR__)) {
@ -15,8 +15,8 @@ if (!chdir(__DIR__)) {
if (in_array('--help', $argv ?? [], true) || in_array('-h', $argv ?? [], true)) {
echo "Usage: php package.php\n\n";
echo "Windows: requires PHP_HOME and PHPX_HOME; creates a self-contained SDK package.\n";
echo "Linux: uses strip; packages the tested ELF and production Composer vendor tree.\n";
echo "macOS: uses strip; packages the tested Mach-O binary and production Composer vendor tree.\n";
echo "Linux: uses strip; packages the tested ELF and release documentation.\n";
echo "macOS: uses strip; packages the tested Mach-O binary and release documentation.\n";
echo "Supported architectures: 64-bit CPUs, including x64 and ARM64.\n";
exit(0);
}
@ -73,10 +73,13 @@ if (PHP_INT_SIZE !== 8) {
$arch = normalizeArchitecture((string)$processorArchitecture);
$osType = 'windows';
$outputFile = "tpc_v{$versionId}_{$osType}_{$arch}.zip";
$phpAbi = packagePhpAbi();
$topLevelDir = "tpc_v{$versionId}_{$osType}_{$arch}_{$phpAbi}";
$outputFile = $topLevelDir . '.zip';
echo "操作系统: {$osType}\n";
echo "硬件架构: {$arch}\n";
echo "PHP ABI: {$phpAbi}\n";
echo "输出文件: {$outputFile}\n\n";
// ==================== 3. 检查必要文件 ====================
@ -85,8 +88,8 @@ echo "[3/7] 检查必要文件...\n";
$requiredFiles = [
$compilerExe,
'README.md',
'README-CN.md',
'LICENSE',
'composer.json',
'examples/hello.php',
];
@ -97,6 +100,8 @@ foreach ($requiredFiles as $file) {
}
}
verifyEmbeddedComposerRuntime($compilerExe);
$phpEmbedLibCandidates = [
'SDK/lib/php8embed.lib' => "{$phpDir}/SDK/lib/php8embed.lib",
'lib/php8embed.lib' => "{$phpDir}/lib/php8embed.lib",
@ -179,7 +184,6 @@ foreach ($requiredPhpxPaths as $description => $path) {
echo "所有文件检查通过\n\n";
$topLevelDir = "tpc_v{$versionId}_{$osType}_{$arch}";
if (is_dir($topLevelDir)) {
echo "清理旧的临时目录...\n";
removeDirectory($topLevelDir);
@ -214,7 +218,7 @@ TypePHP Windows setup
=====================
TypePHP for Windows uses the prebuilt PHPX DLL and import library included in
this package. The Composer path vendor\swoole\phpx is not used on Windows.
this package. Composer runtime dependencies are embedded in tpc.exe.
Open a command prompt in this directory and run:
@ -296,8 +300,8 @@ mustCreateDirectory("{$topLevelDir}/examples");
// 复制项目文件
$projectFiles = [
'README.md' => '.',
'README-CN.md' => '.',
'LICENSE' => '.',
'composer.json' => '.',
'examples/hello.php' => 'examples',
];
@ -332,29 +336,6 @@ if (is_dir($tetrisWin32Dir)) {
echo "项目文件复制完成\n\n";
// ==================== 6.3. 复制 vendor 目录 ====================
echo "[6.3/7] 复制 vendor 目录...\n";
$vendorDir = 'vendor';
if (!is_dir($vendorDir)) {
echo "错误: vendor 目录不存在,请先运行 composer install\n";
exit(1);
} else {
echo "复制 vendor 目录...\n";
// 排除 vendor/swoole/phpx 目录(Windows 下不需要 composer 安装的 phpx)
copyDirectory($vendorDir, "{$topLevelDir}/vendor", ['swoole/phpx']);
// 统计 vendor 目录的文件数量
$vendorIterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator("{$topLevelDir}/vendor"),
RecursiveIteratorIterator::LEAVES_ONLY
);
$vendorFileCount = iterator_count($vendorIterator);
echo "已复制 {$vendorFileCount} 个文件\n";
}
echo "\n";
// ==================== 6.5. 复制 phpx 文件 ====================
echo "[6.5/7] 复制 phpx 相关文件...\n";
@ -441,6 +422,9 @@ if (!$zip->close()) {
$requiredArchiveEntries = [
"{$topLevelDir}/{$compilerExe}",
"{$topLevelDir}/README.md",
"{$topLevelDir}/README-CN.md",
"{$topLevelDir}/LICENSE",
"{$topLevelDir}/php.exe",
"{$topLevelDir}/php.ini",
"{$topLevelDir}/ext/php_zip.dll",
@ -467,6 +451,13 @@ foreach ($requiredArchiveEntries as $entry) {
throw new RuntimeException("压缩包缺少必需文件: {$entry}");
}
}
for ($index = 0; $index < $verificationZip->numFiles; $index++) {
$entry = $verificationZip->getNameIndex($index);
if (is_string($entry) && str_starts_with($entry, "{$topLevelDir}/vendor/")) {
$verificationZip->close();
throw new RuntimeException("压缩包不应包含 vendor 目录: {$entry}");
}
}
$verificationZip->close();
echo "✓ 压缩包创建成功\n\n";
@ -501,9 +492,8 @@ echo " - phpx/include/ (PHPX 头文件)\n";
echo " - phpx/lib/ (PHPX 库文件)\n";
echo " - phpx/src/misc/ (PHPX Embed/CLI runtime adapters)\n";
echo " - PHP 运行时环境 (完整目录结构)\n";
echo " - vendor/ (Composer 依赖包,无需再次安装)\n";
echo " - composer.json (Composer 配置文件)\n";
echo " - README.md/LICENSE (文档)\n";
echo " - Composer 运行时依赖(已内嵌到 {$compilerExe})\n";
echo " - README.md/README-CN.md/LICENSE (文档)\n";
echo " - examples/hello.php (PHP 示例代码)\n";
echo " - examples/win32-hello/ (Windows GUI 编程实例)\n";
echo " - examples/tetris-win32/ (俄罗斯方块游戏实例)\n\n";
@ -584,20 +574,19 @@ function packageUnixLike(): void
}
$arch = normalizeArchitecture(php_uname('m'));
$phpAbi = packagePhpAbi();
$binary = 'tpc';
$requiredFiles = [
$binary,
'vendor/autoload.php',
'vendor/composer/installed.php',
];
$releaseFiles = ['README.md', 'README-CN.md', 'LICENSE'];
$requiredFiles = [$binary, ...$releaseFiles];
foreach ($requiredFiles as $file) {
if (!is_file($file)) {
throw new RuntimeException("Required package file not found: {$file}");
}
}
verifyEmbeddedComposerRuntime($binary);
$versionId = resolvePackageVersion();
$topLevelDir = "tpc_v{$versionId}_{$osType}_{$arch}";
$topLevelDir = "tpc_v{$versionId}_{$osType}_{$arch}_{$phpAbi}";
$outputFile = $topLevelDir . '.tar.gz';
echo "========================================\n";
@ -605,6 +594,7 @@ function packageUnixLike(): void
echo "========================================\n";
echo "Version: {$versionId}\n";
echo "Architecture: {$arch}\n";
echo "PHP ABI: {$phpAbi}\n";
echo "Output: {$outputFile}\n\n";
if (is_dir($topLevelDir)) {
@ -637,6 +627,9 @@ function packageUnixLike(): void
if (!chmod($stagedBinary, 0755)) {
throw new RuntimeException("Unable to mark executable: {$stagedBinary}");
}
foreach ($releaseFiles as $releaseFile) {
mustCopy($releaseFile, "{$topLevelDir}/{$releaseFile}");
}
exec('command -v strip 2>/dev/null', $stripPath, $stripStatus);
if ($stripStatus !== 0) {
@ -652,36 +645,9 @@ function packageUnixLike(): void
throw new RuntimeException("strip failed:\n" . implode("\n", $stripOutput));
}
// Linux and macOS releases intentionally use the target system's libphp,
// libphpx, and other native dependencies. Composer sources and headers are
// portable, but test-built host libraries must not enter the archive.
mustCreateDirectory("{$topLevelDir}/vendor");
copyDirectory(
'vendor',
"{$topLevelDir}/vendor",
[],
null,
['a', 'dll', 'dylib', 'exe', 'exp', 'lib', 'o', 'obj', 'pdb', 'so'],
);
$requiredEntries = [
"{$topLevelDir}/{$binary}",
"{$topLevelDir}/vendor/autoload.php",
"{$topLevelDir}/vendor/composer/installed.php",
];
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(
"{$topLevelDir}/vendor",
FilesystemIterator::SKIP_DOTS,
),
RecursiveIteratorIterator::LEAVES_ONLY,
);
foreach ($files as $file) {
if ($file->isFile() && isNativeLibraryPath($file->getPathname())) {
throw new RuntimeException(
"{$osType} package unexpectedly contains a native library: {$file->getPathname()}",
);
}
$requiredEntries = ["{$topLevelDir}/{$binary}"];
foreach ($releaseFiles as $releaseFile) {
$requiredEntries[] = "{$topLevelDir}/{$releaseFile}";
}
exec('command -v tar 2>/dev/null', $tarPath, $tarStatus);
@ -705,6 +671,11 @@ function packageUnixLike(): void
throw new RuntimeException("Archive is missing required entry: {$entry}");
}
}
foreach ($archiveEntries as $entry) {
if (str_starts_with($entry, "{$topLevelDir}/vendor/")) {
throw new RuntimeException("Archive must not contain a vendor directory: {$entry}");
}
}
removeDirectory($topLevelDir);
$cleanupStage = false;
@ -719,12 +690,47 @@ function formatMegabytes(int $bytes): string
return number_format($bytes / 1024 / 1024, 3, '.', '');
}
function isNativeLibraryPath(string $path): bool
function packagePhpAbi(): string
{
return preg_match(
'/\.(?:a|dll|dylib(?:\.\d+)*|exe|exp|lib|o|obj|pdb|so(?:\.\d+)*)$/i',
$path,
) === 1;
return 'php' . PHP_VERSION . '-' . (PHP_ZTS ? 'zts' : 'nts');
}
/**
* Prove that the release compiler loads Composer from its embedded file table.
* Keeping the source vendor directory visible would let a non-embedded binary
* pass the ordinary --version smoke test and produce a broken release package.
*/
function verifyEmbeddedComposerRuntime(string $compiler): void
{
$vendorDirectory = 'vendor';
if (!is_dir($vendorDirectory)) {
throw new RuntimeException('The build vendor directory is required to verify the embedded compiler');
}
$hiddenVendorDirectory = '.vendor-package-check-' . getmypid();
if (file_exists($hiddenVendorDirectory)) {
throw new RuntimeException("Temporary vendor path already exists: {$hiddenVendorDirectory}");
}
if (!rename($vendorDirectory, $hiddenVendorDirectory)) {
throw new RuntimeException('Unable to hide vendor while verifying the embedded compiler');
}
$status = 1;
$output = [];
try {
exec(escapeshellarg(realpath($compiler) ?: $compiler) . ' --version 2>&1', $output, $status);
} finally {
if (!rename($hiddenVendorDirectory, $vendorDirectory)) {
throw new RuntimeException('Unable to restore vendor after verifying the embedded compiler');
}
}
if ($status !== 0) {
throw new RuntimeException(
"Release compiler cannot start without a vendor directory:\n" . implode("\n", $output),
);
}
echo "Embedded Composer runtime check passed without vendor/\n";
}
/**

Loading…
Cancel
Save