From 43e85b8866de03b933aa93f3985fa0a5adec1853 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Wed, 23 Sep 2026 08:35:40 +0800 Subject: [PATCH] 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 --- .github/actions/unix-arm64-build/action.yml | 8 +- .github/workflows/linux-x64.yml | 8 +- .github/workflows/release.yml | 14 +- .github/workflows/windows-build.yml | 15 +- README-CN.md | 13 +- README.md | 18 +- docs/en/COMPILER_CLI.md | 16 ++ docs/en/EMBEDDED_FILES.md | 288 ++++++++++++++++++++ docs/en/README.md | 1 + docs/zh-cn/COMPILER_CLI.md | 15 + docs/zh-cn/EMBEDDED_FILES.md | 262 ++++++++++++++++++ docs/zh-cn/README.md | 1 + package.php | 160 +++++------ 13 files changed, 714 insertions(+), 105 deletions(-) create mode 100644 docs/en/EMBEDDED_FILES.md create mode 100644 docs/zh-cn/EMBEDDED_FILES.md diff --git a/.github/actions/unix-arm64-build/action.yml b/.github/actions/unix-arm64-build/action.yml index ab999d26..d6cebaf9 100644 --- a/.github/actions/unix-arm64-build/action.yml +++ b/.github/actions/unix-arm64-build/action.yml @@ -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() diff --git a/.github/workflows/linux-x64.yml b/.github/workflows/linux-x64.yml index 47726eeb..1908e4b0 100644 --- a/.github/workflows/linux-x64.yml +++ b/.github/workflows/linux-x64.yml @@ -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() diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a665fbea..ecc3dbbe 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index 46e0908a..5e351a18 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -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() diff --git a/README-CN.md b/README-CN.md index 8fa22b49..9c526a3d 100644 --- a/README-CN.md +++ b/README-CN.md @@ -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 参数与项目配置 diff --git a/README.md b/README.md index 58517508..5e8e53b8 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/en/COMPILER_CLI.md b/docs/en/COMPILER_CLI.md index fa53c827..c9e377bc 100644 --- a/docs/en/COMPILER_CLI.md +++ b/docs/en/COMPILER_CLI.md @@ -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: diff --git a/docs/en/EMBEDDED_FILES.md b/docs/en/EMBEDDED_FILES.md new file mode 100644 index 00000000..1cbf3d27 --- /dev/null +++ b/docs/en/EMBEDDED_FILES.md @@ -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 +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-.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 +``` diff --git a/docs/en/README.md b/docs/en/README.md index 5dbeac92..c96bf48c 100644 --- a/docs/en/README.md +++ b/docs/en/README.md @@ -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. diff --git a/docs/zh-cn/COMPILER_CLI.md b/docs/zh-cn/COMPILER_CLI.md index dedb1837..36e6e797 100644 --- a/docs/zh-cn/COMPILER_CLI.md +++ b/docs/zh-cn/COMPILER_CLI.md @@ -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)。 + ## 查看权威帮助 命令行实现可能继续演进,发布版本的实际参数以以下命令为准: diff --git a/docs/zh-cn/EMBEDDED_FILES.md b/docs/zh-cn/EMBEDDED_FILES.md new file mode 100644 index 00000000..57da817f --- /dev/null +++ b/docs/zh-cn/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 +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-.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 +``` diff --git a/docs/zh-cn/README.md b/docs/zh-cn/README.md index 53cc7782..ec128a55 100644 --- a/docs/zh-cn/README.md +++ b/docs/zh-cn/README.md @@ -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 和不可替代边界。 diff --git a/package.php b/package.php index f17b0356..337442cf 100755 --- a/package.php +++ b/package.php @@ -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 '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 { - return preg_match( - '/\.(?:a|dll|dylib(?:\.\d+)*|exe|exp|lib|o|obj|pdb|so(?:\.\d+)*)$/i', - $path, - ) === 1; + $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"; } /**