diff --git a/packages/php-wasm/cli/src/main.ts b/packages/php-wasm/cli/src/main.ts index 4babb1fcb4..dcb5d8019c 100644 --- a/packages/php-wasm/cli/src/main.ts +++ b/packages/php-wasm/cli/src/main.ts @@ -16,6 +16,7 @@ import { PHP } from '@php-wasm/universal'; import { loadNodeRuntime, useHostFilesystem } from '@php-wasm/node'; import { startBridge } from '@php-wasm/xdebug-bridge'; import path from 'path'; +import { cwd } from 'process'; let args = process.argv.slice(2); if (!args.length) { @@ -106,7 +107,9 @@ ${process.argv[0]} ${process.execArgv.join(' ')} ${process.argv[1]} useHostFilesystem(php); if (hasDevtoolsOption && hasXdebugOption) { - const bridge = await startBridge({}); + const bridge = await startBridge({ + phpRoot: cwd(), + }); bridge.start(); } diff --git a/packages/php-wasm/compile/php-wasm-sapi-override/config.m4 b/packages/php-wasm/compile/php-wasm-sapi-override/config.m4 new file mode 100644 index 0000000000..e79827bca0 --- /dev/null +++ b/packages/php-wasm/compile/php-wasm-sapi-override/config.m4 @@ -0,0 +1,8 @@ +dnl config.m4 for extension wasm_sapi_override + +PHP_ARG_ENABLE(wasm_sapi_override, whether to enable wasm_sapi_override support, +[ --enable-wasm_sapi_override Enable wasm_sapi_override support]) + +if test "$PHP_wasm_sapi_override" != "no"; then + PHP_NEW_EXTENSION(wasm_sapi_override, wasm_sapi_override.c, $ext_shared) +fi \ No newline at end of file diff --git a/packages/php-wasm/compile/php-wasm-sapi-override/config.w32 b/packages/php-wasm/compile/php-wasm-sapi-override/config.w32 new file mode 100644 index 0000000000..4cdb643c3b --- /dev/null +++ b/packages/php-wasm/compile/php-wasm-sapi-override/config.w32 @@ -0,0 +1,5 @@ +ARG_ENABLE("wasm_sapi_override", "Enable wasm_sapi_override support", "no"); + +if (PHP_wasm_sapi_override == "yes") { + EXTENSION("wasm_sapi_override", "wasm_sapi_override.c"); +} \ No newline at end of file diff --git a/packages/php-wasm/compile/php-wasm-sapi-override/wasm_sapi_override.c b/packages/php-wasm/compile/php-wasm-sapi-override/wasm_sapi_override.c new file mode 100644 index 0000000000..9fbcf8169d --- /dev/null +++ b/packages/php-wasm/compile/php-wasm-sapi-override/wasm_sapi_override.c @@ -0,0 +1,135 @@ +/** + * Allows changing the SAPI name at runtime. + * + * Affects: + * - php_sapi_name() + * - PHP_SAPI constant + * + * Usage: + * ```php + * set_sapi_name('wasm'); + * ``` + */ +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "php.h" +#include "SAPI.h" +#include "zend_constants.h" +#include "ext/standard/info.h" +#include "wasm_sapi_override.h" + +static char *set_sapi_name_original_name = NULL; +static char *set_sapi_name_prev_allocated = NULL; + +/* {{{ proto bool set_sapi_name(string $new_name) */ +PHP_FUNCTION(set_sapi_name) +{ + char *new_name; + size_t new_len; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STRING(new_name, new_len) + ZEND_PARSE_PARAMETERS_END(); + + /* --- overwrite sapi_module.name ------------------------------------- */ + if (!set_sapi_name_original_name) { + set_sapi_name_original_name = sapi_module.name; /* remember for restore */ + } + + char *buf = pemalloc(new_len + 1, 1); /* persistent */ + memcpy(buf, new_name, new_len); + buf[new_len] = '\0'; + + if (set_sapi_name_prev_allocated) { + pefree(set_sapi_name_prev_allocated, 1); + } + sapi_module.name = buf; + set_sapi_name_prev_allocated = buf; + + /* --- update PHP_SAPI constant --------------------------------------- */ + zend_string *const_name = zend_string_init("PHP_SAPI", sizeof("PHP_SAPI") - 1, 0); + zend_constant *c = zend_hash_find_ptr(EG(zend_constants), const_name); + + zend_string *new_zstr = zend_string_init(new_name, new_len, 1); /* persistent */ + + if (c) { + if (Z_TYPE(c->value) == IS_STRING) { + zend_string *old = Z_STR(c->value); + /* Only release if it's not interned (interned strings live for entire process) */ + if (!ZSTR_IS_INTERNED(old)) { + zend_string_release_ex(old, 1); + } + } + ZVAL_STR(&c->value, new_zstr); + } else { + zval zv; + ZVAL_STR(&zv, new_zstr); + zend_register_constant(&(zend_constant){ + .name = const_name, + .value = zv, + }); + } + zend_string_release_ex(const_name, 0); + + RETURN_TRUE; +} +/* }}} */ + +/* arginfo */ +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_set_sapi_name, 0, 1, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, new_name, IS_STRING, 0) +ZEND_END_ARG_INFO() + +/* function list */ +static const zend_function_entry wasm_sapi_override_functions[] = { + PHP_FE(set_sapi_name, arginfo_set_sapi_name) + PHP_FE_END +}; + +/* MINIT / MSHUTDOWN */ +static PHP_MINIT_FUNCTION(wasm_sapi_override) { return SUCCESS; } + +static PHP_MSHUTDOWN_FUNCTION(wasm_sapi_override) +{ + if (set_sapi_name_prev_allocated) { + pefree(set_sapi_name_prev_allocated, 1); + set_sapi_name_prev_allocated = NULL; + } + if (set_sapi_name_original_name) { + sapi_module.name = set_sapi_name_original_name; + } + return SUCCESS; +} + +/* info */ +static PHP_MINFO_FUNCTION(wasm_sapi_override) +{ + php_info_print_table_start(); + php_info_print_table_row(2, "set_sapi_name support", "enabled"); + php_info_print_table_row(2, "version", PHP_WASM_SAPI_OVERRIDE_MODULE_VERSION); + php_info_print_table_row(2, "current SAPI", sapi_module.name); + php_info_print_table_end(); +} + +/* module entry */ +zend_module_entry wasm_sapi_override_module_entry = { + STANDARD_MODULE_HEADER, + "wasm_sapi_override", /* Extension name */ + wasm_sapi_override_functions, /* Function entries */ + PHP_MINIT(wasm_sapi_override), /* PHP_MINIT - Module initialization */ + PHP_MSHUTDOWN(wasm_sapi_override), /* PHP_MSHUTDOWN - Module shutdown */ + NULL, /* PHP_RINIT - Request initialization */ + NULL, /* PHP_RSHUTDOWN - Request shutdown */ + PHP_MINFO(wasm_sapi_override), /* PHP_MINFO - Module info */ + PHP_WASM_SAPI_OVERRIDE_MODULE_VERSION, /* Version */ + STANDARD_MODULE_PROPERTIES +}; + +#ifdef COMPILE_DL_WASM_SAPI_OVERRIDE +# ifdef ZTS +ZEND_TSRMLS_CACHE_DEFINE() +# endif +ZEND_GET_MODULE(wasm_sapi_override) +#endif diff --git a/packages/php-wasm/compile/php-wasm-sapi-override/wasm_sapi_override.h b/packages/php-wasm/compile/php-wasm-sapi-override/wasm_sapi_override.h new file mode 100644 index 0000000000..6911af2b15 --- /dev/null +++ b/packages/php-wasm/compile/php-wasm-sapi-override/wasm_sapi_override.h @@ -0,0 +1,11 @@ +/* wasm_memory_storage extension for PHP */ + +#ifndef PHP_WASM_SAPI_OVERRIDE_H +# define PHP_WASM_SAPI_OVERRIDE_H + +extern zend_module_entry wasm_sapi_override_module_entry; +# define phpext_wasm_sapi_override_ptr &wasm_sapi_override_module_entry + +# define PHP_WASM_SAPI_OVERRIDE_MODULE_VERSION "0.0.1" + +#endif /* PHP_WASM_SAPI_OVERRIDE_H */ diff --git a/packages/php-wasm/compile/php/Dockerfile b/packages/php-wasm/compile/php/Dockerfile index 946403be02..8a7dda791b 100644 --- a/packages/php-wasm/compile/php/Dockerfile +++ b/packages/php-wasm/compile/php/Dockerfile @@ -23,6 +23,9 @@ RUN git clone https://github.com/php/php-src.git php-src \ # Work around memory leak due to PHP using Emscripten's incomplete mmap/munmap support COPY ./php-wasm-memory-storage /root/php-src/ext/wasm_memory_storage +# Allow changing the SAPI name at runtime +COPY ./php-wasm-sapi-override /root/php-src/ext/wasm_sapi_override + # Polyfill for dns functions COPY ./php-wasm-dns-polyfill/ /root/php-src/ext/dns_polyfill @@ -412,6 +415,7 @@ CURL_LIBS="-I/root/lib/lib -L/root/lib/lib" \ --enable-ctype \ --enable-tokenizer \ --enable-wasm_memory_storage \ + --enable-wasm_sapi_override \ --enable-dns_polyfill \ --enable-post_message_to_js \ $(cat /root/.php-configure-flags) diff --git a/packages/php-wasm/supported-php-versions.mjs b/packages/php-wasm/supported-php-versions.mjs index 882501b86d..f3671eada0 100644 --- a/packages/php-wasm/supported-php-versions.mjs +++ b/packages/php-wasm/supported-php-versions.mjs @@ -6,7 +6,7 @@ * @property {string} lastRelease */ -export const lastRefreshed = '2025-07-22T07:55:56.719Z'; +export const lastRefreshed = '2025-07-24T23:47:22.787Z'; /** * @type {PhpVersion[]} diff --git a/packages/php-wasm/universal/src/lib/php.ts b/packages/php-wasm/universal/src/lib/php.ts index 19489e7a18..28d5cbd1b4 100644 --- a/packages/php-wasm/universal/src/lib/php.ts +++ b/packages/php-wasm/universal/src/lib/php.ts @@ -68,7 +68,6 @@ type MountObject = { */ export class PHP implements Disposable { protected [__private__dont__use]: any; - #sapiName?: string; #webSapiInitialized = false; #wasmErrorsTarget: UnhandledRejectionsTarget | null = null; #eventListeners: Map> = new Map(); @@ -77,6 +76,7 @@ export class PHP implements Disposable { requestHandler?: PHPRequestHandler; private cliCalled = false; private runStreamCalled = false; + private sapiName = 'playground'; /** * An exclusive lock that prevent multiple requests from running at @@ -338,19 +338,8 @@ export class PHP implements Disposable { /** @inheritDoc */ async setSapiName(newName: string) { - const result = this[__private__dont__use].ccall( - 'wasm_set_sapi_name', - NUMBER, - [STRING], - [newName] - ); - if (result !== 0) { - throw new Error( - 'Could not set SAPI name. This can only be done before the PHP WASM module is initialized.' + - 'Did you already dispatch any requests?' - ); - } - this.#sapiName = newName; + this.sapiName = newName; + this.defineConstant('PLAYGROUND_SAPI_NAME', newName); } /** @@ -591,6 +580,7 @@ export class PHP implements Disposable { async () => { if (!this.#webSapiInitialized) { await this.#initWebRuntime(); + this.setSapiName(this.sapiName); this.#webSapiInitialized = true; } if ( @@ -1252,10 +1242,6 @@ export class PHP implements Disposable { // Initialize the new runtime this.initializeRuntime(runtime); - if (this.#sapiName) { - this.setSapiName(this.#sapiName); - } - // Copy the old /internal directory to the new filesystem copyFS(oldFS, this[__private__dont__use].FS, '/internal'); @@ -1346,14 +1332,20 @@ export class PHP implements Disposable { ); } + const sapiNameBefore = this.sapiName; + this.sapiName = 'cli'; return await this.#executeWithErrorHandling(() => { return this[__private__dont__use].ccall('run_cli', null, [], [], { async: true, }); - }).then((response) => { - response.exitCode.finally(release); - return response; - }); + }) + .then((response) => { + response.exitCode.finally(release); + return response; + }) + .finally(() => { + this.setSapiName(sapiNameBefore); + }); } setSkipShebang(shouldSkip: boolean) { diff --git a/packages/php-wasm/web/public/php/jspi/8_0_30/php_8_0.wasm b/packages/php-wasm/web/public/php/jspi/8_0_30/php_8_0.wasm index 5f55671fbd..2206841b4b 100755 Binary files a/packages/php-wasm/web/public/php/jspi/8_0_30/php_8_0.wasm and b/packages/php-wasm/web/public/php/jspi/8_0_30/php_8_0.wasm differ diff --git a/packages/php-wasm/web/public/php/jspi/php_8_0.js b/packages/php-wasm/web/public/php/jspi/php_8_0.js index 954307aa9a..da84358a02 100644 --- a/packages/php-wasm/web/public/php/jspi/php_8_0.js +++ b/packages/php-wasm/web/public/php/jspi/php_8_0.js @@ -1,6 +1,6 @@ import dependencyFilename from './8_0_30/php_8_0.wasm'; export { dependencyFilename }; -export const dependenciesTotalSize = 16162590; +export const dependenciesTotalSize = 16163531; const phpVersionString = '8.0.30'; export function init(RuntimeName, PHPLoader) { // The rest of the code comes from the built php.js file and esm-suffix.js diff --git a/packages/php-wasm/xdebug-bridge/src/lib/start-bridge.ts b/packages/php-wasm/xdebug-bridge/src/lib/start-bridge.ts index c042356534..a79af0b86d 100644 --- a/packages/php-wasm/xdebug-bridge/src/lib/start-bridge.ts +++ b/packages/php-wasm/xdebug-bridge/src/lib/start-bridge.ts @@ -58,21 +58,17 @@ export async function startBridge(config: StartBridgeConfig) { } const getPHPFile = config.phpInstance - ? (path: string) => - new Promise(() => - config.phpInstance!.readFileAsText(path) - ) + ? (path: string) => config.phpInstance!.readFileAsText(path) : config.getPHPFile ? config.getPHPFile - : (path: string) => - new Promise(() => { - // Default implementation: read from filesystem - // Convert file:/// URLs to local paths - const localPath = path.startsWith('file://') - ? path.replace('file://', '') - : path; - return readFileSync(localPath, 'utf-8'); - }); + : (path: string) => { + // Default implementation: read from filesystem + // Convert file:/// URLs to local paths + const localPath = path.startsWith('file://') + ? path.replace('file://', '') + : path; + return readFileSync(localPath, 'utf-8'); + }; const phpFiles = getPhpFiles(phpRoot); return new XdebugCDPBridge(dbgpSession, cdpServer, { diff --git a/packages/php-wasm/xdebug-bridge/src/lib/xdebug-cdp-bridge.ts b/packages/php-wasm/xdebug-bridge/src/lib/xdebug-cdp-bridge.ts index 5eed4e5816..be0e94218a 100644 --- a/packages/php-wasm/xdebug-bridge/src/lib/xdebug-cdp-bridge.ts +++ b/packages/php-wasm/xdebug-bridge/src/lib/xdebug-cdp-bridge.ts @@ -28,7 +28,7 @@ export interface XdebugCDPBridgeConfig { knownScriptUrls: string[]; remoteRoot?: string; localRoot?: string; - getPHPFile(path: string): Promise; + getPHPFile(path: string): string | Promise; } export class XdebugCDPBridge { @@ -45,7 +45,7 @@ export class XdebugCDPBridge { private xdebugConnected = false; private xdebugStatus = 'starting'; private initFileUri: string | null = null; - private readPHPFile: (path: string) => Promise; + private readPHPFile: (path: string) => string | Promise; private remoteRoot: string; private localRoot: string; @@ -688,7 +688,8 @@ export class XdebugCDPBridge { if (pending && pending.cdpId !== undefined) { // Handle variables or object properties retrieval const props: any = []; - const responseProps = response.property; + const responseProps = + response.property?.property ?? response.property; if (responseProps) { const propertiesArray = Array.isArray(responseProps) ? responseProps @@ -859,10 +860,12 @@ export class XdebugCDPBridge { // Map callFrameId to depth for evaluate this.callFramesMap.set(callFrameId, level); // Prepare scope chain (local and global) + const scopes: any[] = []; - // Local scope - const localObjectId = String(this.nextObjectId++); - this.objectHandles.set(localObjectId, { + + // locals + const localsId = String(this.nextObjectId++); + this.objectHandles.set(localsId, { type: 'context', contextId: 0, depth: level, @@ -870,14 +873,15 @@ export class XdebugCDPBridge { scopes.push({ type: 'local', object: { - objectId: localObjectId, + objectId: localsId, className: 'Object', - description: 'Local', + description: 'Locals', }, }); - // Global scope (superglobals in PHP) - const globalObjectId = String(this.nextObjectId++); - this.objectHandles.set(globalObjectId, { + + // super‑globals (Xdebug context‑id = 1) + const superId = String(this.nextObjectId++); + this.objectHandles.set(superId, { type: 'context', contextId: 1, depth: level, @@ -885,27 +889,23 @@ export class XdebugCDPBridge { scopes.push({ type: 'global', object: { - objectId: globalObjectId, + objectId: superId, className: 'Object', - description: 'Global', + description: 'Superglobals', }, }); - // Build callFrame entry + + // build the frame callFrames.push({ - callFrameId: callFrameId, - functionName: functionName, + callFrameId, + functionName, location: { - scriptId: scriptId, + scriptId, lineNumber: line - 1, columnNumber: 0, }, scopeChain: scopes, - this: { - type: 'object', - className: 'Object', - description: 'Object', - objectId: globalObjectId, - }, + this: { type: 'undefined' }, }); } // Send paused event to DevTools diff --git a/packages/playground/client/src/index.ts b/packages/playground/client/src/index.ts index 2716ea65cd..4797835868 100644 --- a/packages/playground/client/src/index.ts +++ b/packages/playground/client/src/index.ts @@ -176,7 +176,7 @@ export async function startPlaygroundWeb({ * @see https://github.com/WordPress/wordpress-playground/pull/2295 */ if (compiled.features.networking) { - await playground.prefetchUpdateChecks(); + // await playground.prefetchUpdateChecks(); } progressTracker.finish(); diff --git a/packages/playground/remote/src/lib/worker-thread.ts b/packages/playground/remote/src/lib/worker-thread.ts index ec72976816..6bb1ffedbf 100644 --- a/packages/playground/remote/src/lib/worker-thread.ts +++ b/packages/playground/remote/src/lib/worker-thread.ts @@ -181,7 +181,7 @@ export class PlaygroundWorkerEndpoint extends PHPWorker { wpVersion = LatestMinifiedWordPressVersion, sqliteDriverVersion = LatestSqliteDriverVersion, phpVersion = RecommendedPHPVersion, - sapiName = 'cli', + sapiName, withICU = false, withNetworking = true, shouldInstallWordPress = true, diff --git a/packages/playground/remote/src/lib/worker-utils.ts b/packages/playground/remote/src/lib/worker-utils.ts index a88b020ada..b66015bf2b 100644 --- a/packages/playground/remote/src/lib/worker-utils.ts +++ b/packages/playground/remote/src/lib/worker-utils.ts @@ -70,6 +70,9 @@ export function spawnHandlerFactory(processManager: PHPProcessManager) { define('STDOUT', fopen('php://stdout', 'wb')); define('STDERR', fopen('/tmp/stderr', 'wb')); + // Override the SAPI name to pass the check in wp-cli. + set_sapi_name('cli'); + ${options.cwd ? 'chdir(getenv("DOCROOT")); ' : ''} `; diff --git a/packages/playground/wordpress/src/index.ts b/packages/playground/wordpress/src/index.ts index 27ff9c8b14..8dacd5e8da 100644 --- a/packages/playground/wordpress/src/index.ts +++ b/packages/playground/wordpress/src/index.ts @@ -320,6 +320,14 @@ export async function preloadPhpInfoRoute( php: UniversalPHP, requestPath = '/phpinfo.php' ) { + await php.writeFile( + '/internal/shared/preload/0-sapi-name.php', + `