diff --git a/docs/WASI_BUILD.md b/docs/WASI_BUILD.md index 32bbb624..2b0bfbc7 100644 --- a/docs/WASI_BUILD.md +++ b/docs/WASI_BUILD.md @@ -8,6 +8,7 @@ TypePHP 使用稳定的 WASI 0.2(Preview 2)和 Component Model。TypePHP 生 - PHP 8.4 或更高版本,用于运行 TypePHP 编译器 - Wasmtime 47 或更高版本,用于运行和测试产物 - Jco 1 或更高版本,用于 browser profile;component profile 不需要 Jco +- wit-bindgen-cli 0.60.0,用于 library/WasmExport 模式;command 模式不需要 - 与当前 TypePHP 版本绑定的 `wasm32-wasip2` 集成 SDK WASI SDK 的 `bin` 目录和 Wasmtime 必须加入系统 `PATH`。编译器不会探测或使用 `/opt` 等约定安装目录,也不接受专用的工具目录配置。WASI 静态库和头文件统一安装到 PHPX 的 `wasm/wasm32-wasip2/`: @@ -18,7 +19,7 @@ export PATH="::$PATH" TypePHP 使用现有的 PHPX 定位规则:优先读取 `PHPX_HOME`,其次读取 Composer 的 `swoole/phpx` 安装位置,最后使用 `vendor/swoole/phpx`。不新增 WASI 专用环境变量。 -WASI 构建会检查 `wasm32-wasip2-clang`、`wasm32-wasip2-clang++`、`llvm-ar`、`llvm-ranlib`、`llvm-nm`、`wasm-component-ld` 和 `wasmtime`,并确认目标是 `wasm32-unknown-wasip2`。browser profile 另外检查 `jco`。所有工具只从 `PATH` 查找;npm script 会自动将项目本地的 `node_modules/.bin` 加入 `PATH`。 +WASI 构建会检查 `wasm32-wasip2-clang`、`wasm32-wasip2-clang++`、`llvm-ar`、`llvm-ranlib`、`llvm-nm`、`wasm-component-ld` 和 `wasmtime`,并确认目标是 `wasm32-unknown-wasip2`。browser profile 另外检查 `jco`,library 模式另外检查固定版本的 `wit-bindgen`。所有工具只从 `PATH` 查找;npm script 会自动将项目本地的 `node_modules/.bin` 加入 `PATH`。 ## 一条命令构建 @@ -63,7 +64,7 @@ sources: 路径、sources 等详细配置继续放在 `project.yml`,不通过 `--wasm=` 传递。 -PHP、PHPX、TypePHP runtime、GMP、MPFR 和 mpdecimal 由 SDK 发布阶段预编译为 WASI 静态库。应用构建只编译 TypePHP 为当前程序生成的 C++,然后链接这些 `.a`。`tpc --wasm` 不会下载源码,也不会调用 PHP、PHPX 或高精度库的构建脚本。library 模式会调用 PHPX 包内固定版本的 `wit-bindgen` 生成当前应用的 Canonical ABI 绑定;普通用户不需要从 `PATH` 安装它。 +PHP、PHPX、TypePHP runtime、GMP、MPFR 和 mpdecimal 由 SDK 发布阶段预编译为 WASI 静态库。应用构建只编译 TypePHP 为当前程序生成的 C++,然后链接这些 `.a`。`tpc --wasm` 不会下载源码,也不会调用 PHP、PHPX 或高精度库的构建脚本。library 模式会调用 `PATH` 中的 `wit-bindgen-cli 0.60.0` 生成当前应用的 Canonical ABI 绑定。 PHP/WASI 当前静态内建 `date`、`pcre`、`hash`、`json`、`lexbor`、`random`、`Reflection`、`SPL`、`standard`、`uri`、`ctype`、`calendar`、`bcmath`、`filter`、`tokenizer`、`mbstring`、`zlib`、`fileinfo`、`sodium`、`openssl`、`libxml`、`dom`、`SimpleXML`、`xml`、`xmlreader`、`xmlwriter`、`PDO`、`pdo_sqlite`、`zip`、`bz2` 和 `exif` 扩展。OpenSSL 采用 crypto-only 构建,不包含 TLS stream transport;HTTP/HTTPS 仍由 WASI HTTP Component 提供。 diff --git a/package.php b/package.php index 7f58f22a..89aec679 100755 --- a/package.php +++ b/package.php @@ -707,7 +707,9 @@ function packageUnixLike(): void mustCreateDirectory("{$topLevelDir}/completions"); mustCopy('completions/tpc.bash', "{$topLevelDir}/completions/tpc.bash"); copyDirectory('wasm', "{$topLevelDir}/wasm"); - copyDirectory("{$phpxSourceDir}/wasm", "{$topLevelDir}/vendor/swoole/phpx/wasm"); + // Host build tools such as wit-bindgen are installed independently and + // discovered through PATH; only the target SDK belongs in the package. + copyDirectory("{$phpxSourceDir}/wasm", "{$topLevelDir}/vendor/swoole/phpx/wasm", ['bin']); if (is_file($outputFile) && !unlink($outputFile)) { throw new RuntimeException("Unable to remove existing archive: {$outputFile}"); diff --git a/phpunit/src/Build/WasiToolchainTest.php b/phpunit/src/Build/WasiToolchainTest.php index d620c9ef..c27f2042 100644 --- a/phpunit/src/Build/WasiToolchainTest.php +++ b/phpunit/src/Build/WasiToolchainTest.php @@ -65,6 +65,28 @@ final class WasiToolchainTest extends TestCase $this->assertSame('wasm32-unknown-wasip2', $tools['target']); } + public function testLibraryModeRequiresPinnedWitBindgenFromPath(): void + { + $this->installFakeTools(22, 47, 1, 'wasm32-unknown-wasip2'); + putenv('PATH=' . $this->directory); + + $tools = (new WasiToolchain())->detect(false, true); + + $this->assertSame($this->directory . '/wit-bindgen', $tools['wit-bindgen']); + $this->assertSame('0.60.0', $tools['wit-bindgen-version']); + } + + public function testLibraryModeRejectsIncompatibleWitBindgen(): void + { + $this->installFakeTools(22, 47, 1, 'wasm32-unknown-wasip2'); + $this->writeExecutable('wit-bindgen', "#!/bin/sh\necho 'wit-bindgen-cli 0.61.0'\n"); + putenv('PATH=' . $this->directory); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('wit-bindgen-cli 0.60.0 is required'); + (new WasiToolchain())->detect(false, true); + } + public function testRejectsOldLlvm(): void { $this->installFakeTools(21, 47, 1, 'wasm32-unknown-wasip2'); @@ -97,6 +119,7 @@ final class WasiToolchainTest extends TestCase ); $this->writeExecutable('wasmtime', "#!/bin/sh\necho 'wasmtime {$wasmtimeMajor}.0.0'\n"); $this->writeExecutable('jco', "#!/bin/sh\necho 'jco {$jcoMajor}.0.0'\n"); + $this->writeExecutable('wit-bindgen', "#!/bin/sh\necho 'wit-bindgen-cli 0.60.0'\n"); } private function writeExecutable(string $name, string $contents): void diff --git a/src/Build/WasiToolchain.php b/src/Build/WasiToolchain.php index ac9fb036..fc84436a 100644 --- a/src/Build/WasiToolchain.php +++ b/src/Build/WasiToolchain.php @@ -9,9 +9,10 @@ final class WasiToolchain public const MIN_LLVM_MAJOR = 22; public const MIN_WASMTIME_MAJOR = 47; public const MIN_JCO_MAJOR = 1; + public const WIT_BINDGEN_VERSION = '0.60.0'; /** @return array */ - public function detect(bool $requireBrowserTools = true): array + public function detect(bool $requireBrowserTools = true, bool $requireWitBindgen = false): array { $tools = []; $requiredTools = [ @@ -26,6 +27,9 @@ final class WasiToolchain if ($requireBrowserTools) { $requiredTools[] = 'jco'; } + if ($requireWitBindgen) { + $requiredTools[] = 'wit-bindgen'; + } foreach ($requiredTools as $name) { $tools[$name] = $this->findExecutable($name); } @@ -39,6 +43,19 @@ final class WasiToolchain if ($requireBrowserTools) { $versions['jco'] = $this->requireVersion('jco', $tools['jco'], self::MIN_JCO_MAJOR); } + if ($requireWitBindgen) { + [$exitCode, $output, $error] = $this->run([$tools['wit-bindgen'], '--version']); + $version = trim($output . "\n" . $error); + $expectedVersion = 'wit-bindgen-cli ' . self::WIT_BINDGEN_VERSION; + if ($exitCode !== 0 || $version !== $expectedVersion) { + throw new RuntimeException( + "WASI tool `wit-bindgen` has an incompatible version: " + . ($version !== '' ? $version : 'unknown') + . "; {$expectedVersion} is required", + ); + } + $versions['wit-bindgen'] = self::WIT_BINDGEN_VERSION; + } [$exitCode, $target, $error] = $this->run([$tools['wasm32-wasip2-clang++'], '--print-target-triple']); $target = trim($target); @@ -59,6 +76,9 @@ final class WasiToolchain if ($requireBrowserTools) { $tools['jco-version'] = $versions['jco']; } + if ($requireWitBindgen) { + $tools['wit-bindgen-version'] = $versions['wit-bindgen']; + } return $tools; } diff --git a/src/Build/WasmInterfaceGenerator.php b/src/Build/WasmInterfaceGenerator.php index bb4388e7..2177e018 100644 --- a/src/Build/WasmInterfaceGenerator.php +++ b/src/Build/WasmInterfaceGenerator.php @@ -151,7 +151,7 @@ final class WasmInterfaceGenerator /** * Render the small TypePHP-specific half of the C binding. The generic * Canonical ABI half and component type object are emitted by the pinned - * wit-bindgen binary shipped with PHPX. + * wit-bindgen version detected in PATH. * * @param array $manifest */ diff --git a/wasm/README.md b/wasm/README.md index 1644deb7..4b72d965 100644 --- a/wasm/README.md +++ b/wasm/README.md @@ -64,11 +64,10 @@ GMP, MPFR, the vendored mpdecimal, `libphpx.a`, and their headers. The orchestrator validates the combined installation before writing `.typephp-wasi-sdk-abi`. -Autoconf, Bison, re2c, Rust, upstream `wit-bindgen`, and the PHP/PHPX source -trees are SDK producer dependencies only. They are never searched for by an -application build. PHPX release packages include the pinned host-side -`phpx-wit-bindgen` needed for application-specific exports below -`/wasm/bin/-/`. +Autoconf, Bison, re2c, and the PHP/PHPX source trees are SDK producer +dependencies only. They are never searched for by an application build. +Library components additionally require the pinned host-side +`wit-bindgen-cli 0.60.0` in `PATH`; command components do not use it. WASM integration checks live in `tests/wasm` and run through `run-tests.php`. The target-independent high-precision TypePHP example lives at diff --git a/wasm/build-program.sh b/wasm/build-program.sh index 7143f20e..a3ae4e92 100755 --- a/wasm/build-program.sh +++ b/wasm/build-program.sh @@ -158,22 +158,21 @@ include_flags=( -I"${generated_dir}/include" ) -# WIT is application-specific, but its generator is a host build tool. PHPX -# packages a pinned wit-bindgen binary per host so users never install it or a -# Rust toolchain. It is not linked into PHPX or the resulting component. +# WIT is application-specific, and its generator is a host build tool found by +# tpc in PATH. It is not linked into PHPX or the resulting component. binding_objects=() if [[ "${wasm_mode}" == library ]]; then bindgen=${TYPEPHP_WIT_BINDGEN:?TYPEPHP_WIT_BINDGEN is required in library mode} if [[ ! -x "${bindgen}" ]]; then fatal_error \ - "PHPX bundled WIT binding generator is missing: ${bindgen}" \ - "Install the matching PHPX package; installing wit-bindgen separately is not required." + "WASI build tool wit-bindgen is unavailable: ${bindgen}" \ + "Install wit-bindgen-cli 0.60.0 and add it to PATH." fi bindgen_version=$("${bindgen}" --version 2>/dev/null || true) if [[ "${bindgen_version}" != 'wit-bindgen-cli 0.60.0' ]]; then fatal_error \ - "PHPX bundled WIT binding generator has an incompatible version: ${bindgen_version:-unknown}" \ - "Expected wit-bindgen-cli 0.60.0 from the matching PHPX package." + "WASI build tool wit-bindgen has an incompatible version: ${bindgen_version:-unknown}" \ + "Install wit-bindgen-cli 0.60.0 and add it to PATH." fi binding_world=${TYPEPHP_WASM_WORLD//-/_} "${bindgen}" c \