diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 78876eaa54..354975ea34 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -6,6 +6,27 @@ env: IMAGE_ID: $IMAGE_ID +e2e_config: &e2e_config + command: bash .buildkite/commands/run-e2e-tests.sh "{{matrix.platform}}" "{{matrix.arch}}" + artifact_paths: + - test-results/**/*.zip + - test-results/**/*.png + - test-results/**/*error-context.md + plugins: [$CI_TOOLKIT_PLUGIN, $NVM_PLUGIN] + agents: + queue: "{{matrix.platform}}" + env: + # See https://playwright.dev/docs/ci#debugging-browser-launches + DEBUG: "pw:browser" + matrix: + setup: { platform: [], arch: [] } + adjustments: + - with: { platform: mac, arch: arm64 } + - with: { platform: windows, arch: x64 } + notify: + - github_commit_status: + context: E2E Tests + steps: - label: Lint agents: @@ -51,30 +72,20 @@ steps: # E2E tests run on supported platform/architecture combinations. # - mac-arm64: Native on Apple Silicon agents # - windows-x64: Native on x64 agents - - label: E2E Tests on {{matrix.platform}}-{{matrix.arch}} + - <<: *e2e_config + label: E2E Tests on {{matrix.platform}}-{{matrix.arch}} key: e2e_tests - command: bash .buildkite/commands/run-e2e-tests.sh "{{matrix.platform}}" "{{matrix.arch}}" - artifact_paths: - - test-results/**/*.zip - - test-results/**/*.png - - test-results/**/*error-context.md - plugins: [$CI_TOOLKIT_PLUGIN, $NVM_PLUGIN] - agents: - queue: "{{matrix.platform}}" - env: - # See https://playwright.dev/docs/ci#debugging-browser-launches - DEBUG: "pw:browser" - matrix: - setup: { platform: [], arch: [] } - adjustments: - - with: { platform: mac, arch: arm64 } - - with: { platform: windows, arch: x64 } # Skip E2E tests on draft PRs to save CI resources # Tests will run automatically when PR is marked ready for review - if: build.branch == 'trunk' || build.tag =~ /^v[0-9]+/ || !build.pull_request.draft - notify: - - github_commit_status: - context: E2E Tests + if: (build.branch == 'trunk' || build.tag =~ /^v[0-9]+/ || !build.pull_request.draft) && !(build.pull_request.labels includes 'native-php') + + - <<: *e2e_config + label: E2E Tests on {{matrix.platform}}-{{matrix.arch}} with native PHP + key: e2e_tests_native_php + env: + DEBUG: "pw:browser" + STUDIO_RUNTIME: native-php + if: (build.branch == 'trunk' || build.tag =~ /^v[0-9]+/ || !build.pull_request.draft) && build.pull_request.labels includes 'native-php' - label: ":chart_with_upwards_trend: Performance Metrics" key: metrics diff --git a/.gitignore b/.gitignore index 02069cdcac..7d8a73607e 100644 --- a/.gitignore +++ b/.gitignore @@ -95,7 +95,10 @@ typings/ out/ # The latest WordPress files downloaded from the web -wp-files/ +wp-files/* +!wp-files/blueprints/ +wp-files/blueprints/* +!wp-files/blueprints/blueprints.phar # Release Tooling vendor/* diff --git a/apps/cli/commands/wp.ts b/apps/cli/commands/wp.ts index a0418e2630..4f61109505 100644 --- a/apps/cli/commands/wp.ts +++ b/apps/cli/commands/wp.ts @@ -8,6 +8,7 @@ import { SiteData } from 'cli/lib/cli-config/core'; import { getSiteByFolder } from 'cli/lib/cli-config/sites'; import { connectToDaemon, disconnectFromDaemon } from 'cli/lib/daemon-client'; import { getPhpBinaryPath, getWpCliPharPath } from 'cli/lib/dependency-management/paths'; +import { ensurePhpBinaryAvailable } from 'cli/lib/dependency-management/php-binary'; import { runWpCliCommand, runGlobalWpCliCommand, WpCliResponse } from 'cli/lib/run-wp-cli-command'; import { validatePhpVersion } from 'cli/lib/utils'; import { isServerRunning, sendWpCliCommand } from 'cli/lib/wordpress-server-manager'; @@ -44,6 +45,7 @@ enum Mode { async function runNativePhpWpCliCommand( site: SiteData, args: string[] ): Promise< void > { const phpVersion = validateNativePhpVersion( site.phpVersion ); + await ensurePhpBinaryAvailable( phpVersion ); await writeStudioMuPluginsForNativePhpRuntime( site.path, site.isWpAutoUpdating ); const child = spawn( getPhpBinaryPath( phpVersion ), diff --git a/apps/cli/lib/dependency-management/utils.ts b/apps/cli/lib/dependency-management/utils.ts index 65fa3ff605..fcae1b7c95 100644 --- a/apps/cli/lib/dependency-management/utils.ts +++ b/apps/cli/lib/dependency-management/utils.ts @@ -1,5 +1,6 @@ import fs from 'fs'; import path from 'path'; +import { isErrnoException } from '@studio/common/lib/is-errno-exception'; import ignore from 'ignore'; export { downloadFile } from '@studio/common/lib/download-file'; @@ -17,7 +18,17 @@ async function collectDirectoryMetadata( basePath = directoryPath ): Promise< Map< string, FileMetadata > > { const files = new Map< string, FileMetadata >(); - const entries = await fs.promises.readdir( directoryPath, { withFileTypes: true } ); + let entries: fs.Dirent[]; + try { + entries = await fs.promises.readdir( directoryPath, { withFileTypes: true } ); + } catch ( error ) { + // Directory disappeared between the parent's readdir and our entry into it. + // Treat as empty so the caller can still compare what remains. + if ( isErrnoException( error ) && error.code === 'ENOENT' ) { + return files; + } + throw error; + } for ( const entry of entries ) { const fullPath = path.join( directoryPath, entry.name ); @@ -39,8 +50,16 @@ async function collectDirectoryMetadata( continue; } - const stats = await fs.promises.lstat( fullPath ); - files.set( relativePath, { size: stats.size, mtimeMs: Math.floor( stats.mtimeMs ) } ); + try { + const stats = await fs.promises.lstat( fullPath ); + files.set( relativePath, { size: stats.size, mtimeMs: Math.floor( stats.mtimeMs ) } ); + } catch ( error ) { + // File vanished between readdir and lstat (e.g. concurrent cleanup). Skip it. + if ( isErrnoException( error ) && error.code === 'ENOENT' ) { + continue; + } + throw error; + } } return files; diff --git a/apps/cli/lib/socket.ts b/apps/cli/lib/socket.ts index 32208c1a8b..520ca16e1d 100644 --- a/apps/cli/lib/socket.ts +++ b/apps/cli/lib/socket.ts @@ -6,6 +6,9 @@ import { DaemonResponse } from 'cli/lib/types/process-manager-ipc'; const DEFAULT_CONNECT_TIMEOUT_MS = 500; const DEFAULT_RECONNECT_DELAY_MS = 500; +// How long to wait for a socket's pending writes to flush during graceful close before +// giving up and forcefully destroying it. +const GRACEFUL_SOCKET_END_TIMEOUT_MS = 500; function isWindowsNamedPipe( endpoint: string ): boolean { return endpoint.startsWith( '\\\\.\\pipe\\' ); @@ -421,20 +424,41 @@ export class SocketServer extends SocketServerEventEmitter { } ); } - close(): Promise< void > { - return new Promise< void >( ( resolve ) => { - for ( const socket of this.sockets ) { - socket.destroy(); - } + async close(): Promise< void > { + // End each socket gracefully so any pending writes (e.g. an in-flight response) + // have a chance to flush. Forcefully destroy as a fallback if the peer is slow. + await Promise.all( + Array.from( this.sockets ).map( ( socket ) => this.endSocketGracefully( socket ) ) + ); + + if ( ! this.server.listening ) { + return; + } + + await new Promise< void >( ( resolve ) => { + this.server.close( () => { + resolve(); + } ); + } ); + } - if ( ! this.server.listening ) { + private endSocketGracefully( socket: net.Socket ): Promise< void > { + return new Promise< void >( ( resolve ) => { + if ( socket.destroyed ) { resolve(); return; } - this.server.close( () => { + const fallback = setTimeout( () => { + socket.destroy(); + }, GRACEFUL_SOCKET_END_TIMEOUT_MS ); + + socket.once( 'close', () => { + clearTimeout( fallback ); resolve(); } ); + + socket.end(); } ); } } diff --git a/apps/cli/lib/wordpress-server-manager.ts b/apps/cli/lib/wordpress-server-manager.ts index 8ae24906de..adf5fa5598 100644 --- a/apps/cli/lib/wordpress-server-manager.ts +++ b/apps/cli/lib/wordpress-server-manager.ts @@ -12,6 +12,7 @@ import { PLAYGROUND_CLI_MAX_TIMEOUT, } from '@studio/common/constants'; import { SiteCommandLoggerAction } from '@studio/common/logger-actions'; +import { __ } from '@wordpress/i18n'; import { z } from 'zod'; import { SiteData, SiteRuntime } from 'cli/lib/cli-config/core'; import { @@ -197,6 +198,11 @@ export async function startWordPressServer( await ensurePhpBinaryAvailableIfNeeded( site, logger ); + const startMessage = options?.blueprint + ? __( 'Starting WordPress server and applying Blueprint…' ) + : __( 'Starting WordPress server…' ); + logger.reportStart( SiteCommandLoggerAction.START_SITE, startMessage ); + const wordPressServerChildPath = getChildScriptPath( site.runtime ); const processName = getProcessName( site.id ); const serverConfig = buildServerConfig( site, options ); @@ -514,6 +520,7 @@ export async function runBlueprint( options: RunBlueprintOptions ): Promise< void > { await ensurePhpBinaryAvailableIfNeeded( site, logger ); + logger.reportStart( SiteCommandLoggerAction.APPLY_BLUEPRINT, __( 'Applying Blueprint…' ) ); const wordPressServerChildPath = getChildScriptPath( site.runtime ); const processName = getProcessName( site.id ); diff --git a/apps/cli/php-server-child.ts b/apps/cli/php-server-child.ts index efdafd84bf..5e49f2a507 100644 --- a/apps/cli/php-server-child.ts +++ b/apps/cli/php-server-child.ts @@ -63,11 +63,61 @@ type SpawnPhpProcessOptions = { mode?: 'pipe' | 'capture-stdout'; }; +// Process-scoped opcache dir, created lazily and removed when the process exits +let opcacheRootDir: string | null = null; + +function getOpcacheRootDir(): string { + if ( opcacheRootDir ) { + return opcacheRootDir; + } + + // Resolve to the long-form path on Windows. `os.tmpdir()` can return an 8.3 + // short name (e.g. C:\Users\BUILDK~1\AppData\…) when the user has a long + // username, and PHP's INI scanner treats `~` as a special token, breaking + // `-d opcache.file_cache=` parsing. + const tmpRoot = + process.platform === 'win32' ? fs.realpathSync.native( os.tmpdir() ) : os.tmpdir(); + opcacheRootDir = fs.mkdtempSync( path.join( tmpRoot, 'studio-opcache-' ) ); + const dirToClean = opcacheRootDir; + process.once( 'exit', () => { + try { + fs.rmSync( dirToClean, { recursive: true, force: true } ); + } catch { + // Best effort. The OS will reap tmp eventually. + } + } ); + return opcacheRootDir; +} + +function getDefaultPhpArgs( phpVersion: NativePhpSupportedVersion ): string[] { + if ( process.platform !== 'win32' ) { + return []; + } + + // Partition the file_cache by PHP version: opcache's on-disk script blob + // format isn't stable across minor versions, and reusing a cache populated + // by a different PHP can crash the server at startup on Windows. + const cacheId = `php${ phpVersion }`; + const cacheDirectory = path.join( getOpcacheRootDir(), cacheId ); + fs.mkdirSync( cacheDirectory, { recursive: true } ); + + return [ + '-d', + `opcache.file_cache="${ cacheDirectory }"`, + '-d', + 'opcache.file_cache_fallback=1', + '-d', + `opcache.cache_id="studio-${ cacheId }"`, + ]; +} + function spawnPhpProcess( args: string[], { phpVersion, cwd, signal, mode = 'pipe' }: SpawnPhpProcessOptions ): ChildProcess { - const phpScriptProcess = spawn( getPhpBinaryPath( phpVersion ), args, { + const defaultArgs = getDefaultPhpArgs( phpVersion ); + const phpArgs = [ ...defaultArgs, ...args ]; + const phpScriptProcess = spawn( getPhpBinaryPath( phpVersion ), phpArgs, { cwd, stdio: [ 'ignore', 'pipe', 'pipe' ], signal, @@ -627,6 +677,8 @@ async function ipcMessageHandler( packet: unknown ) { }; process.send!( response ); + // If the `stopServer` function ran successfully, the last open handle should be the IPC channel. + // Disconnect so that the process can exit cleanly. if ( validMessage.topic === 'stop-server' && result === StopServerResult.OK ) { process.disconnect(); } @@ -663,7 +715,11 @@ function shutdownOnSignal( signal: NodeJS.Signals ): void { // If this node process is going down (normal exit or IPC disconnect), make sure PHP goes with it. process.on( 'exit', killPhpProcess ); process.on( 'disconnect', () => { + logToConsole( 'IPC channel disconnected, shutting down' ); killPhpProcess(); + // Without an explicit exit, the wrapper would linger until the event loop drains, + // which delays the daemon's stop sequence and risks the force-kill timer firing. + process.exit( 0 ); } ); // Without explicit signal handlers, the process is terminated abruptly and the 'exit' event diff --git a/apps/cli/process-manager-daemon.ts b/apps/cli/process-manager-daemon.ts index 6ac0d9624e..40a8d8e002 100644 --- a/apps/cli/process-manager-daemon.ts +++ b/apps/cli/process-manager-daemon.ts @@ -1,4 +1,4 @@ -import { ChildProcess, spawn } from 'child_process'; +import { ChildProcess, spawn, spawnSync } from 'child_process'; import fs, { createWriteStream, WriteStream } from 'fs'; import net from 'net'; import path from 'path'; @@ -21,7 +21,7 @@ import { import { ManagerMessage } from 'cli/lib/types/wordpress-server-ipc'; const SOCKET_TIMEOUT_MS = 2_500; -const STOP_TIMEOUT_MS = 5_000; +const STOP_TIMEOUT_MS = 2_500; // In-memory tail of stderr kept per child so we can include the current invocation's error // output in the `exit` event. Bounded to avoid unbounded memory growth on chatty processes. @@ -91,7 +91,7 @@ export class ProcessManagerDaemon { ); private readonly managedProcesses = new Map< number, ManagedProcess >(); private nextPmId = 1; - private shuttingDown = false; + private shutdownPromise: Promise< void > | null = null; async start(): Promise< void > { fs.mkdirSync( PROCESS_MANAGER_LOGS_DIR, { recursive: true } ); @@ -104,7 +104,7 @@ export class ProcessManagerDaemon { process.on( 'SIGINT', () => void this.shutdown( 'signal' ) ); process.on( 'SIGTERM', () => void this.shutdown( 'signal' ) ); process.on( 'exit', () => { - this.forceCleanupChildren(); + void this.forceCleanupChildren(); } ); } @@ -126,7 +126,7 @@ export class ProcessManagerDaemon { if ( request.type === 'kill-daemon' ) { setImmediate( () => { - void this.shutdown( 'kill-daemon' ); + void this.finalizeShutdownByClosingSocketServersAndExiting(); } ); } } catch ( error ) { @@ -175,6 +175,7 @@ export class ProcessManagerDaemon { payload: {}, }; case 'kill-daemon': + await this.beginShutdownByKillingChildren( 'kill-daemon' ); return { type: 'result', payload: {}, @@ -294,7 +295,7 @@ export class ProcessManagerDaemon { await new Promise< void >( ( resolve ) => { const timeoutId = setTimeout( () => { - this.forceCleanupChild( managedProcess ); + void this.signalProcessGroup( managedProcess, 'SIGKILL' ); }, STOP_TIMEOUT_MS ); managedProcess.child.once( 'exit', () => { @@ -309,7 +310,7 @@ export class ProcessManagerDaemon { resolve(); } ); - this.signalProcessGroup( managedProcess, 'SIGTERM' ); + void this.signalProcessGroup( managedProcess, 'SIGTERM' ); } ); } @@ -414,23 +415,49 @@ export class ProcessManagerDaemon { }; } - private forceCleanupChildren() { + private async forceCleanupChildren() { for ( const managedProcess of this.managedProcesses.values() ) { if ( managedProcess.settled ) { continue; } - this.forceCleanupChild( managedProcess ); + await this.signalProcessGroup( managedProcess, 'SIGKILL' ); } } - private forceCleanupChild( managedProcess: ManagedProcess ) { - // On Windows, child.kill() maps any signal to TerminateProcess, so SIGKILL and SIGTERM - // are equivalent there. On non-Windows the helper sends SIGKILL to the whole group. - this.signalProcessGroup( managedProcess, 'SIGKILL' ); - } + private async signalProcessGroup( + managedProcess: ManagedProcess, + signal: NodeJS.Signals + ): Promise< void > { + const pid = managedProcess.child.pid; + if ( ! pid ) { + return; + } - private signalProcessGroup( managedProcess: ManagedProcess, signal: NodeJS.Signals ): void { - if ( process.platform === 'win32' || ! managedProcess.child.pid ) { + if ( process.platform === 'win32' ) { + if ( signal === 'SIGKILL' ) { + // Windows has no process-group concept Node can reach. /T walks the descendant + // tree via parent-PID lookup; /F forces termination. Without /T, grandchildren + // (e.g. the PHP server spawned by the wrapper) would be orphaned. + spawnSync( 'taskkill', [ '/F', '/T', '/PID', String( pid ) ], { + windowsHide: true, + stdio: 'ignore', + } ); + return; + } + // Console apps on Windows have no SIGTERM equivalent — `child.kill( 'SIGTERM' )` + // maps to TerminateProcess of a single PID, so neither cleanup nor tree-walk runs. + // Closing the IPC channel triggers the wrapper's 'disconnect' handler instead, which + // kills the PHP child and exits cleanly. Force escalation falls back to taskkill /T. + if ( managedProcess.child.connected ) { + try { + managedProcess.child.disconnect(); + // Wait very briefly to allow the disconnect handler to run in the child process + await new Promise( ( resolve ) => setTimeout( resolve, 10 ) ); + } catch { + // Do nothing + } + return; + } try { managedProcess.child.kill( signal ); } catch { @@ -443,7 +470,7 @@ export class ProcessManagerDaemon { // process group. Signalling the negative PID delivers to every member of that group, // including grandchildren (e.g. the PHP server spawned by the wrapper). try { - process.kill( -managedProcess.child.pid, signal ); + process.kill( -pid, signal ); } catch { // Group send can fail if the leader has already exited but children remain. try { @@ -454,28 +481,39 @@ export class ProcessManagerDaemon { } } - async shutdown( reason?: string ): Promise< void > { - if ( this.shuttingDown ) { - return; - } - - this.shuttingDown = true; + private async shutdown( reason?: string ): Promise< void > { + await this.beginShutdownByKillingChildren( reason ); + await this.finalizeShutdownByClosingSocketServersAndExiting(); + } - await Promise.allSettled( - Array.from( this.managedProcesses.values() ).map( ( managedProcess ) => - this.stopProcess( managedProcess.name ) - ) - ); + private beginShutdownByKillingChildren( reason?: string ): Promise< void > { + const stopAllChildren = async (): Promise< void > => { + await Promise.allSettled( + Array.from( this.managedProcesses.values() ).map( ( managedProcess ) => + this.stopProcess( managedProcess.name ) + ) + ); + + await this.broadcastEvent( { + type: 'daemon-kill', + payload: { reason }, + } ); + }; - await this.broadcastEvent( { - type: 'daemon-kill', - payload: { reason }, - } ); + // Track in-flight shutdown so concurrent callers (e.g. kill-daemon + a SIGTERM) + // share the same work and all wait for it to finish. + if ( ! this.shutdownPromise ) { + this.shutdownPromise = stopAllChildren().finally( () => { + this.shutdownPromise = null; + } ); + } + return this.shutdownPromise; + } - await new Promise< void >( ( resolve ) => { - void this.controlServer.close().then( () => resolve() ); - } ); + private async finalizeShutdownByClosingSocketServersAndExiting(): Promise< void > { + await this.controlServer.close(); await this.eventsServer.close(); + process.exit( 0 ); } } diff --git a/scripts/download-wp-server-files.ts b/scripts/download-wp-server-files.ts index 093ff353bd..47a05105bb 100644 --- a/scripts/download-wp-server-files.ts +++ b/scripts/download-wp-server-files.ts @@ -93,21 +93,6 @@ const FILES_TO_DOWNLOAD: FileToDownload[] = [ getUrl: () => PHPMYADMIN_DOWNLOAD_URL, destinationPath: path.join( WP_SERVER_FILES_PATH, 'phpmyadmin' ), }, - { - name: 'blueprints-phar', - description: 'blueprints.phar CLI tool', - getUrl: async () => { - const release = await fetchLatestGithubRelease( 'WordPress/php-toolkit' ); - const asset = release.assets.find( ( a ) => a.name === 'blueprints.phar' ); - if ( ! asset ) { - throw new Error( - `blueprints.phar not found in latest php-toolkit release ${ release.tag_name }` - ); - } - return asset.browser_download_url; - }, - destinationPath: path.join( WP_SERVER_FILES_PATH, 'blueprints' ), - }, ]; async function downloadFile( file: FileToDownload ): Promise< void > { @@ -134,9 +119,6 @@ async function downloadFile( file: FileToDownload ): Promise< void > { if ( name === 'wp-cli' ) { console.log( `[${ name }] Moving WP-CLI to destination ...` ); fs.moveSync( zipPath, path.join( extractedPath, 'wp-cli.phar' ), { overwrite: true } ); - } else if ( name === 'blueprints-phar' ) { - console.log( `[${ name }] Moving blueprints.phar to destination ...` ); - fs.moveSync( zipPath, path.join( extractedPath, 'blueprints.phar' ), { overwrite: true } ); } else if ( name === 'sqlite' ) { /** * The SQLite database integration plugin zip extracts into a folder named diff --git a/tools/common/lib/php-binary-metadata.ts b/tools/common/lib/php-binary-metadata.ts index 837e01eebc..dbf5bc5980 100644 --- a/tools/common/lib/php-binary-metadata.ts +++ b/tools/common/lib/php-binary-metadata.ts @@ -39,23 +39,23 @@ export const PHP_PATCH_VERSIONS: Record< NativePhpSupportedVersion, string > = { // Windows ARM64 falls back to x64, so there is no separate win32-arm64 entry. export const PHP_BINARY_HASHES: Record< string, string > = { // PHP 8.5 (8.5.5) - '8.5-darwin-arm64': '025d07dc55e806f98fc36871cf8b342228379b9d5ac3d739d66744a2b550de2a', - '8.5-darwin-x64': 'f206193e86c6ca188fbb46532c432ff3f216f203be9fe67bfc839973de64f9b8', + '8.5-darwin-arm64': '7f072385de34fe31970f82981e841b24d69c590d31d9e0664295b6787a86b689', + '8.5-darwin-x64': '7c0efc0d3bfdb0b9273a9f1481604904aa07a77248096f4cc4a6d92602c6f6a7', '8.5-linux-x64': '72a877af1cb93d7c14f79344bdbd78dc2a57a2e915a76b13e9a3141700df3f21', '8.5-linux-arm64': 'a45fc1f818497586bfd8f749c808f1f63c9d10bf48f439985e77bee607a2c76b', - '8.5-win32-x64': 'ddd8098a1e71dfe53c147c392eeaa50eb61259a1c430e3cbe016b0edbdc1cf91', + '8.5-win32-x64': '1eaacf3a0c91069a596cd9ce59d2931216c56cd575034514ac4bfb30cf8e1892', // PHP 8.4 (8.4.20) - '8.4-darwin-arm64': 'e1ce00874e398bcef5884f2b9067a984480e7dd32d256747f89a75e5f183113c', - '8.4-darwin-x64': '6fc09f87d9676bf8f22b05cf77a91b99ee120414e9a028c6f357c18df531fa83', + '8.4-darwin-arm64': 'fb060f7a5ed7daf201aab3283a314a7b2f2546d730dc755f1bf2b417c6116d18', + '8.4-darwin-x64': 'cf1692ee50fbd888e39c54a211d20b425c65a96e7a4fcb6120ca1aba2544c5cb', '8.4-linux-x64': '3624293e0556625e19f4483d74eac21d41b70d21bdbb7e8ea3e1247303886148', '8.4-linux-arm64': '1be37b0cc533edc691632828ecdaddd84eaa0f71bb9c06a3458347aa13da2987', '8.4-win32-x64': '174ee2fefa1da9727bfc3d89d37b0bc31b474d7cbbf2005d71bf364cf93fd3f2', // PHP 8.3 (8.3.30) - '8.3-darwin-arm64': '8cfc16a7741c94bb1c7c991de554aeae7275308a6233e6da898e69d8d7e88920', + '8.3-darwin-arm64': '99220ea2d706660ee3df04a195d435f7d265c8849f29bf2ec2a99e8bd85e21ca', '8.3-darwin-x64': 'a6389d12d35661a5e9428fa074397f6b086e1aaf5aa7a5461012986d2d2ca3da', '8.3-linux-x64': 'b23bd3cae443dc23f1013190456ce16406b5eb89115ba7c4b6e19ee613948739', '8.3-linux-arm64': 'd8c6c064585190d5ce27671b862c3acacf98b1b248efb402536051768edfa8a9', - '8.3-win32-x64': '7eb9f5f45c24d984e69214e6b1f264bf9bf98f1469e32a720a39b849ab490837', + '8.3-win32-x64': 'df11f5adad83f931d40bb4904c611c058a3ef4d34c596dbae82faaffe7e77579', // PHP 8.2 (8.2.30) '8.2-darwin-arm64': '71189159f072c2b0dec2c4c778f313187234725cc888dd648fde06b9671536b9', '8.2-darwin-x64': '51bbea18b71ee81a547a85fd6f64c671c3bb21e1e2aaaa8317b9ec786755f160', diff --git a/wp-files/blueprints/blueprints.phar b/wp-files/blueprints/blueprints.phar new file mode 100755 index 0000000000..d9027bec3a Binary files /dev/null and b/wp-files/blueprints/blueprints.phar differ