From 65af0646bf3a55141f132fe6694219b75ba17e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 13 Feb 2026 15:06:09 +0100 Subject: [PATCH 1/3] Support split web root and WordPress core directory On WP Cloud sites, the web root (/srv/htdocs with wp-content/) and the WordPress core directory (/srv/htdocs/__wp__/ with wp-load.php, wp-admin, wp-includes) can be separate paths. Previously the runner assumed they were always the same directory. This introduces a WP_CORE_DIR env var alongside DOCROOT so subprocess scripts can find WordPress core files independently of the web root. The ExistingSiteResolver auto-detects the core directory by scanning immediate subdirectories for wp-load.php, and a --wp-core-path CLI flag allows explicit configuration. --- .../class-existingsiteresolver.php | 61 ++++++++- .../SiteResolver/class-newsiteresolver.php | 5 +- .../Steps/class-activatepluginstep.php | 4 +- .../Steps/class-activatethemestep.php | 2 +- .../Steps/class-defineconstantsstep.php | 2 +- .../Steps/class-enablemultisitestep.php | 4 +- .../Steps/class-importcontentstep.php | 2 +- .../Steps/class-importmediastep.php | 6 +- .../class-importthemestartercontentstep.php | 2 +- .../Steps/class-installpluginstep.php | 14 +- .../Steps/class-installthemestep.php | 12 +- .../Blueprints/Steps/class-runsqlstep.php | 2 +- .../Steps/class-setsitelanguagestep.php | 10 +- .../Steps/class-setsiteoptionsstep.php | 2 +- .../Steps/scripts/import-content.php | 2 +- .../Tests/Unit/RunnerConfigurationTest.php | 45 +++++++ .../DetectWordPressCoreDirTest.php | 126 ++++++++++++++++++ .../Unit/Steps/DefineConstantsStepTest.php | 2 +- .../Unit/Steps/EnableMultisiteStepTest.php | 10 +- .../Unit/Steps/InstallPluginStepTest.php | 12 +- .../Tests/Unit/Steps/InstallThemeStepTest.php | 6 +- .../Tests/Unit/Steps/RunPHPStepTest.php | 4 +- .../Tests/Unit/Steps/RunSQLStepTest.php | 10 +- .../Unit/Steps/SetSiteOptionsStepTest.php | 4 +- .../Version1/class-v1tov2transpiler.php | 4 +- components/Blueprints/bin/blueprint.php | 10 ++ components/Blueprints/class-runner.php | 4 +- .../Blueprints/class-runnerconfiguration.php | 28 ++++ components/Blueprints/class-runtime.php | 8 +- 29 files changed, 332 insertions(+), 71 deletions(-) create mode 100644 components/Blueprints/Tests/Unit/RunnerConfigurationTest.php create mode 100644 components/Blueprints/Tests/Unit/SiteResolver/DetectWordPressCoreDirTest.php diff --git a/components/Blueprints/SiteResolver/class-existingsiteresolver.php b/components/Blueprints/SiteResolver/class-existingsiteresolver.php index f42d6d0b..ebe9c4dd 100644 --- a/components/Blueprints/SiteResolver/class-existingsiteresolver.php +++ b/components/Blueprints/SiteResolver/class-existingsiteresolver.php @@ -24,17 +24,26 @@ public static function resolve( Runtime $runtime, Tracker $progress, ?VersionCon // 1. Verify it's a valid WordPress installation. $progress['verify_installation']->setCaption( 'Verifying WordPress installation' ); + + // Auto-detect the WordPress core directory. On WP Cloud sites, the + // WordPress core files (wp-load.php, wp-admin, wp-includes) live in + // a subdirectory like __wp__/ while wp-content stays in the web root. if ( ! $target_fs->exists( 'wp-load.php' ) ) { - throw new BlueprintExecutionException( - 'The target site does not appear to be a valid WordPress installation (wp-load.php not found)' - ); + $detected_core_dir = self::detect_wordpress_core_dir( $config->get_target_site_root() ); + if ( null !== $detected_core_dir ) { + $config->set_wordpress_core_dir( $detected_core_dir ); + } else { + throw new BlueprintExecutionException( + 'The target site does not appear to be a valid WordPress installation (wp-load.php not found)' + ); + } } // Additional check to ensure we can actually load WordPress. try { $result = $runtime->eval_php_code_in_subprocess( 'eval_php_code_in_subprocess( 'output_file_content @@ -92,7 +101,7 @@ public static function resolve( Runtime $runtime, Tracker $progress, ?VersionCon if ( 'sqlite' === $required_engine ) { $sqlite_active = $runtime->eval_php_code_in_subprocess( 'eval_php_code_in_subprocess( 'finish(); $progress->finish(); } + + /** + * Scans the web root for a WordPress core directory. On WP Cloud and + * similar setups the core files live in a subdirectory (e.g. __wp__/) + * while wp-content/ stays in the web root. + * + * @param string $web_root Absolute path to the web root. + * + * @return string|null Absolute path to the WordPress core directory, or + * null when wp-load.php cannot be found anywhere. + */ + public static function detect_wordpress_core_dir( string $web_root ): ?string { + // Standard layout: wp-load.php is in the web root itself. + if ( file_exists( $web_root . '/wp-load.php' ) ) { + return $web_root; + } + + // Scan immediate subdirectories for wp-load.php. This covers the + // WP Cloud convention (__wp__/) as well as any other single-level + // subdirectory layout. + $entries = @scandir( $web_root ); + if ( false === $entries ) { + return null; + } + + foreach ( $entries as $entry ) { + if ( '.' === $entry || '..' === $entry ) { + continue; + } + + $candidate = $web_root . '/' . $entry; + if ( is_dir( $candidate ) && file_exists( $candidate . '/wp-load.php' ) ) { + return $candidate; + } + } + + return null; + } } diff --git a/components/Blueprints/SiteResolver/class-newsiteresolver.php b/components/Blueprints/SiteResolver/class-newsiteresolver.php index 5aa4ef81..4ace812a 100644 --- a/components/Blueprints/SiteResolver/class-newsiteresolver.php +++ b/components/Blueprints/SiteResolver/class-newsiteresolver.php @@ -153,7 +153,7 @@ private static function is_wordpress_installed( Runtime $runtime, Tracker $progr $install_check = $runtime->eval_php_code_in_subprocess( <<<'PHP' $runtime->get_configuration()->get_target_site_root(), + 'DOCROOT' => $runtime->get_configuration()->get_target_site_root(), + 'WP_CORE_DIR' => $runtime->get_configuration()->get_wordpress_core_dir(), ), null, 5 diff --git a/components/Blueprints/Steps/class-activatepluginstep.php b/components/Blueprints/Steps/class-activatepluginstep.php index 7af6362c..9de21314 100644 --- a/components/Blueprints/Steps/class-activatepluginstep.php +++ b/components/Blueprints/Steps/class-activatepluginstep.php @@ -15,8 +15,8 @@ class ActivatePluginStep implements StepInterface { 'Administrator' ) )[0] ); diff --git a/components/Blueprints/Steps/class-activatethemestep.php b/components/Blueprints/Steps/class-activatethemestep.php index a6e5bc61..147709cc 100644 --- a/components/Blueprints/Steps/class-activatethemestep.php +++ b/components/Blueprints/Steps/class-activatethemestep.php @@ -16,7 +16,7 @@ class ActivateThemeStep implements StepInterface { 'Administrator' ) )[0] ); diff --git a/components/Blueprints/Steps/class-defineconstantsstep.php b/components/Blueprints/Steps/class-defineconstantsstep.php index 07a672e9..6f92199b 100644 --- a/components/Blueprints/Steps/class-defineconstantsstep.php +++ b/components/Blueprints/Steps/class-defineconstantsstep.php @@ -430,7 +430,7 @@ function find_first_token_index( $tokens, $type, $search = null ) { return null; } -$wp_config_path = getenv( "DOCROOT" ) . "/wp-config.php"; +$wp_config_path = getenv( "WP_CORE_DIR" ) . "/wp-config.php"; if ( ! file_exists( $wp_config_path ) ) { error_log( "Blueprint Error: wp-config.php file not found at " . $wp_config_path ); diff --git a/components/Blueprints/Steps/class-enablemultisitestep.php b/components/Blueprints/Steps/class-enablemultisitestep.php index 14e6ded2..42aab631 100644 --- a/components/Blueprints/Steps/class-enablemultisitestep.php +++ b/components/Blueprints/Steps/class-enablemultisitestep.php @@ -22,8 +22,8 @@ public function run( Runtime $runtime, Tracker $tracker ) { * See: https://github.com/wp-cli/core-command/blob/f157fb37dae1d13fe7318452f932917161e83e53/src/Core_Command.php#L505 */ -require_once getenv( 'DOCROOT' ) . '/wp-load.php'; -require_once getenv( 'DOCROOT' ) . '/wp-admin/includes/upgrade.php'; +require_once getenv( 'WP_CORE_DIR' ) . '/wp-load.php'; +require_once getenv( 'WP_CORE_DIR' ) . '/wp-admin/includes/upgrade.php'; // need to register the multisite tables manually for some reason foreach ( $wpdb->tables( 'ms_global' ) as $table => $prefixed_table ) { diff --git a/components/Blueprints/Steps/class-importcontentstep.php b/components/Blueprints/Steps/class-importcontentstep.php index 6c81d8c2..b0908856 100644 --- a/components/Blueprints/Steps/class-importcontentstep.php +++ b/components/Blueprints/Steps/class-importcontentstep.php @@ -151,7 +151,7 @@ private function importPosts( Runtime $runtime, $post ): void { $runtime->eval_php_code_in_subprocess( <<<'PHP' get_target_filesystem(); $wp_upload_dir = $runtime->eval_php_code_in_subprocess( 'eval_php_code_in_subprocess( <<<'CODE' 0, ); -require getenv( "DOCROOT" ) . '/wp-load.php'; +require getenv( "WP_CORE_DIR" ) . '/wp-load.php'; // Return early if there's no starter content. if ( ! get_theme_starter_content() ) { diff --git a/components/Blueprints/Steps/class-installpluginstep.php b/components/Blueprints/Steps/class-installpluginstep.php index 113ec612..6f9113c5 100644 --- a/components/Blueprints/Steps/class-installpluginstep.php +++ b/components/Blueprints/Steps/class-installpluginstep.php @@ -105,13 +105,13 @@ function ( $temp_dir ) use ( $runtime, $tracker, $plugin_data ) { <<<'PHP' eval_php_code_in_subprocess( <<<'CODE' eval_php_code_in_subprocess( 'output_file_content @@ -67,8 +67,8 @@ public function run( Runtime $runtime, Tracker $progress ) { $plugins_data = json_decode( $runtime->eval_php_code_in_subprocess( "eval_php_code_in_subprocess( "setCaption( 'Setting site options' ); $runtime->eval_php_code_in_subprocess( ' $value) { update_option($name, $value); diff --git a/components/Blueprints/Steps/scripts/import-content.php b/components/Blueprints/Steps/scripts/import-content.php index b98095fe..4bfeb25e 100644 --- a/components/Blueprints/Steps/scripts/import-content.php +++ b/components/Blueprints/Steps/scripts/import-content.php @@ -22,7 +22,7 @@ use function WordPress\Filesystem\wp_join_unix_paths; -require_once getenv( 'DOCROOT' ) . '/wp-load.php'; +require_once getenv( 'WP_CORE_DIR' ) . '/wp-load.php'; require_once getenv( 'DOCROOT' ) . '/php-toolkit.phar'; // Progress reporting interfaces and implementations. diff --git a/components/Blueprints/Tests/Unit/RunnerConfigurationTest.php b/components/Blueprints/Tests/Unit/RunnerConfigurationTest.php new file mode 100644 index 00000000..dd023050 --- /dev/null +++ b/components/Blueprints/Tests/Unit/RunnerConfigurationTest.php @@ -0,0 +1,45 @@ +set_target_site_root( '/srv/htdocs' ); + + $this->assertSame( '/srv/htdocs', $config->get_wordpress_core_dir() ); + } + + /** + * When a WordPress core dir is explicitly set, it takes precedence + * over the target site root. + */ + public function test_wordpress_core_dir_can_be_set_independently() { + $config = new RunnerConfiguration(); + $config->set_target_site_root( '/srv/htdocs' ); + $config->set_wordpress_core_dir( '/srv/htdocs/__wp__' ); + + $this->assertSame( '/srv/htdocs', $config->get_target_site_root() ); + $this->assertSame( '/srv/htdocs/__wp__', $config->get_wordpress_core_dir() ); + } + + /** + * Setting WordPress core dir to null resets to the default behavior + * (falling back to the site root). + */ + public function test_wordpress_core_dir_reset_to_null_falls_back_to_site_root() { + $config = new RunnerConfiguration(); + $config->set_target_site_root( '/srv/htdocs' ); + $config->set_wordpress_core_dir( '/srv/htdocs/__wp__' ); + $config->set_wordpress_core_dir( null ); + + $this->assertSame( '/srv/htdocs', $config->get_wordpress_core_dir() ); + } +} diff --git a/components/Blueprints/Tests/Unit/SiteResolver/DetectWordPressCoreDirTest.php b/components/Blueprints/Tests/Unit/SiteResolver/DetectWordPressCoreDirTest.php new file mode 100644 index 00000000..c06a32eb --- /dev/null +++ b/components/Blueprints/Tests/Unit/SiteResolver/DetectWordPressCoreDirTest.php @@ -0,0 +1,126 @@ +temp_dir = wp_unix_sys_get_temp_dir() . '/wp_core_detect_' . uniqid(); + mkdir( $this->temp_dir, 0755, true ); + } + + protected function tearDown(): void { + $this->remove_directory( $this->temp_dir ); + } + + /** + * Standard layout: wp-load.php is in the web root. + */ + public function test_detects_standard_layout() { + touch( $this->temp_dir . '/wp-load.php' ); + + $result = ExistingSiteResolver::detect_wordpress_core_dir( $this->temp_dir ); + + $this->assertSame( $this->temp_dir, $result ); + } + + /** + * WP Cloud layout: wp-load.php lives in __wp__/ subdirectory. + */ + public function test_detects_wp_cloud_layout_with___wp___subdirectory() { + // Web root has wp-content but not wp-load.php. + mkdir( $this->temp_dir . '/wp-content', 0755, true ); + + // WordPress core is in __wp__/. + $wp_core = $this->temp_dir . '/__wp__'; + mkdir( $wp_core, 0755, true ); + touch( $wp_core . '/wp-load.php' ); + + $result = ExistingSiteResolver::detect_wordpress_core_dir( $this->temp_dir ); + + $this->assertSame( $wp_core, $result ); + } + + /** + * Custom subdirectory layout: wp-load.php in an arbitrary subdirectory. + */ + public function test_detects_custom_subdirectory_layout() { + $wp_core = $this->temp_dir . '/core'; + mkdir( $wp_core, 0755, true ); + touch( $wp_core . '/wp-load.php' ); + + $result = ExistingSiteResolver::detect_wordpress_core_dir( $this->temp_dir ); + + $this->assertSame( $wp_core, $result ); + } + + /** + * No WordPress installation: returns null when wp-load.php is not + * found anywhere. + */ + public function test_returns_null_when_no_wordpress_found() { + // Empty directory, no wp-load.php anywhere. + $result = ExistingSiteResolver::detect_wordpress_core_dir( $this->temp_dir ); + + $this->assertNull( $result ); + } + + /** + * Deeply nested wp-load.php should NOT be detected. Only the + * web root and its immediate subdirectories are searched. + */ + public function test_does_not_detect_deeply_nested_wp_load() { + $deep = $this->temp_dir . '/a/b/c'; + mkdir( $deep, 0755, true ); + touch( $deep . '/wp-load.php' ); + + $result = ExistingSiteResolver::detect_wordpress_core_dir( $this->temp_dir ); + + $this->assertNull( $result ); + } + + /** + * Non-existent directory: returns null gracefully. + */ + public function test_returns_null_for_nonexistent_directory() { + $result = ExistingSiteResolver::detect_wordpress_core_dir( '/nonexistent/path/' . uniqid() ); + + $this->assertNull( $result ); + } + + private function remove_directory( string $dir ): void { + if ( ! is_dir( $dir ) ) { + return; + } + $entries = scandir( $dir ); + foreach ( $entries as $entry ) { + if ( '.' === $entry || '..' === $entry ) { + continue; + } + $path = $dir . '/' . $entry; + if ( is_dir( $path ) ) { + $this->remove_directory( $path ); + } else { + unlink( $path ); + } + } + rmdir( $dir ); + } +} diff --git a/components/Blueprints/Tests/Unit/Steps/DefineConstantsStepTest.php b/components/Blueprints/Tests/Unit/Steps/DefineConstantsStepTest.php index bee71d3e..b61d36ce 100644 --- a/components/Blueprints/Tests/Unit/Steps/DefineConstantsStepTest.php +++ b/components/Blueprints/Tests/Unit/Steps/DefineConstantsStepTest.php @@ -245,7 +245,7 @@ private function assertWordPressConstants( array $expected_constants ) { <<<'PHP' runtime->eval_php_code_in_subprocess( <<<'PHP' runtime->eval_php_code_in_subprocess( <<<'PHP' runtime->eval_php_code_in_subprocess( <<<'PHP' runtime->eval_php_code_in_subprocess( <<<'PHP' runtime->eval_php_code_in_subprocess( <<<'PHP' runtime->eval_php_code_in_subprocess( <<<'PHP' runtime->eval_php_code_in_subprocess( <<<'PHP' runtime->eval_php_code_in_subprocess( <<<'PHP' runtime->eval_php_code_in_subprocess( <<<'PHP' runtime->eval_php_code_in_subprocess( <<<'PHP' runtime->eval_php_code_in_subprocess( <<<'PHP' runtime->eval_php_code_in_subprocess( <<<'PHP' 'script.php', 'content' => <<runtime->eval_php_code_in_subprocess( <<<'PHP' runtime->eval_php_code_in_subprocess( <<<'PHP' get_var("SHOW TABLES LIKE '$table_name'"); @@ -57,7 +57,7 @@ public function testRunSQLQueryWithInserts() { $result = $this->runtime->eval_php_code_in_subprocess( <<<'PHP' get_var("SELECT COUNT(*) FROM test_table"); $rows = $wpdb->get_results("SELECT * FROM test_table ORDER BY id", ARRAY_A); @@ -94,7 +94,7 @@ public function testRunSQLQueryModifyingWordPressOptions() { $option_value = $this->runtime->eval_php_code_in_subprocess( <<<'PHP' runtime->eval_php_code_in_subprocess( <<<'PHP' get_var("SELECT value FROM test_table_1 LIMIT 1"); @@ -157,7 +157,7 @@ public function testHandleSQLErrors() { $table_exists = $this->runtime->eval_php_code_in_subprocess( <<<'PHP' get_var("SHOW TABLES LIKE '$table_name'"); diff --git a/components/Blueprints/Tests/Unit/Steps/SetSiteOptionsStepTest.php b/components/Blueprints/Tests/Unit/Steps/SetSiteOptionsStepTest.php index 8693123a..f0affc68 100644 --- a/components/Blueprints/Tests/Unit/Steps/SetSiteOptionsStepTest.php +++ b/components/Blueprints/Tests/Unit/Steps/SetSiteOptionsStepTest.php @@ -16,7 +16,7 @@ private function assertWordPressOptions( array $expected_options ) { $result = $this->runtime->eval_php_code_in_subprocess( <<<'PHP' runtime->eval_php_code_in_subprocess( <<<'PHP' 'script.php', 'content' => <<<'PHP' query('DELETE FROM wp_posts WHERE id > 0'); $GLOBALS['@pdo']->query("UPDATE SQLITE_SEQUENCE SET SEQ=0 WHERE NAME='wp_posts'"); @@ -448,7 +448,7 @@ public function upgrade( array $validated_v1_blueprint ): array { 'filename' => 'script.php', 'content' => <<<'PHP' $value) { update_user_meta(getenv("USER_ID"), $name, $value); diff --git a/components/Blueprints/bin/blueprint.php b/components/Blueprints/bin/blueprint.php index 7a6c43a5..16a9ac8a 100644 --- a/components/Blueprints/bin/blueprint.php +++ b/components/Blueprints/bin/blueprint.php @@ -269,6 +269,7 @@ function createProgressReporter(): ProgressReporter { array( 'site-url' => array( 'u', true, null, 'Public site URL (https://example.com)' ), 'site-path' => array( null, true, null, 'Target directory with WordPress install context)' ), + 'wp-core-path' => array( null, true, null, 'WordPress core directory (if different from site-path, e.g. on WP Cloud)' ), 'execution-context' => array( 'x', true, null, 'Source directory with Blueprint context files' ), 'mode' => array( 'm', true, Runner::EXECUTION_MODE_CREATE_NEW_SITE, sprintf( 'Execution mode (%s|%s)', Runner::EXECUTION_MODE_CREATE_NEW_SITE, Runner::EXECUTION_MODE_APPLY_TO_EXISTING_SITE ) ), 'db-engine' => array( 'd', true, 'mysql', 'Database engine (mysql|sqlite)' ), @@ -475,6 +476,15 @@ function cliArgsToRunnerConfiguration( array $positional_args, array $options ): $config->set_target_site_root( $absolute_target_site_root ); $config->set_target_site_url( $options['site-url'] ); + // Set WordPress core directory if explicitly provided. + if ( ! empty( $options['wp-core-path'] ) ) { + $absolute_wp_core_path = realpath( $options['wp-core-path'] ); + if ( false === $absolute_wp_core_path || ! is_dir( $absolute_wp_core_path ) ) { + throw new InvalidArgumentException( "The --wp-core-path path does not exist: {$options['wp-core-path']}" ); + } + $config->set_wordpress_core_dir( $absolute_wp_core_path ); + } + // Set database engine. if ( ! empty( $options['db-engine'] ) ) { $config->set_database_engine( $options['db-engine'] ); diff --git a/components/Blueprints/class-runner.php b/components/Blueprints/class-runner.php index 5678c849..1daa3495 100644 --- a/components/Blueprints/class-runner.php +++ b/components/Blueprints/class-runner.php @@ -839,7 +839,7 @@ private function create_step_object( string $step_type, array $data ) { } $code = 'root_dir; } + /** + * Sets the WordPress core directory path. This is where wp-load.php, + * wp-admin/, and wp-includes/ live. When null, it defaults to the + * target site root. + */ + public function set_wordpress_core_dir( ?string $d ): self { + $this->wordpress_core_dir = $d; + + return $this; + } + + /** + * Gets the WordPress core directory path. Falls back to the target + * site root when not explicitly set. + */ + public function get_wordpress_core_dir(): string { + return $this->wordpress_core_dir ?? $this->root_dir; + } + public function set_target_site_url( string $u ): self { $this->site_url = $u; diff --git a/components/Blueprints/class-runtime.php b/components/Blueprints/class-runtime.php index 8438661b..55e97be6 100644 --- a/components/Blueprints/class-runtime.php +++ b/components/Blueprints/class-runtime.php @@ -177,7 +177,10 @@ public function create_temporary_file( ?string $suffix = null ): string { * * * append_output( $output ): A function that appends a given string to the output file. Useful for * separating the returned structured data from PHP warnings and echos. - * * DOCROOT environment variable: The path to the WordPress root directory. + * * DOCROOT environment variable: The path to the web root directory (document root). + * * WP_CORE_DIR environment variable: The path to the WordPress core directory (where wp-load.php lives). + * On standard installs this equals DOCROOT. On WP Cloud sites + * the core may live in a subdirectory like __wp__/. * * OUTPUT_FILE environment variable: The path to a file where the output of the code will be appended. * * @TODO: Useful error messages on process failure. Right now we get this mouthful error message: @@ -264,7 +267,8 @@ function ( $script_path ) use ( $code, $env, $input, $timeout ) { array_merge( array( 'DOCROOT' => $this->configuration->get_target_site_root(), - 'OUTPUT_FILE' => $output_path, + 'WP_CORE_DIR' => $this->configuration->get_wordpress_core_dir(), + 'OUTPUT_FILE' => $output_path, ), $env ?? array() ), From f5c339cd357d92fb93ad5c39627f37a69c87071a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 13 Feb 2026 15:09:34 +0100 Subject: [PATCH 2/3] Fix PHPCS array alignment in Runtime env vars --- components/Blueprints/class-runtime.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/Blueprints/class-runtime.php b/components/Blueprints/class-runtime.php index 55e97be6..f03e11a6 100644 --- a/components/Blueprints/class-runtime.php +++ b/components/Blueprints/class-runtime.php @@ -268,7 +268,7 @@ function ( $script_path ) use ( $code, $env, $input, $timeout ) { array( 'DOCROOT' => $this->configuration->get_target_site_root(), 'WP_CORE_DIR' => $this->configuration->get_wordpress_core_dir(), - 'OUTPUT_FILE' => $output_path, + 'OUTPUT_FILE' => $output_path, ), $env ?? array() ), From c1ecdbe959cc1fd01f2815bdc20c119bcda1b8b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 13 Feb 2026 15:32:09 +0100 Subject: [PATCH 3/3] Remove host-specific references from comments and tests Keep the language generic since this is a package for all hosts. --- .../SiteResolver/class-existingsiteresolver.php | 15 +++++++-------- .../Tests/Unit/RunnerConfigurationTest.php | 6 +++--- .../SiteResolver/DetectWordPressCoreDirTest.php | 16 ++++++++-------- components/Blueprints/bin/blueprint.php | 2 +- .../Blueprints/class-runnerconfiguration.php | 4 ++-- components/Blueprints/class-runtime.php | 4 ++-- 6 files changed, 23 insertions(+), 24 deletions(-) diff --git a/components/Blueprints/SiteResolver/class-existingsiteresolver.php b/components/Blueprints/SiteResolver/class-existingsiteresolver.php index ebe9c4dd..738f8515 100644 --- a/components/Blueprints/SiteResolver/class-existingsiteresolver.php +++ b/components/Blueprints/SiteResolver/class-existingsiteresolver.php @@ -25,9 +25,9 @@ public static function resolve( Runtime $runtime, Tracker $progress, ?VersionCon // 1. Verify it's a valid WordPress installation. $progress['verify_installation']->setCaption( 'Verifying WordPress installation' ); - // Auto-detect the WordPress core directory. On WP Cloud sites, the - // WordPress core files (wp-load.php, wp-admin, wp-includes) live in - // a subdirectory like __wp__/ while wp-content stays in the web root. + // Auto-detect the WordPress core directory. Some hosting setups place + // the WordPress core files (wp-load.php, wp-admin, wp-includes) in a + // subdirectory while wp-content stays in the web root. if ( ! $target_fs->exists( 'wp-load.php' ) ) { $detected_core_dir = self::detect_wordpress_core_dir( $config->get_target_site_root() ); if ( null !== $detected_core_dir ) { @@ -150,9 +150,9 @@ public static function resolve( Runtime $runtime, Tracker $progress, ?VersionCon } /** - * Scans the web root for a WordPress core directory. On WP Cloud and - * similar setups the core files live in a subdirectory (e.g. __wp__/) - * while wp-content/ stays in the web root. + * Scans the web root for a WordPress core directory. Some hosting + * setups place the core files in a subdirectory while wp-content/ + * stays in the web root. * * @param string $web_root Absolute path to the web root. * @@ -166,8 +166,7 @@ public static function detect_wordpress_core_dir( string $web_root ): ?string { } // Scan immediate subdirectories for wp-load.php. This covers the - // WP Cloud convention (__wp__/) as well as any other single-level - // subdirectory layout. + // Check immediate subdirectories for any single-level split layout. $entries = @scandir( $web_root ); if ( false === $entries ) { return null; diff --git a/components/Blueprints/Tests/Unit/RunnerConfigurationTest.php b/components/Blueprints/Tests/Unit/RunnerConfigurationTest.php index dd023050..3c50cd1e 100644 --- a/components/Blueprints/Tests/Unit/RunnerConfigurationTest.php +++ b/components/Blueprints/Tests/Unit/RunnerConfigurationTest.php @@ -24,10 +24,10 @@ public function test_wordpress_core_dir_defaults_to_site_root() { public function test_wordpress_core_dir_can_be_set_independently() { $config = new RunnerConfiguration(); $config->set_target_site_root( '/srv/htdocs' ); - $config->set_wordpress_core_dir( '/srv/htdocs/__wp__' ); + $config->set_wordpress_core_dir( '/srv/htdocs/wp' ); $this->assertSame( '/srv/htdocs', $config->get_target_site_root() ); - $this->assertSame( '/srv/htdocs/__wp__', $config->get_wordpress_core_dir() ); + $this->assertSame( '/srv/htdocs/wp', $config->get_wordpress_core_dir() ); } /** @@ -37,7 +37,7 @@ public function test_wordpress_core_dir_can_be_set_independently() { public function test_wordpress_core_dir_reset_to_null_falls_back_to_site_root() { $config = new RunnerConfiguration(); $config->set_target_site_root( '/srv/htdocs' ); - $config->set_wordpress_core_dir( '/srv/htdocs/__wp__' ); + $config->set_wordpress_core_dir( '/srv/htdocs/wp' ); $config->set_wordpress_core_dir( null ); $this->assertSame( '/srv/htdocs', $config->get_wordpress_core_dir() ); diff --git a/components/Blueprints/Tests/Unit/SiteResolver/DetectWordPressCoreDirTest.php b/components/Blueprints/Tests/Unit/SiteResolver/DetectWordPressCoreDirTest.php index c06a32eb..01a69ecb 100644 --- a/components/Blueprints/Tests/Unit/SiteResolver/DetectWordPressCoreDirTest.php +++ b/components/Blueprints/Tests/Unit/SiteResolver/DetectWordPressCoreDirTest.php @@ -10,10 +10,10 @@ /** * Tests for ExistingSiteResolver::detect_wordpress_core_dir(). * - * On WP Cloud sites, the WordPress core files (wp-load.php, wp-admin/, - * wp-includes/) live in a subdirectory like __wp__/ while wp-content/ - * stays in the web root. The detection method must find wp-load.php - * in both standard and split layouts. + * Some hosting setups place the WordPress core files (wp-load.php, + * wp-admin/, wp-includes/) in a subdirectory while wp-content/ stays + * in the web root. The detection method must find wp-load.php in both + * standard and split layouts. */ class DetectWordPressCoreDirTest extends TestCase { /** @@ -42,14 +42,14 @@ public function test_detects_standard_layout() { } /** - * WP Cloud layout: wp-load.php lives in __wp__/ subdirectory. + * Split layout: wp-load.php lives in a subdirectory of the web root. */ - public function test_detects_wp_cloud_layout_with___wp___subdirectory() { + public function test_detects_split_layout_with_subdirectory() { // Web root has wp-content but not wp-load.php. mkdir( $this->temp_dir . '/wp-content', 0755, true ); - // WordPress core is in __wp__/. - $wp_core = $this->temp_dir . '/__wp__'; + // WordPress core is in a subdirectory. + $wp_core = $this->temp_dir . '/wp'; mkdir( $wp_core, 0755, true ); touch( $wp_core . '/wp-load.php' ); diff --git a/components/Blueprints/bin/blueprint.php b/components/Blueprints/bin/blueprint.php index 16a9ac8a..05abf0fa 100644 --- a/components/Blueprints/bin/blueprint.php +++ b/components/Blueprints/bin/blueprint.php @@ -269,7 +269,7 @@ function createProgressReporter(): ProgressReporter { array( 'site-url' => array( 'u', true, null, 'Public site URL (https://example.com)' ), 'site-path' => array( null, true, null, 'Target directory with WordPress install context)' ), - 'wp-core-path' => array( null, true, null, 'WordPress core directory (if different from site-path, e.g. on WP Cloud)' ), + 'wp-core-path' => array( null, true, null, 'WordPress core directory (if different from site-path)' ), 'execution-context' => array( 'x', true, null, 'Source directory with Blueprint context files' ), 'mode' => array( 'm', true, Runner::EXECUTION_MODE_CREATE_NEW_SITE, sprintf( 'Execution mode (%s|%s)', Runner::EXECUTION_MODE_CREATE_NEW_SITE, Runner::EXECUTION_MODE_APPLY_TO_EXISTING_SITE ) ), 'db-engine' => array( 'd', true, 'mysql', 'Database engine (mysql|sqlite)' ), diff --git a/components/Blueprints/class-runnerconfiguration.php b/components/Blueprints/class-runnerconfiguration.php index 886b9e2d..c200555f 100644 --- a/components/Blueprints/class-runnerconfiguration.php +++ b/components/Blueprints/class-runnerconfiguration.php @@ -31,8 +31,8 @@ class RunnerConfiguration { /** * The path to the WordPress core directory, containing wp-load.php, * wp-admin/, and wp-includes/. On standard installs this is the same - * as the site root. On WP Cloud sites the core lives in a subdirectory - * like __wp__/ while wp-content/ stays in the web root. + * as the site root. Some hosting setups place the core in a subdirectory + * while wp-content/ stays in the web root. * * @var string|null */ diff --git a/components/Blueprints/class-runtime.php b/components/Blueprints/class-runtime.php index f03e11a6..712c54d5 100644 --- a/components/Blueprints/class-runtime.php +++ b/components/Blueprints/class-runtime.php @@ -179,8 +179,8 @@ public function create_temporary_file( ?string $suffix = null ): string { * separating the returned structured data from PHP warnings and echos. * * DOCROOT environment variable: The path to the web root directory (document root). * * WP_CORE_DIR environment variable: The path to the WordPress core directory (where wp-load.php lives). - * On standard installs this equals DOCROOT. On WP Cloud sites - * the core may live in a subdirectory like __wp__/. + * On standard installs this equals DOCROOT. Some hosts place + * the core in a subdirectory separate from the web root. * * OUTPUT_FILE environment variable: The path to a file where the output of the code will be appended. * * @TODO: Useful error messages on process failure. Right now we get this mouthful error message: