php-wasm-builder
v0.2.0
Published
Builds php-wasm, php-cgi-wasm & dependencies.
Readme
php-wasm
PHP in WebAssembly, npm not required.
npm | github | unpkg | reddit | discord
📦 Current Packages
php-wasm,php-cgi-wasm,php-cli-wasm,php-dbg-wasm,php-sdl-wasm,php-cloud-wasm, andphp-wasm-builderare published separately.- Published runtimes currently cover PHP
8.0through8.5, depending on the package and entrypoint. - Runtimes default to PHP
8.4, exceptPhpCliWeb, which defaults to8.3.PhpNode,PhpCliNode, andPhpDbgNodeuse thePHP_VERSIONenvironment variable instead when it names a supported version. Passversionexplicitly when your asset filenames need to line up. - Runtime-loadable libraries are available for
gd,iconv,intl,libxml,xml,dom,simplexml,xmlreader,xmlwriter,yaml,zip,mbstring,openssl,phar,sqlite,tidy, andzlib. - Vrzno, pdo_cfd1, and pdo_pglite are maintained as separate packages.
php-cloud-wasmis a dedicated static ES-module build for Cloudflare Workers and Pages, tested with local workerd for PHP8.0through8.5.- The standalone
php-sdl-wasmbrowser runtime supports SDL graphics, input, image loading, TrueType text, audio, and OpenGL shaders, with a textured cube example. See the SDL guide for canvas setup, build flags, and supported APIs.
Install the packages you need:
$ npm i php-wasm
$ npm i php-cgi-wasm
$ npm i php-cli-wasm
$ npm i php-dbg-wasm
$ npm i php-sdl-wasm
$ npm i php-cloud-wasm
$ npm i php-wasm-builder☀️ Examples
[React + php-web/php-cgi-worker demo]
🎩 Introducing php-cgi-wasm!
php-cgi-wasm runs PHP in web-server mode, similar to Apache or nginx. Running within a Service Worker, it can intercept and respond to HTTP requests just like a normal web server. This means the browser can simply navigate to a URL and let PHP generate the page, with AJAX and other in-page requests still flowing over normal HTTP.
Install the php-cgi-wasm package
$ npm install php-cgi-wasmExample Service Worker:
import { PhpCgiWorker } from "php-cgi-wasm/PhpCgiWorker";
// Spawn the PHP-CGI binary
const php = new PhpCgiWorker({
prefix: '/php-wasm',
docroot: '/persist/www',
types: {
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
gif: 'image/gif',
png: 'image/png',
svg: 'image/svg+xml',
}
});
// Set up the event handlers
self.addEventListener('install', event => php.handleInstallEvent(event));
self.addEventListener('activate', event => php.handleActivateEvent(event));
self.addEventListener('fetch', event => php.handleFetchEvent(event));
self.addEventListener('message', event => php.handleMessageEvent(event));You can see examples of php-cgi-wasm running in a service worker and Node.js in demo-web/src/workers/cgi-worker.mjs and demo-node/index.mjs respectively.
Note: php-cgi-wasm and php-wasm are separate packages. One embeds PHP directly into your JavaScript runtime; the other runs in CGI mode, like PHP under Apache or nginx.
You can find documentation specific to php-cgi-wasm here.
🛠️ Install & Use
Install php-wasm with npm:
$ npm install php-wasmInclude the module:
ESM
import { PhpWeb } from 'php-wasm/PhpWeb.mjs';
const php = new PhpWeb;From a CDN:
Note: This does not require npm.
jsdelivr
const { PhpWeb } = await import('https://cdn.jsdelivr.net/npm/php-wasm/PhpWeb.mjs');
const php = new PhpWeb;unpkg
const { PhpWeb } = await import('https://unpkg.com/php-wasm/PhpWeb.mjs');
const php = new PhpWeb;Pre-Packaged Static Assets:
Each runtime module loads its WebAssembly binary with
new URL('<hash>.wasm', import.meta.url). Bundlers that understand this
pattern, such as Vite and webpack 5, emit the binary automatically.
Otherwise, copy the binary referenced by each runtime you import next to the bundled module. It is named by its SHA-1 hash, which changes with every build:
grep -o '[0-9a-f]\{40\}\.wasm' node_modules/php-wasm/php8.4-web.mjs | sort -u
grep -o '[0-9a-f]\{40\}\.wasm' node_modules/php-cgi-wasm/php8.4-cgi-worker.mjs | sort -uRepeat this after upgrading, and for each PHP version and runtime you import.
Core Node runtimes support both ESM and CommonJS.
For 0.2.0, use the published entrypoints across the runtime packages:
php-wasm/PhpNodephp-cgi-wasm/PhpCgiNodephp-cli-wasm/PhpCliNodephp-dbg-wasm/PhpDbgNode
Browser/runtime helper entrypoints such as PhpWeb and php-tags remain ESM-first.
🍎 Quickstart
Inline PHP
Include the php-tags module from a CDN:
<script async type = "module" src = "https://cdn.jsdelivr.net/npm/php-wasm/php-tags.mjs"></script>To serve the installed package locally, expose its directory through your HTTP
server and use its public URL, for example /node_modules/php-wasm/php-tags.mjs.
Keep the package's relative module paths and matching JavaScript/Wasm assets
together. A relative URL such as node_modules/... resolves against the page's
directory, so nested pages may need a leading / or a different public path.
The loader waits for the initial document to finish parsing, including when
an async module in <head> loads before <body> exists.
And run some PHP right in the page!
<script type = "text/php" data-stdout = "#output">
<?php phpinfo();
</script>
<div id = "output"></div>Inline PHP can use standard input, output, and error with data- attributes. Set each attribute value to a selector that matches the corresponding element.
<script async type = "module" src = "https://cdn.jsdelivr.net/npm/php-wasm/php-tags.mjs"></script>
<script id = "input" type = "text/plain">Hello, world!</script>
<script type = "text/php" data-stdin = "#input" data-stdout = "#output" data-stderr = "#error">
<?php echo file_get_contents('php://stdin');
</script>
<div id = "output"></div>
<div id = "error"></div>The src attribute can be used on <script type = "text/php"> tags, as well as their input elements. For example:
<html>
<head>
<script async type = "module" src = "https://cdn.jsdelivr.net/npm/php-wasm/php-tags.mjs"></script>
<script id = "input" src = "/test-input.json" type = "text/json"></script>
<script type = "text/php" src = "/test.php" data-stdin = "#input" data-stdout = "#output" data-stderr = "#error"></script>
</head>
<body>
<div id = "output"></div>
<div id = "error"></div>
</body>
</html>CDNs
JSDelivr
<script async type = "module" src = "https://cdn.jsdelivr.net/npm/php-wasm/php-tags.mjs"></script>Unpkg
<script async type = "module" src = "https://unpkg.com/php-wasm/php-tags.mjs"></script>🥤 Running PHP & Taking Output
Create a PHP instance:
const { PhpWeb } = await import('https://cdn.jsdelivr.net/npm/php-wasm/PhpWeb.mjs');
const php = new PhpWeb;Add your output listeners:
// Listen to STDOUT
php.addEventListener('output', (event) => {
console.log(event.detail);
});
// Listen to STDERR
php.addEventListener('error', (event) => {
console.log(event.detail);
});Provide some input data on STDIN if you need to:
php.inputString('This is a string of data provided on STDIN.');... then run some PHP!
const exitCode = await php.run('<?php echo "Hello, world!";');run() accepts complete PHP source, including an initial
<?php declare(strict_types=1);, namespaces, and mixed PHP/HTML. A leading
PHP opening tag does not introduce output before the declaration, and line
numbers are preserved. Leading HTML, whitespace outside PHP, or a UTF-8 BOM
still count as output and cannot precede strict_types.
Dynamic Extensions in Static Pages
Dynamic extensions can be loaded in static webpages like so:
<script async type = "module" src = "https://cdn.jsdelivr.net/npm/php-wasm/php-tags.mjs"></script>
<script type = "text/php" data-stdout = "#output" data-stderr = "#error" data-libs = '[
{"url": "https://unpkg.com/php-wasm-yaml/php8.4-yaml.so", "ini": true},
{"url": "https://unpkg.com/php-wasm-yaml/libyaml.so", "ini": false}
]'><?php
print yaml_emit([1,2,3,"string",["k1" => "value", "k2" => "value2", "k3" => "value3"],"now" => date("Y-m-d h:i:s")]);
</script>The example above assumes the default php-tags runtime version (8.4). If you set data-version to something else, update the php8.x-*.so filenames to match.
⚙️ Configuration
You can pass in the ini property to the constructor to add lines to /php.ini:
const php = new PhpWeb({ini: `
date.timezone=${Intl.DateTimeFormat().resolvedOptions().timeZone}
tidy.clean_output=1
expose_php=0
`});The /config/php.ini and /preload/php.ini files will also be loaded, if they exist. Neither of these files will be created if they do not exist. They're left completely up to the programmer to create & populate.
Options like the following may appear in these files. See the PHP docs for the full list.
[php]
date.timezone=UTC
tidy.clean_output=1
expose_php=0CGI Configuration
When running in CGI mode, php will look for a php.ini file in the document root directory, and load it along with the files listed above.
Writing an INI for multiple PHP versions
PHP will replace strings in INI files in the form ${ENVIRONMENT_VARIABLE} with the env value of ENVIRONMENT_VARIABLE. The PHP_VERSION environment variable is available to allow loading of the extension compatible with the currently running version of PHP:
[php]
extension=php${PHP_VERSION}-phar.soRemember to correctly escape the $ if you're supplying the INI from JavaScript with backticks:
const php = new PhpWeb({ini: `
extension=php\${PHP_VERSION}-phar.so
date.timezone=${Intl.DateTimeFormat().resolvedOptions().timeZone}
`});🔌 Extensions
Loading extensions at runtime
The following extensions may be loaded at runtime. This allows the shared extensions and their dependencies to be cached, reused, and selected a la carte for each application.
- gd (https://www.npmjs.com/package/php-wasm-gd)
- iconv (https://www.npmjs.com/package/php-wasm-iconv)
- intl (https://www.npmjs.com/package/php-wasm-intl)
- libxml (https://www.npmjs.com/package/php-wasm-libxml)
- xml (https://www.npmjs.com/package/php-wasm-xml)
- dom (https://www.npmjs.com/package/php-wasm-dom)
- simplexml (https://www.npmjs.com/package/php-wasm-simplexml)
- xmlreader (https://www.npmjs.com/package/php-wasm-xmlreader)
- xmlwriter (https://www.npmjs.com/package/php-wasm-xmlwriter)
- yaml (https://www.npmjs.com/package/php-wasm-yaml)
- zip (https://www.npmjs.com/package/php-wasm-libzip)
- mbstring (https://www.npmjs.com/package/php-wasm-mbstring)
- openssl (https://www.npmjs.com/package/php-wasm-openssl)
- phar (https://www.npmjs.com/package/php-wasm-phar)
- sqlite (https://www.npmjs.com/package/php-wasm-sqlite)
- pdo-sqlite (https://www.npmjs.com/package/php-wasm-sqlite)
- tidy (https://www.npmjs.com/package/php-wasm-tidy)
- zlib (https://www.npmjs.com/package/php-wasm-zlib)
There are two ways to load extensions at runtime, using the dl() function or php.ini.
<?php
dl('php8.4-xml.so');
dl('php8.4-dom.so');Or pass a sharedLibs array to the constructor from JavaScript to auto-generate an INI file that loads your extensions:
const php = new PhpWeb({sharedLibs: [
`php8.4-xml.so`,
`php8.4-dom.so`,
]});Keep the runtime version and the shared-library filenames in sync when you target something other than the default build:
const version = '8.4';
const php = new PhpWeb({version, sharedLibs: [
`php${version}-xml.so`,
`php${version}-dom.so`,
]});Dynamic Extensions from Remote Servers:
You can also load extensions from remote servers with URLs:
const version = '8.4';
const php = new PhpWeb({version, sharedLibs: [`https://unpkg.com/php-wasm-phar/php${version}-phar.so`]});The above is actually shorthand for the following code. Passing ini: true will automatically load the extension via /php.ini, passing ini: false will wait for a call to dl() to do the lookup.
const php = new PhpWeb({sharedLibs: [
{
name: `php8.4-phar.so`,
url: `https://unpkg.com/php-wasm-phar/php8.4-phar.so`,
ini: true,
}
]});Strings starting with /, ./, http:// or https:// will be treated as URLs:
const php = new PhpWeb({sharedLibs: [
`./php8.4-phar.so`
]});Some extensions require supporting libraries. You can provide URLs for those as sharedLibs as well, just pass ini: false:
(name is implied to be the last section of the URL here.)
const php = new PhpWeb({sharedLibs: [
{ url: 'https://unpkg.com/php-wasm-sqlite/php8.4-sqlite.so', ini: true },
{ url: 'https://unpkg.com/php-wasm-sqlite/libsqlite3.so', ini: false },
]});Loading Dynamic Extensions as JS Modules
Dynamic extensions can be loaded as modules. The module's main file defines getLibs, and optionally getFiles for preload files. Extensions may be loaded like so:
new PhpNode({sharedLibs:[ await import('php-wasm-intl') ]})Dynamic extensions can also be loaded as modules from any static HTTP server with an ESM directory structure.
// This will load both libsqlite3.so and php8.4-sqlite.so:
const php = new PhpWeb({sharedLibs: [ await import('https://cdn.jsdelivr.net/npm/php-wasm-sqlite') ]});This notation is not available in Service Workers, which do not support dynamic import(). Import the extension module statically or pass its asset URLs instead.
The extension helper JS packages shown above are ESM-only. If you need to bypass those helper packages, pass the extension assets manually instead of importing the helper package:
import { PhpNode } from 'php-wasm/PhpNode';
const php = new PhpNode({
sharedLibs: [
{
name: 'php8.4-intl.so',
url: new URL('./vendor/php8.4-intl.so', import.meta.url).href,
ini: true,
},
{ name: 'libicuuc.so', url: new URL('./vendor/libicuuc.so', import.meta.url).href },
{ name: 'libicutu.so', url: new URL('./vendor/libicutu.so', import.meta.url).href },
{ name: 'libicutest.so', url: new URL('./vendor/libicutest.so', import.meta.url).href },
{ name: 'libicuio.so', url: new URL('./vendor/libicuio.so', import.meta.url).href },
{ name: 'libicui18n.so', url: new URL('./vendor/libicui18n.so', import.meta.url).href },
{ name: 'libicudata.so', url: new URL('./vendor/libicudata.so', import.meta.url).href },
],
files: [
{
name: 'icudt72l.dat',
parent: '/preload/',
url: new URL('./vendor/icudt72l.dat', import.meta.url).href,
},
],
});When you manage extension assets this way, build mode matters:
dynamic: provide the extension.soplus any support libraries and preload files it needsshared: provide only the extra support libraries and preload files the runtime still needsstatic: do not inject the extension assets again
Compiling extensions
Extensions may be compiled as dynamic, shared, or static. See Custom Builds for more information on compiling php-wasm.
- dynamic - these extensions may be loaded selectively at runtime.
- shared - these extensions will always be loaded at startup and can be cached and reused.
- static - these extensions will be built directly into the main wasm binary (may cause a huge filesize).
📦 Loading Files
Loading single files at runtime
When spawning a new instance of PHP, a files array can be provided to be loaded into the filesystem. For example, the php-intl extension requires us to load icudt72l.dat into the /preload directory.
const sharedLibs = [`https://unpkg.com/php-wasm-intl/php\${PHP_VERSION}-intl.so`];
const files = [
{
name: 'icudt72l.dat',
parent: '/preload/',
url: 'https://unpkg.com/php-wasm-intl/icudt72l.dat'
}
];
const php = new PhpWeb({sharedLibs, files});Preloaded FS
Use the PRELOAD_ASSETS key in your .php-wasm-rc file to define a list of files and directories to include by default.
The files and directories will be collected into a single directory. Individual files & directories will appear in the top level, while directories will maintain their internal structure.
When you use php-wasm-builder, relative entries are resolved from the current project directory. Anchored paths such as /path/to/file.txt and ~/path/to/file.txt are copied as-is.
These files & directories will be available under /preload in the final package, packaged into the .data file that is built along with the .wasm file.
PRELOAD_ASSETS='./php-scripts /some/directory ~/other-dir/example.php /path/to/other_file.txt'locateFile
You can provide the locateFile option to php-wasm as a callback to map the names of files to URLs where they're loaded from. undefined can be returned as a fallback to default.
You can use this if your static assets are served from a different directory than your JavaScript.
This applies to .wasm files, shared libraries, single files and preloaded FS packages in .data files.
const php = new PhpWeb({locateFile: filename => `/my/static/path/${filename}`});💾 Persistent Storage (IDBFS & NodeFS)
IDBFS (Web & Worker)
To use IDBFS in PhpWeb, pass a persist object with a mountPath key.
mountPath will be used as the path to the persistent directory within the PHP environment.
const { PhpWeb } = await import('https://cdn.jsdelivr.net/npm/php-wasm/PhpWeb.mjs');
const php = new PhpWeb({persist: {mountPath: '/persist'}});NodeFS (Node.js Only)
To use NodeFS in PhpNode, pass a persist object with mountPath and localPath keys.
localPath will be used as the path to the HOST directory to expose to PHP.
mountPath will be used as the path to the persistent directory within the PHP environment.
import path from 'node:path';
import { PhpNode } from 'php-wasm/PhpNode';
const php = new PhpNode({
persist: {
mountPath: '/persist',
localPath: path.join(process.cwd(), 'persist'),
}
});📁 Filesystem Operations
Filesystem Methods
The following EmscriptenFS methods are exposed via the php object:
Browser CGI filesystem calls refresh persisted storage automatically when
autoTransaction is enabled. Await the writer's persistence before reading from
another runtime. refresh() recreates the PHP runtime and discards temporary
files and in-memory PHP state; it is not required before each CGI filesystem read.
php.analyzePath
Get information about a file or directory.
await php.analyzePath(path);php.readdir
Get entry names as string[]:
await php.readdir(path);Pass {withFileTypes: true} to get serializable {name: string, isFolder: boolean}
entries in one queued operation:
const entries = await php.readdir(path, {withFileTypes: true});Both forms preserve filesystem order and include . and ... Types follow
symbolic links, as analyzePath does. Listing and metadata errors, including
dangling links, reject the operation. Omitted options or withFileTypes: false
keep the name-only result. The option is available in embedded, CLI, CGI,
debugger, and Cloudflare runtimes; the declaration overloads reflect each result.
In browser CGI, a typed listing uses one storage refresh for the directory and
all its entry types. This avoids a separate analyzePath request per entry.
It is also available over the service worker message bridge:
const entries = await sendMessage('readdir', [path, {withFileTypes: true}]);php.readFile
Get the content of a file as a Uint8Array by default, or optionally as utf-8.
await php.readFile(path);await php.readFile(path, {encoding: 'utf8'});php.stat
Get information about a file or directory.
await php.stat(path);php.mkdir
Create a directory.
await php.mkdir(path);php.rmdir
Delete a directory (must be empty).
await php.rmdir(path);php.unlink
Delete a file.
await php.unlink(path);php.rename
Rename a file or directory.
await php.rename(path, newPath);php.writeFile
Create a new file. Content should be supplied as a Uint8Array, or optionally as a string of text.
await php.writeFile(path, data);await php.writeFile(path, data, {encoding: 'utf8'});Transactions
Web and Worker only!
With persistence enabled, browser runtimes synchronize their mounted IDBFS
storage while holding the php-wasm-fs-lock Web Lock.
When Web Locks are unavailable, such as on a plain HTTP origin reached by a LAN IP address, browser runtimes fall back to a FIFO lock within the current page or worker. That fallback coordinates runtimes in the same JavaScript realm only. Use HTTPS, where Web Locks are available, when tabs or workers share persistent storage.
Browser CGI batches queued filesystem calls into one transaction. After the
queue becomes idle it waits up to 25 ms for more work. Storage is refreshed
once per batch. A batch containing only analyzePath, readdir, readFile, or
stat does not flush; any mutation makes the batch writable. All calls wait
for the shared commit before their promises resolve or the worker replies.
A commit failure rejects every call in that batch. Callback failures still
commit possible partial writes and do not prevent later calls from running.
The batching window keeps the wrapper transaction open; it does not hold an
IndexedDB transaction open. IDBFS opens those while hydrating or flushing.
Calls submitted together, including through Promise.all, can share a batch.
Awaiting each call before submitting the next creates separate batches. A
batch commits after 64 operations or a 250 ms processing window, checked
between operations, so continuous traffic cannot postpone acknowledgment
indefinitely. HTTP CGI requests use a separate path and still flush each
successful PHP request. A typed readdir obtains names and entry types in one
operation. PhpWeb and PhpWorker retain their existing queues; their results
can become available before the shared transaction commits.
Browser CGI tracks PHP and filesystem API mutations and flushes only changed IDBFS records. Clean mounts do not open a write transaction. Renamed directory trees, deletions, file contents and metadata are persisted in the existing IDBFS format, so existing stored files and older readers remain compatible. Hydration still reconciles with persistent storage; it uses a direct local-node walk to avoid repeatedly resolving every path. Mounts with nested filesystems use ordinary reconciliation. Failed commits retain their pending changes and retry them before a later hydration can replace local state.
With {autoTransaction: false}, the caller owns transaction boundaries and
serialization across runtimes. startTransaction() loads persisted storage;
commitTransaction() flushes changes. These methods do not hold a Web Lock
across a sequence of public calls. Do not acquire php-wasm-fs-lock and then
await a public queued method that needs the same lock. Prefer automatic
transactions unless you provide coordination for the complete operation.
php.startTransaction
await php.startTransaction();php.commitTransaction
await php.commitTransaction();For a manually managed transaction that performed only reads, use
await php.commitTransaction(true) to close it without flushing. Never pass
true after a mutation that must be persisted.
quickbus
Install quickbus 1.0.2 or newer to call
php-cgi-wasm filesystem methods on the service worker from the page.
php.handleMessageEvent already speaks its request/reply protocol, so each call
can be awaited:
npm install quickbus@^1.0.2import { Client } from 'quickbus';
const SERVICE_WORKER_SCRIPT_URL = '/cgi-worker.mjs';
await navigator.serviceWorker.register(SERVICE_WORKER_SCRIPT_URL, {type: 'module'});
const registration = await navigator.serviceWorker.ready;
const bus = navigator.serviceWorker.controller
? Client.forServiceWorker(navigator.serviceWorker)
: Client.forServiceWorkerRegistration(registration);
const result = await bus.analyzePath('/path/to/your/file');- Use
Client.forServiceWorker(navigator.serviceWorker)once the page is already controlled by the worker. - Use
Client.forServiceWorkerRegistration(registration)on first load, afterawait navigator.serviceWorker.ready.
Errors raised in the worker, including denied persistent storage, reject the
call. Each call returns a request handle; call abort() on it to stop waiting
locally. Private-mode storage availability depends on the browser.
php-cgi-wasm/msg-bus.mjs, the original helper that quickbus grew out of, still
ships for existing code. Use quickbus for new code.
php.handleMessageEvent
Once you've got the above set up, use php.handleMessageEvent to handle the message events on the service worker:
self.addEventListener('message', event => php.handleMessageEvent(event));🏗️ Custom Builds
To use the in-place builder, first install php-wasm-builder globally:
Requires Docker with the docker compose plugin, Node.js/npm, coreutils, wget, and Make.
$ npm install -g php-wasm-builderphp-wasm-build is an alias for php-wasm-builder; both commands use the same
Make targets. The builder package includes build sources, package templates,
and Docker image helpers. Runtime binaries are produced in your project.
Maintainers can prepare a source-only release without publishing it:
make package-builder
npm install -g ./.cache/release/php-wasm-builder-0.2.0.tgzThe tarball and its SHA-256 inventory are written to .cache/release/ (override
with BUILDER_PACKAGE_OUTPUT). Native outputs, caches, local environment files,
and credentials are excluded. ./publish-packages.sh next --dry-run includes
this staged builder in the release inventory and skips unchanged packages.
To prepare the publishable packages, start from a clean checkout of the release commit, then replace every generated package file with the output of a successful Build Artifacts run for that exact commit:
git worktree add --detach ../php-wasm-release v0.2.0
cd ../php-wasm-release
make release-overlay RUN_ID=<build-artifacts-run-id>
./publish-packages.sh latest --dry-runmake release-overlay refuses runs that did not succeed or were built from a
different commit, runs make clean-packages (which removes generated package
files without touching build caches), then overlays the php-indexed-packages
artifact with the GitHub CLI and deletes its download afterwards.
Create the build environment (can be run from anywhere):
$ php-wasm-builder imageOptionally clean up files from a previous build:
$ php-wasm-builder cleanBuild for web
Then navigate to the directory you want the files to be built in, and run php-wasm-builder build
$ cd ~/my-project
$ php-wasm-builder build
# php-wasm-builder build web
# "web" is the default hereBuild for node
$ cd ~/my-project
$ php-wasm-builder build nodeESM Modules:
Build ESM modules with:
$ php-wasm-builder build web mjs
$ php-wasm-builder build node mjsCGI Modules:
Build CGI modules with:
$ php-wasm-builder build web cgi mjs
$ php-wasm-builder build node cgi mjs
$ php-wasm-builder build worker cgi mjsCLI Modules:
Build php-cli-wasm modules with:
$ php-wasm-builder build node cli mjsDBG Modules:
Build php-dbg-wasm modules with:
$ php-wasm-builder build node dbg mjsSDL and Cloudflare Packages:
Build the standalone php-sdl-wasm and php-cloud-wasm packages with:
$ php-wasm-builder build sdl mjs
$ php-wasm-builder build cloudflare mjsThese targets support only embedded PHP as ES modules. See the SDL guide and CLOUDFLARE.md.
This will build the package inside the current directory (or in PHP_DIST_DIR, see below for more info.)
.php-wasm-rc
You can also create a .php-wasm-rc file in this directory to customize the build.
# Select a PHP version
PHP_VERSION=8.4
# Build the package to a directory other than the current one (RELATIVE path)
PHP_DIST_DIR=./public
# Build the extensions to a directory other than the current one (RELATIVE path)
PHP_ASSET_DIR=./public
# Build the cgi package to a directory other than the current one (RELATIVE path)
PHP_CGI_DIST_DIR=./public
# Build the cgi package's extensions to a directory other than the current one (RELATIVE path)
PHP_CGI_ASSET_DIR=./public
# Space separated list of files/directories to include under /preload.
# Relative paths are resolved from the current project directory.
PRELOAD_ASSETS=./php-scripts ~/other-dir/example.php
# Memory to start the instance with, before growth
INITIAL_MEMORY=2048MB
# Build with assertions enabled
ASSERTIONS=0
# Select the optimization level
OPTIMIZE=3
# Build with extensions
WITH_GD=1
WITH_LIBPNG=1
WITH_LIBJPEG=1
WITH_FREETYPE=1Options
The following options may appear in .php-wasm-rc.
PHP_VERSION
8.0|8.1|8.2|8.3|8.4|8.5
PHP 8.0 builds must also set WITH_PDO_PGLITE=0, because PDO-PGlite requires
PHP 8.1 or newer.
PHP_DIST_DIR
This is the directory where JavaScript and wasm files will be built. It accepts
an absolute path or a path relative to the current build directory. When using
php-wasm-builder, relative paths in .php-wasm-rc resolve from the project
directory.
PHP_ASSET_DIR
This is the directory where preload .data / .dat files and other supporting
assets will be built. Paths resolve in the same way as PHP_DIST_DIR, which is
also the default. Shared libraries and side modules remain in their owning
packages. Preload staging reports an error if the native build's .data output
is missing.
OPTIMIZE
0|1|2|3
The optimization level to use while compiling.
SUB_OPTIMIZE
The optimization level to use while compiling libraries. Defaults to OPTIMIZE.
PRELOAD_ASSETS
A list of files & directories to build to the /preload directory. Relative paths are resolved from the current project directory. Anchored paths such as /path/to/file and ~/path/to/file are copied as-is. Will produce a .data file.
ASSERTIONS
0|1
Build with/without assertions.
Extensions
As stated above, extensions may be compiled as dynamic, shared, or static.
- dynamic - these extensions may be loaded selectively at runtime.
- shared - these extensions will always be loaded at startup and can be cached and reused.
- static - these extensions will be built directly into the main wasm binary (may cause a huge filesize).
(defaults provided below in bold)
The following options are available for building static PHP extensions:
WITH_BCMATH # [0, 1] Enabled by default
WITH_CALENDAR # [0, 1] Enabled by default
WITH_CTYPE # [0, 1] Enabled by default
WITH_EXIF # [0, 1] Enabled by default
WITH_FILTER # [0, 1] Enabled by default
WITH_TOKENIZER # [0, 1] Enabled by default
WITH_VRZNO # [0, 1] Enabled by default
WITH_WAITLINE # [0, 1] Disabled by default in raw custom buildsThe following extensions may be compiled as static, shared, or dynamic:
WITH_PHAR # [0, 1, static, dynamic]
WITH_LIBXML # [0, 1, static, shared, dynamic]
WITH_ICONV # [0, 1, static, shared, dynamic]
WITH_SQLITE # [0, 1, static, shared, dynamic]
WITH_LIBZIP # [0, 1, static, shared, dynamic]
WITH_ZLIB # [0, 1, static, shared, dynamic]
WITH_GD # [0, 1, static, dynamic]
WITH_LIBPNG # [0, 1, static, shared]
WITH_FREETYPE # [0, 1, static, shared]
WITH_LIBJPEG # [0, 1, static, shared]
WITH_YAML # [0, 1, static, shared, dynamic]
WITH_TIDY # [0, 1, static, shared, dynamic]
WITH_MBSTRING # [0, 1, static, dynamic]
WITH_ONIGURUMA # [0, 1, static, shared, dynamic]
WITH_OPENSSL # [0, 1, static, shared, dynamic]
WITH_INTL # [0, 1, static, shared, dynamic]SDL runtime options
Build the standalone php-sdl-wasm browser package with make sdl-mjs.
Its Make profile selects WITH_SDL=1. dynamic remains a
legacy alias for 1; SDL PHP extensions are built into the main runtime.
| Option | Values | Default |
| --- | --- | --- |
| WITH_SDL | 0, 1, dynamic | 0 |
| WITH_SDL_IMAGE | 0, 1 | Follows SDL |
| WITH_SDL_MIXER | 0, 1 | Follows SDL |
| WITH_SDL_TTF | 0, 1 | Follows SDL |
| WITH_OPENGL | 0, 1 | Follows SDL |
The add-ons require SDL. Image loading reuses WITH_LIBPNG/WITH_LIBJPEG, and
text reuses WITH_FREETYPE; those codec libraries must be enabled as static or
shared. WITH_ZLIB=0 still provides the native zlib archive needed by codecs.
Set all four add-on flags to 0 for core SDL only. Build with the existing
make sdl-mjs target, or set the flags in .php-wasm-rc and use
php-wasm-builder build sdl mjs. See the SDL build and API guide.
WITH_PHAR
static|dynamic
When compiled as a dynamic extension, this will produce the extension file php8.x-phar.so.
WITH_LIBXML
static|shared|dynamic
The libxml extension itself must be statically compiled, but libxml2 may be loaded as a shared library.
When compiled as a shared library, it will produce the library libxml2.so.
WITH_LIBZIP
static|shared|dynamic
When compiled as a dynamic extension, this will produce the extension php8.x-zip.so.
When compiled as a dynamic or shared extension, it will produce the library libzip.so.
This extension depends on zlib.
WITH_ICONV
static|shared|dynamic
When compiled as a dynamic extension, this will produce the extension php8.x-iconv.so.
When compiled as a dynamic or shared extension, it will produce the library libiconv.so.
WITH_SQLITE
static|shared|dynamic
When compiled as a dynamic extension, this will produce the extensions php8.x-sqlite.so and php8.x-pdo-sqlite.so.
When compiled as a dynamic or shared extension, it will produce the library libsqlite3.so.
WITH_GD
static|dynamic
This extension makes use of freetype, libjpeg, libpng, and zlib.
When compiled as a dynamic extension, this will produce the extension php8.x-gd.so.
