diff --git a/docs/TYPEPHP_WASI_BUILD.md b/docs/TYPEPHP_WASI_BUILD.md index 518d01c7..4a4dc7bd 100644 --- a/docs/TYPEPHP_WASI_BUILD.md +++ b/docs/TYPEPHP_WASI_BUILD.md @@ -6,19 +6,18 @@ TypePHP 使用稳定的 WASI 0.2(Preview 2)和 Component Model。TypePHP 生 - WASI SDK 33 或更高版本(LLVM/Clang/LLD 22 或更高) - PHP 8.4 或更高版本,用于运行 TypePHP 编译器 -- Autoconf、Automake、Libtool、Bison、re2c 和常规 C/C++ 构建工具 - Wasmtime 47 或更高版本,用于运行和测试产物 - Jco 1 或更高版本,用于 browser profile;component profile 不需要 Jco +- 与当前 TypePHP 版本绑定的 `wasm32-wasip2` 集成 SDK -WASI SDK 的 `bin` 目录和 Wasmtime 必须加入系统 `PATH`。编译器不会探测或使用 `/opt` 等约定安装目录,也不接受专用的工具目录配置。可以继续通过环境变量覆盖缓存位置: +WASI SDK 的 `bin` 目录和 Wasmtime 必须加入系统 `PATH`。编译器不会探测或使用 `/opt` 等约定安装目录,也不接受专用的工具目录配置。WASI 静态库和头文件统一安装到 PHPX 的 `wasm/wasm32-wasip2/`: ```bash export PATH="::$PATH" -export PHP_WASM_BUILD_DIR=/tmp/php-8.5.9-wasip2-build -export TYPEPHP_WASM_DEPS_PREFIX=/tmp/typephp-wasip2-numeric/prefix -export TYPEPHP_WASM_RUNTIME_PREFIX=/tmp/typephp-wasip2-runtime ``` +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`。 ## 一条命令构建 @@ -66,7 +65,9 @@ wasm-browser-dir: generated 路径、sources 等详细配置继续放在 `project.yml`,不通过 `--wasm=` 传递。 -PHP、PHPX、TypePHP runtime、GMP、MPFR 和 mpdecimal 会预编译并缓存为 WASI 静态库;正常的应用构建只编译 TypePHP 为当前程序生成的 C++,然后链接这些 `.a`。源码开发环境首次执行时会自动建立缺失的运行时缓存,发行包可直接携带预编译静态库。 +PHP、PHPX、TypePHP runtime、GMP、MPFR 和 mpdecimal 由 SDK 发布阶段预编译为 WASI 静态库。应用构建只编译 TypePHP 为当前程序生成的 C++,然后链接这些 `.a`。`tpc --wasm` 不会下载源码、运行 `wit-bindgen`,也不会调用 PHP、PHPX 或高精度库的构建脚本。 + +PHP/WASI 当前静态内建 `date`、`pcre`、`hash`、`json`、`lexbor`、`random`、`Reflection`、`SPL`、`standard`、`uri`、`ctype`、`calendar`、`bcmath`、`filter` 和 `tokenizer` 扩展。 每个 C/C++ 翻译单元统一使用标准 Wasm C++ exceptions 和 WASI SJLJ;链接阶段将 ABI 警告视为错误,旧的 32 位 `zend_long` 缓存也会自动失效。 @@ -123,8 +124,25 @@ wasm32 使用 32 位指针,但 PHP 的 `zend_long` 保持 64 位,以维持 T PHPX Facade 只是为 PHP 可选扩展生成的便捷包装,并非 TypePHP ABI 的组成部分。WASI 下整体关闭它,可以避免把不存在的 curl、socket、Swoole、PDO 等 API 暴露为“可编译但链接失败”的接口。 -## 内部构建层次 +## WASI SDK 目录 + +集成 SDK 使用唯一、完整的前缀,位于 PHPX 根目录的 `wasm/wasm32-wasip2/`: + +```text +phpx/wasm/wasm32-wasip2/ +├── include/php/ # PHP 安装头文件 +├── include/phpx/ # PHPX 和 TypePHP runtime 头文件 +├── include/gmp.h ... +├── lib/libphp.a +├── lib/libphpx.a +├── lib/libgmp.a +├── lib/libgmpxx.a +├── lib/libmpfr.a +├── lib/libmpdec.a +├── lib/libmpdec++.a +└── .typephp-wasi-sdk-abi +``` -编译器内部会分别建立 PHP 8.5 NTS、GMP/MPFR/mpdecimal,以及 PHPX core/TypePHP runtime 的静态库缓存。内部构建脚本不是用户接口,不需要也不应由用户手动执行。发行包可直接附带目标平台对应的预编译 `.a` 文件。 +普通用户通过 TypePHP/PHPX 集成安装包获得该目录。TypePHP 开发者需要自行 clone 与当前版本绑定的 `php-8.5.9-wasm` 和 PHPX 源码,分别构建 PHP、PHPX、GMP、MPFR 与 mpdecimal,再按上述结构安装到 PHPX checkout。若 PHPX 不在 `vendor/swoole/phpx`,继续使用已有的 `PHPX_HOME` 指向该 checkout。 -用户始终通过 `php bin/tpc.php --wasm program.php` 构建最终程序,避免 PHP、PHPX 和生成代码使用不一致的 `zend_long`、异常或 SJLJ ABI。 +不提供单独覆盖 `libphp.a`、`libphpx.a` 或数值库的路径;所有库、头文件和 `.typephp-wasi-sdk-abi` 必须来自同一次兼容构建,避免混用不同的 `zend_long`、C++ exceptions、SJLJ 或 Component Model ABI。 diff --git a/examples/https.php b/examples/https.php new file mode 100644 index 00000000..b6b7de83 --- /dev/null +++ b/examples/https.php @@ -0,0 +1,6 @@ +

