Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/recipe-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,25 @@ Raw Playground-oriented fields such as `runtime.blueprint`, `runtime.phpVersion`
compatibility fields. Prefer the neutral `wordpress` backend and recipe inputs
for new runtime setup where possible.

## Playground PHP bootstrap settings

Playground disables opcache by default (`opcache.enable = 0` and
`opcache.enable_cli = 0`) in a `php.ini` mounted before PHP initializes. The
php-wasm file cache is slower than compiling scripts from source. To opt back
in, set `inputs.pluginRuntime.php.bootstrapIniEntries`, for example:

```json
{
"opcache.enable": "1",
"opcache.enable_cli": "1",
"opcache.file_cache": "/tmp/opcache"
}
```

Recipe bootstrap entries override these defaults. Use `bootstrapIniEntries`,
not runtime `iniEntries`, for opcache settings that must apply at PHP startup.
The defaults do not affect the choice of Playground runner.

## Stable Field Names

Recipe field names are case-sensitive. The current recipe schema accepts these
Expand Down
25 changes: 12 additions & 13 deletions packages/runtime-playground/src/playground-cli-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,25 +358,18 @@ export function shouldUseProgrammaticPlaygroundRunner(spec: RuntimeCreateSpec, o

async function pluginRuntimeBootstrapSharedMounts(spec: RuntimeCreateSpec, requestWorkerEndpoint?: { route: string; token: string; payloadDirectory: string }): Promise<Array<{ hostPath: string; vfsPath: string }>> {
const iniEntries = runtimeBootstrapPhpIniEntries(spec)
if (!iniEntries && !requestWorkerEndpoint) {
return []
}

const directory = join(spec.artifactsDirectory ?? "artifacts", "playground-internal-shared")
await mkdir(directory, { recursive: true })
if (iniEntries) {
await writeFile(join(directory, "php.ini"), phpIniContent(iniEntries), "utf8")
await writeFile(join(directory, "wp-codebox-auto-prepend.php"), runtimeAutoPrependPhp(spec), "utf8")
}
// Mount before PHP initializes so php-wasm's opcache-enabled defaults never apply.
await writeFile(join(directory, "php.ini"), phpIniContent(iniEntries ?? {}), "utf8")
await writeFile(join(directory, "wp-codebox-auto-prepend.php"), runtimeAutoPrependPhp(spec), "utf8")
if (requestWorkerEndpoint) await writeFile(join(directory, "request-worker.php"), requestWorkerPhp(requestWorkerEndpoint.token), "utf8")
const wpConfig = databaseBootstrapWpConfig(spec)
if (wpConfig) await writeFile(join(directory, "wp-config.php"), wpConfig, "utf8")

return [
...(iniEntries ? [
{ hostPath: join(directory, "php.ini"), vfsPath: "/internal/shared/php.ini" },
{ hostPath: join(directory, "wp-codebox-auto-prepend.php"), vfsPath: "/internal/shared/wp-codebox-auto-prepend.php" },
] : []),
{ hostPath: join(directory, "php.ini"), vfsPath: "/internal/shared/php.ini" },
{ hostPath: join(directory, "wp-codebox-auto-prepend.php"), vfsPath: "/internal/shared/wp-codebox-auto-prepend.php" },
...(requestWorkerEndpoint ? [
{ hostPath: directory, vfsPath: "/internal/wp-codebox" },
{ hostPath: join(directory, "request-worker.php"), vfsPath: `/wordpress${requestWorkerEndpoint.route}` },
Expand Down Expand Up @@ -489,7 +482,13 @@ function phpIniContent(entries: Record<string, string>, autoPrependFile = "/inte
"max_execution_time = 0",
"max_input_time = -1",
]
for (const [name, value] of Object.entries(entries)) {
// Restoring cached scripts is slower than compiling source in php-wasm.
// Merge recipe overrides before serializing so each opcache setting occurs once.
for (const [name, value] of Object.entries({
"opcache.enable": "0",
"opcache.enable_cli": "0",
...entries,
})) {
lines.push(`${name} = ${value}`)
}

Expand Down
38 changes: 34 additions & 4 deletions tests/playground-cli-runner-bootstrap-ini.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ try {
pluginRuntime: {
php: {
iniEntries: { memory_limit: "512M" },
bootstrapIniEntries: { "opcache.file_cache": "/tmp/opcache" },
bootstrapIniEntries: { "opcache.enable_cli": "1", "opcache.file_cache": "/tmp/opcache" },
},
},
},
Expand Down Expand Up @@ -110,6 +110,8 @@ try {
assert.equal(typeof sharedAutoPrependPath, "string")
const sharedPhpIni = await readFile(sharedPhpIniPath as string, "utf8")
assert.match(sharedPhpIni, /opcache\.file_cache = \/tmp\/opcache/)
assert.deepEqual(sharedPhpIni.match(/^opcache\.enable_cli = .*$/gm), ["opcache.enable_cli = 1"], "recipe override is the only serialized value")
assert.match(sharedPhpIni, /^opcache\.enable = 0$/m)
// The runtime default memory ceiling stays high enough for collect_artifacts to
// base64 heavy snapshot/declared-artifact files without a hard PHP fatal.
assert.match(sharedPhpIni, /memory_limit=512M/)
Expand Down Expand Up @@ -217,13 +219,41 @@ try {
await downloadedWordPressServer[Symbol.asyncDispose]()

assert.equal(calls.length, 1)
assert.equal(calls[0]["mount-before-install"]?.length, 2)
assert.equal(calls[0]["mount-before-install"]?.[0]?.vfsPath, "/internal/wp-codebox")
assert.match(calls[0]["mount-before-install"]?.[1]?.vfsPath ?? "", /^\/wordpress\/wp-codebox-execute-[a-f0-9]{24}\.php$/)
assert.equal(calls[0]["mount-before-install"]?.length, 4)
assert.equal(calls[0]["mount-before-install"]?.[0]?.vfsPath, "/internal/shared/php.ini")
assert.equal(calls[0]["mount-before-install"]?.[1]?.vfsPath, "/internal/shared/wp-codebox-auto-prepend.php")
assert.equal(calls[0]["mount-before-install"]?.[2]?.vfsPath, "/internal/wp-codebox")
assert.match(calls[0]["mount-before-install"]?.[3]?.vfsPath ?? "", /^\/wordpress\/wp-codebox-execute-[a-f0-9]{24}\.php$/)
assert.equal(calls[0].wordpressInstallMode, undefined)
assert.equal(calls[0].workers, 1)
assert.equal(shouldUseProgrammaticPlaygroundRunner(downloadedWordPressSpec), false)

// No pluginRuntime, extensions, or auto-prepend body: defaults must still be
// mounted without causing a mounted WordPress source to select the programmatic runner.
calls.length = 0
const plainSpec: RuntimeCreateSpec = {
...defaultRuntimeIniSpec,
environment: { ...defaultRuntimeIniSpec.environment, extensions: undefined },
runtimeEnv: undefined,
secretEnv: undefined,
secretEnvTargets: undefined,
}
assert.equal(shouldUseProgrammaticPlaygroundRunner(plainSpec), false)
assert.equal(shouldUseProgrammaticPlaygroundRunner({
...plainSpec,
metadata: spec.metadata,
}), true, "explicit bootstrap entries still select the programmatic runner")
const plainServer = await startPlaygroundCliServer(plainSpec, [], { cliModule })
await plainServer[Symbol.asyncDispose]()
const plainIniMount = calls[0]["mount-before-install"]?.find((mount) => mount.vfsPath === "/internal/shared/php.ini")
assert.ok(plainIniMount, "default php.ini is mounted before install")
const plainIni = await readFile(plainIniMount.hostPath, "utf8")
assert.match(plainIni, /^opcache\.enable = 0$/m)
assert.match(plainIni, /^opcache\.enable_cli = 0$/m)
const plainAutoPrependMount = calls[0]["mount-before-install"]?.find((mount) => mount.vfsPath === "/internal/shared/wp-codebox-auto-prepend.php")
assert.ok(plainAutoPrependMount)
assert.match(await readFile(plainAutoPrependMount.hostPath, "utf8"), /require_once '\/internal\/shared\/auto_prepend_file\.php'/)

calls.length = 0
const unmanagedInstallServer = await startPlaygroundCliServer({
...downloadedWordPressSpec,
Expand Down
Loading