- 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 requirementsmaster
parent
cbdf5d6a11
commit
43e85b8866
13 changed files with 714 additions and 105 deletions
@ -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 |
||||
``` |
||||
@ -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 |
||||
``` |
||||
Loading…
Reference in new issue