- Add build-typephp-program.sh script for compiling PHP to WASI - Integrate HTTP fetch capability via file_get_contents() using WASI HTTP - Move WASI builder script from projects/php-8.5.9/wasm to wasm directory - Add PhpxLocator class to resolve PHPX directory with proper fallback logic - Update compiler to pass PHPX directory and compiler executable to builder - Add WASI SDK layoutpull/46/head
parent
07c7d4b84a
commit
9e26887c03
16 changed files with 476 additions and 74 deletions
@ -0,0 +1,6 @@ |
|||||||
|
<?php |
||||||
|
function main() |
||||||
|
{ |
||||||
|
$homepage = file_get_contents('https://www.example.com/'); |
||||||
|
echo $homepage; |
||||||
|
} |
||||||
@ -0,0 +1,4 @@ |
|||||||
|
{ |
||||||
|
"message": "Hello from browser fetch via PHP file_get_contents()", |
||||||
|
"runtime": "WASI HTTP 0.2 + JSPI" |
||||||
|
} |
||||||
@ -0,0 +1,47 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace TypePhpTest\Build; |
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase; |
||||||
|
use TypePhp\Build\PhpxLocator; |
||||||
|
|
||||||
|
final class PhpxLocatorTest extends TestCase |
||||||
|
{ |
||||||
|
private string|false $originalPhpxHome; |
||||||
|
private string $phpxHome; |
||||||
|
|
||||||
|
protected function setUp(): void |
||||||
|
{ |
||||||
|
$this->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), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,48 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace TypePhp\Build; |
||||||
|
|
||||||
|
use Composer\InstalledVersions; |
||||||
|
use RuntimeException; |
||||||
|
|
||||||
|
final class PhpxLocator |
||||||
|
{ |
||||||
|
public static function resolve(string $rootPath): string |
||||||
|
{ |
||||||
|
$phpxHome = getenv('PHPX_HOME'); |
||||||
|
if (is_string($phpxHome) && $phpxHome !== '') { |
||||||
|
$resolved = self::existingDirectory($phpxHome); |
||||||
|
if ($resolved !== null) { |
||||||
|
return $resolved; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (class_exists(InstalledVersions::class) && InstalledVersions::isInstalled('swoole/phpx')) { |
||||||
|
$installPath = InstalledVersions::getInstallPath('swoole/phpx'); |
||||||
|
if (is_string($installPath)) { |
||||||
|
$resolved = self::existingDirectory($installPath); |
||||||
|
if ($resolved !== null) { |
||||||
|
return $resolved; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
$resolved = self::existingDirectory(rtrim($rootPath, '/\\') . '/vendor/swoole/phpx'); |
||||||
|
if ($resolved !== null) { |
||||||
|
return $resolved; |
||||||
|
} |
||||||
|
|
||||||
|
throw new RuntimeException( |
||||||
|
"phpx directory not found. Set PHPX_HOME or install swoole/phpx with Composer.", |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
private static function existingDirectory(string $path): ?string |
||||||
|
{ |
||||||
|
$path = rtrim($path, '/\\'); |
||||||
|
if (!is_dir($path)) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
return realpath($path) ?: $path; |
||||||
|
} |
||||||
|
} |
||||||
@ -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 |
||||||
|
<phpx>/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`. |
||||||
@ -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 <program.php> <output.wasm|-> <phpx-dir> <tpc-executable>" >&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 |
||||||
Loading…
Reference in new issue