Filesystem

Memory FS + OPFS snapshot +
+

HTTP fetch

file_get_contents() + WASI HTTP +
@@ -106,7 +109,7 @@
TypePHP → C++17 → WASI 0.2 Component - 运行于独立 ES module Worker · 网络能力关闭 + 运行于独立 ES module Worker · HTTP 通过 JSPI 挂起等待
diff --git a/examples/wasm-hello/main.js b/examples/wasm-hello/main.js index 2eafdff3..108b9d15 100644 --- a/examples/wasm-hello/main.js +++ b/examples/wasm-hello/main.js @@ -3,7 +3,7 @@ const elements = Object.fromEntries([ 'args', 'env', 'stdin', 'persistent', 'run', 'reset', 'status', 'output', 'runtime-value', 'platform-value', 'clock-value', 'random-value', 'token-value', 'filesystem-value', 'files-value', 'argv-value', 'env-value', 'stdin-value', - 'bigint-value', 'decimal-value', 'bigfloat-value', + 'http-value', 'http-detail', 'bigint-value', 'decimal-value', 'bigfloat-value', ].map((id) => [id, document.getElementById(id)])); let worker = null; @@ -41,6 +41,8 @@ function renderReport(report) { value('token-value', report.random.token); value('filesystem-value', `第 ${report.filesystem.run} 次运行`); value('files-value', report.filesystem.files.join(' · ')); + value('http-value', report.http.ok ? `${report.http.bytes} bytes` : '请求失败'); + value('http-detail', report.http.preview); value('argv-value', report.input.argv.join(' ')); value('env-value', report.input.greeting); value('stdin-value', report.input.stdin); diff --git a/examples/wasm-hello/public/fetch-demo.json b/examples/wasm-hello/public/fetch-demo.json new file mode 100644 index 00000000..5c97e4fb --- /dev/null +++ b/examples/wasm-hello/public/fetch-demo.json @@ -0,0 +1,4 @@ +{ + "message": "Hello from browser fetch via PHP file_get_contents()", + "runtime": "WASI HTTP 0.2 + JSPI" +} diff --git a/examples/wasm-hello/src/WasiDemo.php b/examples/wasm-hello/src/WasiDemo.php index a2597ac5..f87af540 100644 --- a/examples/wasm-hello/src/WasiDemo.php +++ b/examples/wasm-hello/src/WasiDemo.php @@ -35,14 +35,35 @@ final class WasiDemo 'stdin' => trim($stdin), ], 'filesystem' => self::filesystemReport(), + 'http' => self::httpReport(), 'precision' => self::precisionReport(), 'capabilities' => [ - 'supported' => ['arguments', 'environment', 'stdin/stdout/stderr', 'clock', 'random', 'filesystem'], - 'disabled' => ['network', 'process', 'signals', 'shell', 'Fiber', 'Generator'], + 'supported' => ['arguments', 'environment', 'stdin/stdout/stderr', 'clock', 'random', 'filesystem', 'HTTP GET'], + 'disabled' => ['raw sockets', 'process', 'signals', 'shell', 'Fiber', 'Generator'], ], ]; } + private static function httpReport(): array + { + $url = getenv('TYPEPHP_FETCH_URL'); + if ($url === false || $url === '') { + return ['ok' => false, 'url' => '', 'bytes' => 0, 'preview' => 'No URL configured']; + } + + $body = file_get_contents($url); + if ($body === false) { + return ['ok' => false, 'url' => $url, 'bytes' => 0, 'preview' => 'Request failed']; + } + + return [ + 'ok' => true, + 'url' => $url, + 'bytes' => strlen($body), + 'preview' => trim(substr($body, 0, 80)), + ]; + } + private static function filesystemReport(): array { $directory = '/workspace'; diff --git a/examples/wasm-hello/style.css b/examples/wasm-hello/style.css index 62f78118..3903b197 100644 --- a/examples/wasm-hello/style.css +++ b/examples/wasm-hello/style.css @@ -20,14 +20,13 @@ h1 { margin: 0; font-size: clamp(4rem, 8vw, 7rem); font-weight: 760; letter-spac h1 span { color: transparent; background: linear-gradient(90deg, #77eaff, #a789ff 62%, #f7a95c); background-clip: text; } .lede { max-width: 620px; margin: 28px 0 0; color: #9fb1ca; font-size: 1.08rem; line-height: 1.8; } .runtime-orbit { position: relative; width: 280px; height: 280px; margin: auto; display: grid; place-items: center; } -.orbit-ring { position: absolute; inset: 18px; border: 1px solid #83e9ff40; border-radius: 50%; box-shadow: inset 0 0 60px #558cff0c; animation: spin 22s linear infinite; } +.orbit-ring { position: absolute; inset: 18px; border: 1px solid #83e9ff40; border-radius: 50%; box-shadow: inset 0 0 60px #558cff0c; } .orbit-ring::before, .orbit-ring::after { content: ""; position: absolute; inset: 34px; border: 1px dashed #a789ff38; border-radius: 50%; } .orbit-ring::after { inset: -15px; border-style: solid; border-color: #ffffff0d; } .runtime-core { z-index: 1; display: grid; place-items: center; width: 120px; height: 120px; border: 1px solid #79e8ff55; border-radius: 32px; background: linear-gradient(145deg, #163552, #0b1b2c); color: #87ecff; text-align: center; box-shadow: 0 26px 80px #0008, inset 0 1px #ffffff18; transform: rotate(-5deg); } .runtime-core strong { color: #fff; font-size: 2rem; } .satellite { position: absolute; padding: 7px 11px; border: 1px solid #ffffff18; border-radius: 99px; background: #0c1929dd; color: #aebed3; font: 700 .66rem ui-monospace, monospace; } .satellite.one { top: 20px; left: 31px; }.satellite.two { right: 3px; top: 108px; }.satellite.three { bottom: 22px; left: 16px; } -@keyframes spin { to { transform: rotate(360deg); } } .panel { border: 1px solid #ffffff12; border-radius: 24px; background: linear-gradient(145deg, #0d1c2dcc, #091522d9); box-shadow: 0 25px 70px #00000035, inset 0 1px #ffffff09; backdrop-filter: blur(18px); } .control-panel { padding: 30px; } .panel-heading, .actions { display: flex; align-items: center; justify-content: space-between; gap: 24px; } diff --git a/examples/wasm-hello/typephp-worker.mjs b/examples/wasm-hello/typephp-worker.mjs index c263f3a0..e006a18c 100644 --- a/examples/wasm-hello/typephp-worker.mjs +++ b/examples/wasm-hello/typephp-worker.mjs @@ -96,6 +96,10 @@ self.onmessage = async ({ data }) => { const persistent = data.persistent === true; const storageName = String(data.storageName || 'typephp-wasi-filesystem.json'); try { + if (typeof WebAssembly.Suspending !== 'function' + || typeof WebAssembly.promising !== 'function') { + throw new Error('This browser does not support WebAssembly JSPI, which is required for blocking WASI I/O'); + } fileData = persistent ? await loadFileData(storageName) : { dir: {} }; _setFileData(fileData); _setStdin(inputHandler(String(data.stdin || ''))); @@ -103,12 +107,13 @@ self.onmessage = async ({ data }) => { _setStderr(outputHandler('stderr')); const args = ['typephp.wasm', ...(Array.isArray(data.args) ? data.args.map(String) : [])]; - const env = data.env && typeof data.env === 'object' ? data.env : {}; + const env = data.env && typeof data.env === 'object' ? { ...data.env } : {}; + env.TYPEPHP_FETCH_URL ??= new URL('/fetch-demo.json', self.location.href).href; const wasi = new WASIShim({ sandbox: { args, env, - enableNetwork: false, + enableNetwork: true, }, }); const { instantiate } = await import('./generated/program.js'); @@ -131,5 +136,6 @@ self.onmessage = async ({ data }) => { } } self.postMessage({ type: 'exit', code: exitCode }); + self.close(); } }; diff --git a/package.php b/package.php index 40aaa165..3d95d446 100755 --- a/package.php +++ b/package.php @@ -595,13 +595,34 @@ function packageUnixLike(): void $binary = 'tpc'; $versionFile = 'version.txt'; + $phpxSourceDir = getenv('PHPX_HOME'); + if (!is_string($phpxSourceDir) || $phpxSourceDir === '' || !is_dir($phpxSourceDir)) { + $phpxSourceDir = 'vendor/swoole/phpx'; + } + $phpxSourceDir = realpath($phpxSourceDir) ?: rtrim($phpxSourceDir, '/\\'); + $wasiSdkSourceRoot = $phpxSourceDir . '/wasm/wasm32-wasip2'; + $wasiSdkPackageRoot = 'vendor/swoole/phpx/wasm/wasm32-wasip2'; $requiredFiles = [ $binary, 'composer.json', 'README.md', 'LICENSE.md', 'examples/hello.php', + 'wasm/build-typephp-program.sh', + "{$wasiSdkSourceRoot}/.typephp-wasi-sdk-abi", + "{$wasiSdkSourceRoot}/include/php/main/php.h", + "{$wasiSdkSourceRoot}/include/php/main/php_config.h", + "{$wasiSdkSourceRoot}/include/php/Zend/zend_config.h", + "{$wasiSdkSourceRoot}/include/php/ext/date/lib/timelib_config.h", + "{$wasiSdkSourceRoot}/include/phpx/phpx.h", + "{$wasiSdkSourceRoot}/include/phpx/typephp_helper.h", + "{$wasiSdkSourceRoot}/include/gmp.h", + "{$wasiSdkSourceRoot}/include/mpfr.h", + "{$wasiSdkSourceRoot}/include/decimal.hh", ]; + foreach (['libphp.a', 'libphpx.a', 'libgmp.a', 'libgmpxx.a', 'libmpfr.a', 'libmpdec.a', 'libmpdec++.a'] as $library) { + $requiredFiles[] = "{$wasiSdkSourceRoot}/lib/{$library}"; + } foreach ($requiredFiles as $file) { if (!is_file($file)) { throw new RuntimeException("Required package file not found: {$file}"); @@ -682,6 +703,8 @@ function packageUnixLike(): void mustCopy('LICENSE.md', "{$topLevelDir}/LICENSE.md"); mustCreateDirectory("{$topLevelDir}/examples"); mustCopy('examples/hello.php', "{$topLevelDir}/examples/hello.php"); + copyDirectory('wasm', "{$topLevelDir}/wasm"); + copyDirectory("{$phpxSourceDir}/wasm", "{$topLevelDir}/vendor/swoole/phpx/wasm"); if (is_file($outputFile) && !unlink($outputFile)) { throw new RuntimeException("Unable to remove existing archive: {$outputFile}"); @@ -717,7 +740,14 @@ function packageUnixLike(): void "{$topLevelDir}/README.md", "{$topLevelDir}/LICENSE.md", "{$topLevelDir}/examples/hello.php", + "{$topLevelDir}/wasm/build-typephp-program.sh", + "{$topLevelDir}/{$wasiSdkPackageRoot}/.typephp-wasi-sdk-abi", + "{$topLevelDir}/{$wasiSdkPackageRoot}/include/php/main/php.h", + "{$topLevelDir}/{$wasiSdkPackageRoot}/include/phpx/phpx.h", ]; + foreach (['libphp.a', 'libphpx.a', 'libgmp.a', 'libgmpxx.a', 'libmpfr.a', 'libmpdec.a', 'libmpdec++.a'] as $library) { + $requiredEntries[] = "{$topLevelDir}/{$wasiSdkPackageRoot}/lib/{$library}"; + } $verificationZip = new ZipArchive(); if ($verificationZip->open($outputFile) !== true) { throw new RuntimeException("Unable to verify archive: {$outputFile}"); diff --git a/phpunit/src/Build/PhpxLocatorTest.php b/phpunit/src/Build/PhpxLocatorTest.php new file mode 100644 index 00000000..cf904a55 --- /dev/null +++ b/phpunit/src/Build/PhpxLocatorTest.php @@ -0,0 +1,47 @@ +originalPhpxHome = getenv('PHPX_HOME'); + $this->phpxHome = sys_get_temp_dir() . '/typephp-phpx-locator-' . bin2hex(random_bytes(6)); + mkdir($this->phpxHome, 0777, true); + } + + protected function tearDown(): void + { + if ($this->originalPhpxHome === false) { + putenv('PHPX_HOME'); + } else { + putenv('PHPX_HOME=' . $this->originalPhpxHome); + } + rmdir($this->phpxHome); + } + + public function testPhpxHomeHasPriorityAndReturnsAnAbsolutePath(): void + { + putenv('PHPX_HOME=' . $this->phpxHome); + + self::assertSame(realpath($this->phpxHome), PhpxLocator::resolve('/not-used')); + } + + public function testInvalidPhpxHomeFallsBackToComposerInstallation(): void + { + putenv('PHPX_HOME=' . $this->phpxHome . '/missing'); + $projectRoot = dirname(__DIR__, 3); + + self::assertSame( + realpath($projectRoot . '/vendor/swoole/phpx'), + PhpxLocator::resolve($projectRoot), + ); + } +} diff --git a/src/Build/PhpxLocator.php b/src/Build/PhpxLocator.php new file mode 100644 index 00000000..2c9c3d86 --- /dev/null +++ b/src/Build/PhpxLocator.php @@ -0,0 +1,48 @@ +normalizePhpxDir($phpxDir); - } - } - - // Composer-installed TypePHP and PHPX are sibling packages. Ask - // Composer for the actual package path instead of assuming a nested - // vendor directory inside the TypePHP package. - if (class_exists(\Composer\InstalledVersions::class) - && \Composer\InstalledVersions::isInstalled('swoole/phpx')) { - $composerInstallPath = \Composer\InstalledVersions::getInstallPath('swoole/phpx'); - if (is_string($composerInstallPath)) { - $composerInstallPath = rtrim($composerInstallPath, '\/'); - if (is_dir($composerInstallPath)) { - return $this->normalizePhpxDir($composerInstallPath); - } - } - } - - // TypePHP source checkout: phpx is installed below this repository. - $composerPhpxDir = $this->rootPath . self::PHPX_VENDOR_DIR; - if (is_dir($composerPhpxDir)) { - return $this->normalizePhpxDir($composerPhpxDir); - } - - if (defined('ROOT_PATH')) { - $rootPhpxDir = ROOT_PATH . self::PHPX_VENDOR_DIR; - if (is_dir($rootPhpxDir)) { - return $this->normalizePhpxDir($rootPhpxDir); - } + try { + return PhpxLocator::resolve($this->rootPath); + } catch (\RuntimeException $exception) { + $this->error($exception->getMessage()); } - - // 两个路径都不存在,报错 - $this->error( - 'phpx directory not found. Please either:\n' . - '1. Set PHPX_HOME environment variable to your phpx installation path\n' . - '2. Install phpx via Composer: composer require swoole/phpx' - ); - } - - /** - * 规范化 phpx 目录路径(解析相对路径与 ..,便于错误提示和后续路径拼接) - */ - private function normalizePhpxDir(string $dir): string - { - $real = realpath($dir); - return $real !== false ? $real : $dir; } protected function getPlatform(): PlatformBase diff --git a/src/compiler.php b/src/compiler.php index dd404bd6..947a57d9 100644 --- a/src/compiler.php +++ b/src/compiler.php @@ -2,6 +2,7 @@ use TypePhp\Translator; use TypePhp\Build\WasiToolchain; use TypePhp\Build\WasiProjectConfig; +use TypePhp\Build\PhpxLocator; function main(int $argc, array $argv): void { @@ -137,7 +138,7 @@ function compileWasmProgram(array $argv): void exit(1); } - $builder = dirname(__DIR__) . '/projects/php-8.5.9/wasm/build-typephp-program.sh'; + $builder = dirname(__DIR__) . '/wasm/build-typephp-program.sh'; if (!is_executable($builder)) { fwrite(STDERR, "TypePHP WASI builder is not executable: {$builder}\n"); exit(1); @@ -176,14 +177,22 @@ function compileWasmProgram(array $argv): void $environment['TYPEPHP_WASI_CLANG_VERSION'] = $tools['clang-version']; $environment['TYPEPHP_WASMTIME_VERSION'] = $tools['wasmtime-version']; $environment['TYPEPHP_WASM_PROGRAM_BUILD_DIR'] = $project->buildDir; + $compilerExecutable = realpath($argv[0]); + if ($compilerExecutable === false || !is_executable($compilerExecutable)) { + fwrite(STDERR, "Unable to resolve the current TypePHP compiler executable: {$argv[0]}\n"); + exit(1); + } if ($project->browserDir !== null) { $environment['TYPEPHP_WASM_BROWSER_DIR'] = $project->browserDir; } - $command = [$builder, $project->input]; - if ($project->output !== null) { - $command[] = $project->output; + try { + $phpxDir = PhpxLocator::resolve(ROOT_PATH); + } catch (RuntimeException $exception) { + fwrite(STDERR, "Unable to locate PHPX: {$exception->getMessage()}\n"); + exit(1); } + $command = [$builder, $project->input, $project->output ?? '-', $phpxDir, $compilerExecutable]; $process = proc_open( $command, diff --git a/wasm/README.md b/wasm/README.md new file mode 100644 index 00000000..0a5c12d3 --- /dev/null +++ b/wasm/README.md @@ -0,0 +1,43 @@ +# TypePHP WASI SDK layout + +TypePHP application builds never compile PHP, PHPX, GMP, MPFR, or mpdecimal. +The integrated PHPX installer places their prebuilt `wasm32-wasip2` SDK at: + +```text +/wasm/wasm32-wasip2/ +├── include/ +│ ├── php/ PHP installed and generated headers +│ ├── phpx/ PHPX public and TypePHP runtime headers +│ ├── gmp.h +│ ├── gmpxx.h +│ ├── mpfr.h +│ ├── mpdecimal.h +│ └── decimal.hh +├── lib/ +│ ├── libphp.a +│ ├── libphpx.a +│ ├── libgmp.a +│ ├── libgmpxx.a +│ ├── libmpfr.a +│ ├── libmpdec.a +│ └── libmpdec++.a +└── .typephp-wasi-sdk-abi +``` + +The ABI file must contain exactly: + +```text +typephp-wasip2-sdk-abi-v2 +``` + +TypePHP locates PHPX through the existing `PHPX_HOME` setting, Composer's +`swoole/phpx` installation metadata, or `vendor/swoole/phpx`. TypePHP developers +who independently clone and build the matching `php-8.5.9-wasm` and PHPX +repositories install the complete SDK below that PHPX checkout. There is no +additional WASI SDK environment variable and no set of per-library search +paths: all headers, archives, and the ABI marker must be installed together so +an application cannot accidentally mix incompatible builds. + +`wit-bindgen`, Autoconf, Bison, re2c, and the PHP/PHPX source trees are SDK +producer dependencies only. They are never searched for or invoked by +`tpc --wasm`. diff --git a/wasm/build-typephp-program.sh b/wasm/build-typephp-program.sh new file mode 100755 index 00000000..bcbda9e0 --- /dev/null +++ b/wasm/build-typephp-program.sh @@ -0,0 +1,204 @@ +#!/usr/bin/env bash + +set -euo pipefail + +fatal_error() { + local red='' + local reset='' + if [[ -t 2 && -z "${NO_COLOR:-}" && "${TERM:-}" != dumb ]]; then + red=$'\033[1;31m' + reset=$'\033[0m' + fi + printf '%sFatal error: %s%s\n' "${red}" "$1" "${reset}" >&2 + shift + for line in "$@"; do + printf '%s %s%s\n' "${red}" "${line}" "${reset}" >&2 + done + exit 1 +} + +if [[ $# -ne 4 ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +caller_dir=${PWD} +script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +compiler_dir=$(cd "${script_dir}/.." && pwd) +phpx_dir=$3 +typephp_compiler=$4 +wasi_sdk_dir=${phpx_dir}/wasm/wasm32-wasip2 +wasi_include_dir=${wasi_sdk_dir}/include +wasi_php_include_dir=${wasi_include_dir}/php +wasi_phpx_include_dir=${wasi_include_dir}/phpx +wasi_library_dir=${wasi_sdk_dir}/lib +wasi_cxx=${TYPEPHP_WASI_CXX:?TYPEPHP_WASI_CXX is required} + +input=$1 +if [[ "${input}" != /* ]]; then + input=${caller_dir}/${input} +fi +input=$(realpath "${input}") + +stem=$(basename "${input}" .php) +stem=${stem//[^a-zA-Z0-9_-]/_} +build_root=${TYPEPHP_WASM_PROGRAM_BUILD_DIR:-${caller_dir}/build} +mkdir -p "${build_root}" +build_root=$(cd "${build_root}" && pwd) +generated_dir=${build_root} +generated_source_list=${build_root}/.typephp-wasm-sources +cleanup_generated_source_list() { + rm -f -- "${generated_source_list}" +} +trap cleanup_generated_source_list EXIT + +if [[ $2 != - ]]; then + output=$2 + if [[ "${output}" != /* ]]; then + output=${caller_dir}/${output} + fi +else + output=${caller_dir}/${stem}.wasm +fi + +mkdir -p "${generated_dir}" "$(dirname "${output}")" + +# Convert first so target-specific source errors are reported before validating +# and linking the separately installed WASI SDK. +TYPEPHP_WASM_INTERNAL_COMPILE=1 TYPEPHP_GENERATED_SOURCE_LIST="${generated_source_list}" "${typephp_compiler}" "${input}" \ + --dry \ + --target-platform wasm32-wasip2 \ + --build-dir "${generated_dir}" \ + --no-progress \ + --no-color + +if [[ ! -s "${generated_source_list}" ]]; then + echo "TypePHP did not write the generated C++ source manifest: ${generated_source_list}" >&2 + exit 1 +fi +mapfile -t generated_sources < "${generated_source_list}" +if [[ ${#generated_sources[@]} -eq 0 ]]; then + echo "TypePHP did not generate any C++ source files" >&2 + exit 1 +fi + +wasi_sdk_stamp=${wasi_sdk_dir}/.typephp-wasi-sdk-abi +if [[ ! -f "${wasi_sdk_stamp}" ]] \ + || ! grep -qx 'typephp-wasip2-sdk-abi-v2' "${wasi_sdk_stamp}"; then + fatal_error \ + "TypePHP WASI SDK is missing or ABI-incompatible: ${wasi_sdk_dir}" \ + "Install the matching PHPX package or set PHPX_HOME to its installation directory." +fi + +required_libraries=(libphp.a libphpx.a libgmp.a libgmpxx.a libmpfr.a libmpdec.a libmpdec++.a) +for library in "${required_libraries[@]}"; do + if [[ ! -f "${wasi_library_dir}/${library}" ]]; then + fatal_error "TypePHP WASI SDK library is missing: ${wasi_library_dir}/${library}" + fi +done +required_headers=( + php/main/php.h + php/main/php_config.h + php/Zend/zend_config.h + php/ext/date/lib/timelib_config.h + phpx/phpx.h + phpx/typephp_helper.h + gmp.h + mpfr.h + decimal.hh +) +for header in "${required_headers[@]}"; do + if [[ ! -f "${wasi_include_dir}/${header}" ]]; then + fatal_error "TypePHP WASI SDK header is missing: ${wasi_include_dir}/${header}" + fi +done + +compile_flags=( + -std=c++17 + -O2 + -fwasm-exceptions + -mllvm -wasm-enable-sjlj + -mllvm -wasm-use-legacy-eh=false + -Wno-deprecated-literal-operator +) +include_flags=( + -I"${wasi_php_include_dir}" + -I"${wasi_php_include_dir}/main" + -I"${wasi_php_include_dir}/Zend" + -I"${wasi_php_include_dir}/TSRM" + -I"${wasi_php_include_dir}/ext/date/lib" + -I"${wasi_phpx_include_dir}" + -I"${wasi_include_dir}" + -I"${generated_dir}/include" +) + +generated_objects=() +for source in "${generated_sources[@]}"; do + if [[ ! -f "${source}" ]]; then + echo "Generated C++ source file not found: ${source}" >&2 + exit 1 + fi + object=${source%.cc}.o + "${wasi_cxx}" "${compile_flags[@]}" "${include_flags[@]}" -c "${source}" -o "${object}" + generated_objects+=("${object}") +done + +# Every generated object and runtime archive is already built with -O2. Keep +# the final driver invocation optimized as well, but do not let Clang discover +# an arbitrary system wasm-opt: older Binaryen releases cannot parse the Wasm +# exception-reference instructions emitted by the current WASI SDK. Stripping +# linker metadata has a much larger browser startup benefit than another slow +# whole-module optimization pass and does not change runtime semantics. +"${wasi_cxx}" \ + -O2 \ + --no-wasm-opt \ + -std=c++17 \ + -fwasm-exceptions \ + "${generated_objects[@]}" \ + -Wl,--whole-archive \ + "${wasi_library_dir}/libphpx.a" \ + -Wl,--no-whole-archive \ + "${wasi_library_dir}/libphp.a" \ + "${wasi_library_dir}/libmpdec++.a" \ + "${wasi_library_dir}/libmpdec.a" \ + "${wasi_library_dir}/libmpfr.a" \ + "${wasi_library_dir}/libgmpxx.a" \ + "${wasi_library_dir}/libgmp.a" \ + -lwasi-emulated-signal -lsetjmp -lunwind -ldl -lm \ + -Wl,--strip-all \ + -Wl,--fatal-warnings \ + -o "${output}" + +echo "Built TypePHP/WASI program: ${output}" + +if [[ "${TYPEPHP_WASM_BROWSER:-1}" == 1 ]]; then + # Chrome does not yet load components natively, so Jco lowers the same + # WASI 0.2 component to core Wasm + ESM. + jco_bin=${TYPEPHP_JCO:-jco} + browser_dir=${TYPEPHP_WASM_BROWSER_DIR:-${output%.wasm}.browser} + mkdir -p "${browser_dir}" + jco_flags=() + if "${jco_bin}" transpile --help 2>&1 | grep -q -- '--bindgen-enable-wasm-exnref'; then + jco_flags+=(--bindgen-enable-wasm-exnref) + fi + if ! "${jco_bin}" transpile --help 2>&1 | grep -q -- '--async-wasi-imports'; then + echo "Jco does not support JSPI-backed asynchronous WASI imports; upgrade Jco" >&2 + exit 1 + fi + jco_flags+=(--async-mode jspi --async-wasi-imports --async-wasi-exports) + "${jco_bin}" transpile "${output}" \ + -o "${browser_dir}" \ + --name program \ + --no-nodejs-compat \ + --no-namespaced-exports \ + --instantiation async \ + --base64-cutoff=0 \ + "${jco_flags[@]}" + echo "Built TypePHP/WASI browser module: ${browser_dir}" +fi + +if [[ "${TYPEPHP_WASM_RUN:-0}" == 1 ]]; then + wasmtime_bin=${TYPEPHP_WASMTIME:-wasmtime} + XDG_CACHE_HOME=${XDG_CACHE_HOME:-/tmp/typephp-wasmtime-cache} \ + "${wasmtime_bin}" "${output}" +fi