From 7d1211535f810997bdc793828bcf3deceb45a962 Mon Sep 17 00:00:00 2001 From: Alain Schlesser Date: Mon, 2 Nov 2026 09:44:54 +0100 Subject: [PATCH 1/2] Keep the shared opcode cache from changing what the code does The optimizer evaluates calls like function_exists() at compile time, so code cached by a process with the default settings skipped the ini_set() fallback of WP-CLI in a process that disables it. Cache the code without optimizing it. Behat and Composer restart without Xdebug, which clears PHP_INI_SCAN_DIR, so the WP-CLI processes lost the scan directory of the system, and the extensions configured there. Take the original directories from the restart settings instead. Also leave out SQLITE_ENABLE_UPDATE_DELETE_LIMIT, which needs a parser the amalgamation does not have. Fixes #416. Co-Authored-By: Claude Opus 5.5 --- features/testing.feature | 11 +++++++++++ src/Context/FeatureContext.php | 14 ++++++++++++++ utils/docker/Dockerfile | 2 +- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/features/testing.feature b/features/testing.feature index bbab3a22..f721edc8 100644 --- a/features/testing.feature +++ b/features/testing.feature @@ -208,3 +208,14 @@ Feature: Test that WP-CLI loads. C:\Windows """ + + @skip-windows + Scenario: Run WP-CLI with a disabled function after it ran without + Given an empty directory + When I run `wp cli version` + And I try `{INVOKE_WP_CLI_WITH_PHP_ARGS--ddisable_functions=ini_set} cli info` + Then the return code should be 0 + And STDOUT should contain: + """ + PHP binary + """ diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 5f1c54a0..3c93e40a 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -600,16 +600,30 @@ private static function get_php_tuning_env_variables(): array { $excluded = array_unique( array_filter( [ $temp_dir, realpath( $temp_dir ) ] ) ); file_put_contents( "{$config_dir}/opcache-blacklist.txt", implode( "/\n", $excluded ) . "/\n" ); + // The optimizer bakes in what it finds at compile time, such as whether a function exists, + // which differs between processes with other settings, such as disabled functions. $ini = [ 'opcache.enable_cli=1', "opcache.file_cache={$cache_dir}", 'opcache.file_cache_only=1', + 'opcache.optimization_level=0', "opcache.blacklist_filename={$config_dir}/opcache-blacklist.txt", ]; file_put_contents( "{$config_dir}/wp-cli-tests.ini", implode( "\n", $ini ) . "\n" ); // Add to the directories PHP scans for configuration files rather than replacing them. $scan_dirs = getenv( 'PHP_INI_SCAN_DIR' ); + + // Behat and Composer restart without Xdebug through composer/xdebug-handler, which clears + // the directories, and keeps the original ones in its settings, with "*" if there were none. + $restart_settings = getenv( 'XDEBUG_HANDLER_SETTINGS' ); + if ( '' === $scan_dirs && is_string( $restart_settings ) ) { + $settings = explode( '|', $restart_settings ); + if ( isset( $settings[2] ) ) { + $scan_dirs = '*' === $settings[2] ? false : $settings[2]; + } + } + if ( false === $scan_dirs ) { $scan_dirs = PHP_CONFIG_FILE_SCAN_DIR; } diff --git a/utils/docker/Dockerfile b/utils/docker/Dockerfile index 749e835d..2d3c33e3 100644 --- a/utils/docker/Dockerfile +++ b/utils/docker/Dockerfile @@ -40,7 +40,7 @@ RUN if dpkg --compare-versions "$(dpkg-query -W -f '${Version}' libsqlite3-0)" l && [ "$(openssl dgst -sha3-256 -r sqlite.tar.gz | cut -d' ' -f1)" = "${SQLITE_SHA3_256}" ] \ && tar xzf sqlite.tar.gz \ && cd "sqlite-autoconf-${SQLITE_VERSION}" \ - && CFLAGS="-O2 -DSQLITE_ALLOW_ROWID_IN_VIEW -DSQLITE_ENABLE_COLUMN_METADATA=1 -DSQLITE_ENABLE_DBPAGE_VTAB -DSQLITE_ENABLE_DBSTAT_VTAB -DSQLITE_ENABLE_FTS3 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_FTS3_TOKENIZER -DSQLITE_ENABLE_FTS4 -DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_LOAD_EXTENSION -DSQLITE_ENABLE_MATH_FUNCTIONS -DSQLITE_ENABLE_PREUPDATE_HOOK -DSQLITE_ENABLE_RTREE -DSQLITE_ENABLE_SESSION -DSQLITE_ENABLE_STMTVTAB -DSQLITE_ENABLE_UNLOCK_NOTIFY -DSQLITE_ENABLE_UPDATE_DELETE_LIMIT -DSQLITE_LIKE_DOESNT_MATCH_BLOBS -DSQLITE_MAX_SCHEMA_RETRY=25 -DSQLITE_SECURE_DELETE -DSQLITE_SOUNDEX -DSQLITE_USE_URI=1" \ + && CFLAGS="-O2 -DSQLITE_ALLOW_ROWID_IN_VIEW -DSQLITE_ENABLE_COLUMN_METADATA=1 -DSQLITE_ENABLE_DBPAGE_VTAB -DSQLITE_ENABLE_DBSTAT_VTAB -DSQLITE_ENABLE_FTS3 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_FTS3_TOKENIZER -DSQLITE_ENABLE_FTS4 -DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_LOAD_EXTENSION -DSQLITE_ENABLE_MATH_FUNCTIONS -DSQLITE_ENABLE_PREUPDATE_HOOK -DSQLITE_ENABLE_RTREE -DSQLITE_ENABLE_SESSION -DSQLITE_ENABLE_STMTVTAB -DSQLITE_ENABLE_UNLOCK_NOTIFY -DSQLITE_LIKE_DOESNT_MATCH_BLOBS -DSQLITE_MAX_SCHEMA_RETRY=25 -DSQLITE_SECURE_DELETE -DSQLITE_SOUNDEX -DSQLITE_USE_URI=1" \ ./configure --prefix=/usr/local --disable-static > /dev/null \ && make -j"$(nproc)" > /dev/null \ && make install > /dev/null \ From 87d1bf15e828f20bf809c3f9ece44c9706527838 Mon Sep 17 00:00:00 2001 From: Alain Schlesser Date: Mon, 2 Nov 2026 09:53:16 +0100 Subject: [PATCH 2/2] Name the cache after its settings and restore PHPRC as well A cache directory written by 5.4.0 holds optimized code, which opcache keeps using, and an older suite running at the same time would rewrite the configuration file. Name both directories after the settings, so that each set has its own. After a restart without Xdebug, PHPRC points to a configuration file that holds the scanned files already, which loaded them twice when it reached the WP-CLI processes. Co-Authored-By: Claude Opus 5.5 --- src/Context/FeatureContext.php | 39 ++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 3c93e40a..1692abc6 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -508,7 +508,12 @@ private static function get_process_env_variables(): array { $env['WP_CLI_REQUIRE'] = $updated; } - $env = array_merge( $env, self::get_php_tuning_env_variables() ); + $env = array_filter( + array_merge( $env, self::get_php_tuning_env_variables() ), + static function ( $value ) { + return null !== $value; + } + ); $config_path = getenv( 'WP_CLI_CONFIG_PATH' ); if ( false !== $config_path ) { @@ -569,7 +574,7 @@ private static function get_process_env_variables(): array { * * Set WP_CLI_TEST_PHP_TUNING=off to launch the processes with the PHP configuration as it is. * - * @return array + * @return array Null for variables to leave out. */ private static function get_php_tuning_env_variables(): array { if ( 'off' === getenv( 'WP_CLI_TEST_PHP_TUNING' ) || self::running_with_code_coverage() ) { @@ -587,9 +592,13 @@ private static function get_php_tuning_env_variables(): array { return $env; } + // The directories are named after the settings, so that a cache written with other settings, + // such as by an older version of this package, is never used, nor its configuration file. + $settings = [ 'opcache.enable_cli=1', 'opcache.file_cache_only=1', 'opcache.optimization_level=0' ]; + $key = substr( md5( implode( "\n", $settings ) ), 0, 8 ); $temp_dir = rtrim( sys_get_temp_dir(), '/' ); - $config_dir = "{$temp_dir}/wp-cli-test-php-config"; - $cache_dir = "{$temp_dir}/wp-cli-test-opcache"; + $config_dir = "{$temp_dir}/wp-cli-test-php-config-{$key}"; + $cache_dir = "{$temp_dir}/wp-cli-test-opcache-{$key}"; foreach ( [ $config_dir, $cache_dir ] as $dir ) { if ( ! is_dir( $dir ) && ! @mkdir( $dir, 0777, true ) && ! is_dir( $dir ) ) { @@ -602,13 +611,13 @@ private static function get_php_tuning_env_variables(): array { // The optimizer bakes in what it finds at compile time, such as whether a function exists, // which differs between processes with other settings, such as disabled functions. - $ini = [ - 'opcache.enable_cli=1', - "opcache.file_cache={$cache_dir}", - 'opcache.file_cache_only=1', - 'opcache.optimization_level=0', - "opcache.blacklist_filename={$config_dir}/opcache-blacklist.txt", - ]; + $ini = array_merge( + $settings, + [ + "opcache.file_cache={$cache_dir}", + "opcache.blacklist_filename={$config_dir}/opcache-blacklist.txt", + ] + ); file_put_contents( "{$config_dir}/wp-cli-tests.ini", implode( "\n", $ini ) . "\n" ); // Add to the directories PHP scans for configuration files rather than replacing them. @@ -616,11 +625,13 @@ private static function get_php_tuning_env_variables(): array { // Behat and Composer restart without Xdebug through composer/xdebug-handler, which clears // the directories, and keeps the original ones in its settings, with "*" if there were none. + // The configuration file it points PHPRC to holds the scanned files already, so restore that too. $restart_settings = getenv( 'XDEBUG_HANDLER_SETTINGS' ); if ( '' === $scan_dirs && is_string( $restart_settings ) ) { - $settings = explode( '|', $restart_settings ); - if ( isset( $settings[2] ) ) { - $scan_dirs = '*' === $settings[2] ? false : $settings[2]; + $restart = explode( '|', $restart_settings ); + if ( isset( $restart[2], $restart[3] ) ) { + $scan_dirs = '*' === $restart[2] ? false : $restart[2]; + $env['PHPRC'] = '*' === $restart[3] ? null : $restart[3]; } }