Skip to content

Build it

PHP runs a copy of the code of the project, without its dev dependencies, embedded at /app. The build says which PHP version, which extensions (see Supported extensions), and where the files go: next to the page that loads them.

The build is a stack, declared once in a function: the build and the checks need the same one. Its methods are the build arguments, see Stacks:

<?php
// castor.php

use Castor\Attribute\AsOption;
use Castor\Attribute\AsTask;
use PhpWasm\Stack;
use PhpWasm\Target;
use Symfony\Component\Console\Input\InputOption;

use function Castor\context;
use function Castor\fs;
use function Castor\run;
use function PhpWasm\build;

defined('CASTOR_USE_CHDIR') || define('CASTOR_USE_CHDIR', true);

function wasm_stack(): Stack
{
    return Stack::named('app')
        // Keep in sync with config.platform.php in composer.json
        ->php('8.5.12')
        ->extensions('ctype', 'mbstring')
        // Relative to the project, at /app in the WASM filesystem
        ->embed('build/embed', at: '/app')
        // Only php-web.mjs, php-web.wasm and php-sw.js
        ->targets(Target::Web)
        ->outputDir('public/build');
}

#[AsTask('build', namespace: 'wasm', description: 'Builds PHP as WebAssembly, with the code of the project')]
function wasm_build(
    #[AsOption(description: 'Given to "docker buildx bake --set", for a cache', mode: InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY)]
    array $set = [],
): void {
    $embed = __DIR__ . '/build/embed';
    fs()->remove($embed);
    fs()->mirror(__DIR__ . '/src', "{$embed}/src");
    fs()->copy(__DIR__ . '/composer.json', "{$embed}/composer.json");
    fs()->copy(__DIR__ . '/composer.lock', "{$embed}/composer.lock");
    // What is embedded takes some of the memory of PHP: no dev dependencies
    run(['composer', 'install', '--no-dev', '--classmap-authoritative'], context: context()->withWorkingDirectory($embed));

    build([wasm_stack()], $set);
}

castor wasm:build writes public/build/php-web.mjs, php-web.wasm and php-sw.js. build() takes several stacks too: they are built in a single buildx invocation, in parallel, and sharing the layers they have in common.

The build is a bake file, at the root of the project. Its arguments are the ones of Build arguments:

# docker-bake.hcl
target "default" {
  context = "vendor/lyrixx/php-wasm-builder/docker"
  # What is embedded, at EMBED_AT
  contexts = {
    embed = "build/embed"
  }
  args = {
    # Keep in sync with config.platform.php in composer.json
    PHP_VERSION = "8.5.12"
    EXTENSIONS = "ctype mbstring"
    EMBED_AT = "/app"
    # Only php-web.mjs, php-web.wasm and php-sw.js
    TARGETS = "web"
  }
  output = ["type=local,dest=public/build"]
}

Composer scripts copy the code, install its dependencies, and build:

{
    "scripts": {
        "wasm:build": [
            "@wasm:embed",
            "docker buildx bake",
            "chmod 755 public/build"
        ],
        "wasm:embed": [
            "rm -rf build/embed && mkdir -p build/embed && cp -R src composer.json composer.lock build/embed/",
            "@composer install --working-dir=build/embed --no-dev --classmap-authoritative"
        ]
    }
}

composer wasm:build writes public/build/php-web.mjs, php-web.wasm and php-sw.js. buildx creates public/build/ readable by its owner only: chmod lets a web server read it.