aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock5
-rw-r--r--Cargo.toml1
-rw-r--r--LICENSE.md2
-rw-r--r--crates/shirabe-php-rpc/LICENSE (renamed from crates/shirabe-xdebug-handler/LICENSE)0
-rw-r--r--crates/shirabe-php-rpc/php/worker.php5
-rw-r--r--crates/shirabe-php-rpc/src/lib.rs62
-rw-r--r--crates/shirabe-php-rpc/src/xdebug.rs35
-rw-r--r--crates/shirabe-xdebug-handler/Cargo.toml11
-rw-r--r--crates/shirabe-xdebug-handler/src/lib.rs3
-rw-r--r--crates/shirabe-xdebug-handler/src/xdebug_handler.rs32
-rw-r--r--crates/shirabe/Cargo.toml1
-rw-r--r--crates/shirabe/src/command/diagnose_command.rs3
-rw-r--r--crates/shirabe/src/console/application.rs3
-rw-r--r--crates/shirabe/src/main.rs10
-rw-r--r--crates/shirabe/src/repository/platform_repository.rs20
-rw-r--r--crates/shirabe/src/util/ini_helper.rs8
-rw-r--r--crates/shirabe/tests/util/ini_helper_test.rs109
-rw-r--r--docs/dev/php-rpc.md3
-rw-r--r--docs/dev/xdebug.md28
-rw-r--r--docs/known-incompatibilities.md7
20 files changed, 155 insertions, 193 deletions
diff --git a/Cargo.lock b/Cargo.lock
index c165d98c..a0057ec7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2110,7 +2110,6 @@ dependencies = [
"shirabe-symfony-filesystem",
"shirabe-symfony-finder",
"shirabe-symfony-process",
- "shirabe-xdebug-handler",
"tempfile",
"tokio",
"tracing",
@@ -2273,10 +2272,6 @@ dependencies = [
]
[[package]]
-name = "shirabe-xdebug-handler"
-version = "0.0.1"
-
-[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index d471397c..a681bacf 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -27,7 +27,6 @@ shirabe-symfony-filesystem = { path = "crates/shirabe-symfony-filesystem" }
shirabe-symfony-finder = { path = "crates/shirabe-symfony-finder" }
shirabe-symfony-process = { path = "crates/shirabe-symfony-process" }
shirabe-symfony-string = { path = "crates/shirabe-symfony-string" }
-shirabe-xdebug-handler = { path = "crates/shirabe-xdebug-handler" }
anyhow = "1.0.102"
async-trait = "0.1.89"
base64 = "0.22.1"
diff --git a/LICENSE.md b/LICENSE.md
index b4c4ea4a..12666b3f 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -31,6 +31,7 @@ license of the package it is ported from:
| [`shirabe-class-map-generator`](crates/shirabe-class-map-generator/LICENSE) | composer/class-map-generator |
| [`shirabe-metadata-minifier`](crates/shirabe-metadata-minifier/LICENSE) | composer/metadata-minifier |
| [`shirabe-pcre`](crates/shirabe-pcre/LICENSE) | composer/pcre |
+| [`shirabe-php-rpc`](crates/shirabe-php-rpc/LICENSE) | composer/xdebug-handler |
| [`shirabe-seld-json-lint`](crates/shirabe-seld-json-lint/LICENSE) | seld/jsonlint |
| [`shirabe-seld-signal`](crates/shirabe-seld-signal/LICENSE) | seld/signal-handler |
| [`shirabe-semver`](crates/shirabe-semver/LICENSE) | composer/semver |
@@ -40,7 +41,6 @@ license of the package it is ported from:
| [`shirabe-symfony-finder`](crates/shirabe-symfony-finder/LICENSE) | symfony/finder |
| [`shirabe-symfony-process`](crates/shirabe-symfony-process/LICENSE) | symfony/process |
| [`shirabe-symfony-string`](crates/shirabe-symfony-string/LICENSE) | symfony/string |
-| [`shirabe-xdebug-handler`](crates/shirabe-xdebug-handler/LICENSE) | composer/xdebug-handler |
## PHP
diff --git a/crates/shirabe-xdebug-handler/LICENSE b/crates/shirabe-php-rpc/LICENSE
index 963618a1..963618a1 100644
--- a/crates/shirabe-xdebug-handler/LICENSE
+++ b/crates/shirabe-php-rpc/LICENSE
diff --git a/crates/shirabe-php-rpc/php/worker.php b/crates/shirabe-php-rpc/php/worker.php
index 578c30d0..5946a52f 100644
--- a/crates/shirabe-php-rpc/php/worker.php
+++ b/crates/shirabe-php-rpc/php/worker.php
@@ -526,8 +526,8 @@ final class ShirabePlatformRuntime
}
}
-// Port of Composer\XdebugHandler\XdebugHandler::setXdebugDetails(), which the diagnose payload
-// reports as `xdebug_active`.
+// Port of Composer\XdebugHandler\XdebugHandler::setXdebugDetails(), which the `xdebug_active`
+// query and the diagnose payload both report.
$xdebug_active = static function (): bool {
if (!extension_loaded('xdebug')) {
return false;
@@ -562,6 +562,7 @@ $xdebug_active = static function (): bool {
ShirabeRpcRuntime::$dispatch = [
'constant' => static fn($args) => defined($args[0]) ? constant($args[0]) : null,
+ 'xdebug_active' => static fn($args) => $xdebug_active(),
'get_all_ini_files' => static function ($args) {
$paths = [(string) php_ini_loaded_file()];
$scanned = php_ini_scanned_files();
diff --git a/crates/shirabe-php-rpc/src/lib.rs b/crates/shirabe-php-rpc/src/lib.rs
index a4742664..1b165cfa 100644
--- a/crates/shirabe-php-rpc/src/lib.rs
+++ b/crates/shirabe-php-rpc/src/lib.rs
@@ -3,6 +3,7 @@
pub mod frame;
pub mod session;
pub mod value;
+pub mod xdebug;
pub use value::{PhpClassHandle, PhpObjHandle, PhpObject, PluginValue, RustObjHandle};
@@ -536,14 +537,23 @@ pub fn phpversion(extension: &str) -> Option<String> {
}
}
-/// `Composer\XdebugHandler\XdebugHandler::getAllIniFiles()` (minus the `self::$name` branch,
-/// which is unreachable since this port never constructs an XdebugHandler): `[(string)
+/// What `Composer\XdebugHandler\XdebugHandler::getAllIniFiles()` measures: `[(string)
/// php_ini_loaded_file()]` merged with the trimmed, comma-split `php_ini_scanned_files()` list
-/// when scanning is active.
+/// when scanning is active. The worker runs on the ini files of the machine, so these are the
+/// user's own.
pub fn get_all_ini_files() -> Vec<String> {
string_list(call("get_all_ini_files", ""), "get_all_ini_files")
}
+/// `Composer\XdebugHandler\XdebugHandler::isXdebugActive()` as measured in the worker, which is
+/// the process Xdebug is loaded into.
+pub(crate) fn xdebug_active() -> bool {
+ match call("xdebug_active", "") {
+ PhpMixed::Bool(b) => b,
+ other => panic!("PHP RPC: `xdebug_active` did not return a bool: {other:?}"),
+ }
+}
+
fn string_list(value: PhpMixed, name: &str) -> Vec<String> {
match value {
PhpMixed::List(items) => items
@@ -1070,7 +1080,16 @@ fn spawn_worker() -> anyhow::Result<Worker> {
// supported) serialize_precision; pin the child to it in case a distro php.ini overrides
// the default.
.arg("-d")
- .arg("serialize_precision=-1")
+ .arg("serialize_precision=-1");
+ if xdebug::switches_xdebug_off() {
+ // The environment variable takes precedence over every ini setting, so switching the
+ // mode off takes both. See `docs/dev/xdebug.md`.
+ command
+ .arg("-d")
+ .arg("xdebug.mode=off")
+ .env("XDEBUG_MODE", "off");
+ }
+ command
.arg(&script_path)
.arg(WORKER_SOCKET_FD.to_string())
.arg(&stubs_dir);
@@ -1147,6 +1166,41 @@ mod tests {
}
#[test]
+ fn worker_starts_with_xdebug_switched_off() {
+ if PhpExecutableFinder::new().find(false).is_none() {
+ // No PHP in this environment; the worker cannot start.
+ return;
+ }
+
+ let mut worker = spawn_worker().expect("failed to spawn PHP worker");
+
+ // The `-d xdebug.mode=off` half is invisible from PHP while the extension is not loaded
+ // (an unregistered ini entry is not readable), so only the environment half — the one
+ // that overrides every ini setting — can be asserted here.
+ frame::write_frame(
+ &mut worker.stream,
+ &Frame::CallFunction {
+ corr_id: 1,
+ function_name: "getenv".to_string(),
+ args: vec![PluginValue::string("XDEBUG_MODE")],
+ out_param_positions: Vec::new(),
+ },
+ )
+ .expect("failed to ask the worker for its Xdebug mode");
+ let reply = frame::read_frame(&mut worker.stream).expect("failed to read the worker reply");
+ match reply {
+ Frame::Return { value, .. } => assert_eq!(
+ value.to_php_mixed().expect("unusable reply"),
+ PhpMixed::String("off".to_string())
+ ),
+ other => panic!("unexpected reply: {other:?}"),
+ }
+
+ worker.child.kill().expect("failed to kill PHP worker");
+ worker.child.wait().expect("failed to reap PHP worker");
+ }
+
+ #[test]
fn queries_string_lists_when_php_available() {
if PhpExecutableFinder::new().find(false).is_none() {
// No PHP in this environment; the worker cannot start.
diff --git a/crates/shirabe-php-rpc/src/xdebug.rs b/crates/shirabe-php-rpc/src/xdebug.rs
new file mode 100644
index 00000000..6ed048cf
--- /dev/null
+++ b/crates/shirabe-php-rpc/src/xdebug.rs
@@ -0,0 +1,35 @@
+//! ref: composer/vendor/composer/xdebug-handler/src/XdebugHandler.php
+//!
+//! Keeping Xdebug out of the PHP worker, which is where Composer's restart of itself lands in
+//! this port. See `docs/dev/xdebug.md`.
+
+use shirabe_php_shim::getenv;
+
+/// `XdebugHandler::$name.XdebugHandler::SUFFIX_ALLOW`, where the name is the uppercased prefix of
+/// the one construction Composer makes: `new XdebugHandler('Composer')` in `bin/composer`.
+const ALLOW: &str = "COMPOSER_ALLOW_XDEBUG";
+
+/// PHP: `XdebugHandler::isXdebugActive()`. Whether Xdebug is loaded and running in an active mode.
+///
+/// Answered without asking PHP whenever the worker is started with the mode switched off, since
+/// that settles the question for every Xdebug that honours the setting — Xdebug 2, which has no
+/// such setting, is reported inactive while it is not. `diagnose` reports what the worker measures
+/// instead.
+pub fn is_xdebug_active() -> bool {
+ if switches_xdebug_off() {
+ return false;
+ }
+
+ crate::xdebug_active()
+}
+
+/// Whether the worker is started with Xdebug switched off, which is this port's stand-in for
+/// `XdebugHandler::check()` restarting the process. The answer is a property of the environment
+/// alone, so it holds whether or not the worker has been spawned yet.
+pub(crate) fn switches_xdebug_off() -> bool {
+ // PHP: `!((bool) explode('|', getenv($this->envAllowXdebug))[0])`, where the pipe-separated
+ // form is the handoff to a process xdebug-handler restarted. Nothing restarts here, so what
+ // is left is PHP's truthiness of the value.
+ let allow_xdebug = getenv(ALLOW).unwrap_or_default();
+ matches!(allow_xdebug.to_string_lossy().as_ref(), "" | "0")
+}
diff --git a/crates/shirabe-xdebug-handler/Cargo.toml b/crates/shirabe-xdebug-handler/Cargo.toml
deleted file mode 100644
index 3deda4ac..00000000
--- a/crates/shirabe-xdebug-handler/Cargo.toml
+++ /dev/null
@@ -1,11 +0,0 @@
-[package]
-name = "shirabe-xdebug-handler"
-version.workspace = true
-edition.workspace = true
-rust-version.workspace = true
-description = "A Rust port of composer/xdebug-handler"
-repository.workspace = true
-license.workspace = true
-
-[lints]
-workspace = true
diff --git a/crates/shirabe-xdebug-handler/src/lib.rs b/crates/shirabe-xdebug-handler/src/lib.rs
deleted file mode 100644
index c95f237d..00000000
--- a/crates/shirabe-xdebug-handler/src/lib.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-pub mod xdebug_handler;
-
-pub use xdebug_handler::*;
diff --git a/crates/shirabe-xdebug-handler/src/xdebug_handler.rs b/crates/shirabe-xdebug-handler/src/xdebug_handler.rs
deleted file mode 100644
index 6ccedc6a..00000000
--- a/crates/shirabe-xdebug-handler/src/xdebug_handler.rs
+++ /dev/null
@@ -1,32 +0,0 @@
-//! ref: composer/vendor/composer/xdebug-handler/src/XdebugHandler.php
-
-#[derive(Debug)]
-pub struct XdebugHandler;
-
-impl XdebugHandler {
- pub fn is_xdebug_active() -> bool {
- // TODO(php-runtime)
- false
- }
-
- pub fn get_skipped_version() -> Option<String> {
- // TODO(php-runtime)
- // The restart-to-disable-xdebug mechanism is not ported (`is_xdebug_active` is
- // hardcoded `false`), so a restart never happens and `self::$skipped` stays at
- // its PHP default of `""`.
- Some(String::new())
- }
-
- pub fn get_all_ini_files() -> Vec<String> {
- // TODO(php-runtime)
- // No XdebugHandler is ever constructed (`self::$name` stays null), because the
- // `new XdebugHandler('Composer'); $xdebug->check();` bootstrap in `bin/composer` is not
- // ported (see the TODO(phase-c) at the top of shirabe's main.rs), so the
- // COMPOSER_ORIGINAL_INIS env-var branch is unreachable here.
- //
- // Callers that need the real PHP runtime's ini files (php_ini_loaded_file() /
- // php_ini_scanned_files()) query shirabe_php_rpc directly instead of going through this
- // stub; see IniHelper::get_all in the shirabe crate.
- vec![String::new()]
- }
-}
diff --git a/crates/shirabe/Cargo.toml b/crates/shirabe/Cargo.toml
index ccc2ca8a..6d29c886 100644
--- a/crates/shirabe/Cargo.toml
+++ b/crates/shirabe/Cargo.toml
@@ -22,7 +22,6 @@ shirabe-symfony-console.workspace = true
shirabe-symfony-filesystem.workspace = true
shirabe-symfony-finder.workspace = true
shirabe-symfony-process.workspace = true
-shirabe-xdebug-handler.workspace = true
anyhow.workspace = true
async-trait.workspace = true
base64.workspace = true
diff --git a/crates/shirabe/src/command/diagnose_command.rs b/crates/shirabe/src/command/diagnose_command.rs
index 2af2acfc..5094f06a 100644
--- a/crates/shirabe/src/command/diagnose_command.rs
+++ b/crates/shirabe/src/command/diagnose_command.rs
@@ -887,9 +887,6 @@ impl DiagnoseCommand {
if filter_var_boolean(diagnostics.ini_get("xdebug.profiler_enabled").unwrap_or("")) {
warnings.insert("xdebug_profile".to_string(), PhpMixed::Bool(true));
} else if diagnostics.xdebug_active {
- // PHP: XdebugHandler::isXdebugActive(). As with IniHelper::get_all, the port of that
- // method in shirabe_xdebug_handler is a stub, so the real runtime is queried through
- // the diagnose payload instead.
warnings.insert("xdebug_loaded".to_string(), PhpMixed::Bool(true));
}
diff --git a/crates/shirabe/src/console/application.rs b/crates/shirabe/src/console/application.rs
index cc456c3c..8fed08e9 100644
--- a/crates/shirabe/src/console/application.rs
+++ b/crates/shirabe/src/console/application.rs
@@ -98,7 +98,6 @@ use shirabe_symfony_console::style::style_interface::StyleInterface;
use shirabe_symfony_console::style::symfony_style::SymfonyStyle;
use shirabe_symfony_console::terminal::Terminal;
use shirabe_symfony_process::exception::ProcessTimedOutException;
-use shirabe_xdebug_handler::XdebugHandler;
/// The PHP `Composer\Console\Application` and `Symfony\Component\Console\Application` are
/// flattened into a single struct. Methods that are overridden by subclass and called via
@@ -2275,7 +2274,7 @@ impl ApplicationHandle {
io.write_error(&format!("<warning>Composer supports PHP 7.2.5 and above, you will most likely encounter problems with your PHP {}. Upgrading is strongly recommended but you can use Composer 2.2.x LTS as a fallback.</warning>", PHP_VERSION));
}
- if XdebugHandler::is_xdebug_active()
+ if shirabe_php_rpc::xdebug::is_xdebug_active()
&& Platform::get_env("COMPOSER_DISABLE_XDEBUG_WARN").is_none()
{
io.write_error("<warning>Composer is operating slower than normal because you have Xdebug enabled. See https://getcomposer.org/xdebug</warning>");
diff --git a/crates/shirabe/src/main.rs b/crates/shirabe/src/main.rs
index dcf33803..f8b64c46 100644
--- a/crates/shirabe/src/main.rs
+++ b/crates/shirabe/src/main.rs
@@ -29,18 +29,14 @@ fn init_tracing() {
}
fn main() {
- // TODO(phase-c): PHP: `$xdebug = new XdebugHandler('Composer'); $xdebug->check(); unset($xdebug);`
- // (restart the process without Xdebug loaded, for performance) is not ported. Since no
- // XdebugHandler is ever constructed, `self::$name` never gets set, so the
- // COMPOSER_ORIGINAL_INIS-env-var branch of XdebugHandler::getAllIniFiles() (see
- // shirabe-xdebug-handler's xdebug_handler.rs and shirabe/src/util/ini_helper.rs) is
- // unreachable in this port, not merely unexercised by current tests.
-
// Take the $_ENV / $_SERVER snapshots before any putenv() mutates the real environment.
// See `docs/dev/env-vars-porting.md` for details.
std::sync::LazyLock::force(&PHP_ENV);
std::sync::LazyLock::force(&PHP_SERVER);
+ // Composer calls XdebugHandler::check() here to restart without Xdebug, which is intentionally
+ // unported. See `docs/dev/xdebug.md`.
+
// The single process-wide tokio Runtime. `shirabe::run` and everything under it
// (Command::execute and friends) is still synchronous top to bottom; entering the runtime
// here (rather than driving `run` via `.block_on`) just makes it ambiently available via
diff --git a/crates/shirabe/src/repository/platform_repository.rs b/crates/shirabe/src/repository/platform_repository.rs
index 7e123151..3e1f35ec 100644
--- a/crates/shirabe/src/repository/platform_repository.rs
+++ b/crates/shirabe/src/repository/platform_repository.rs
@@ -20,11 +20,10 @@ use shirabe_pcre::{CaptureKey, Preg};
use shirabe_php_rpc::PlatformInfo;
use shirabe_php_shim::{
InvalidArgumentException, PhpMixed, UnexpectedValueException, array_map_str_fn,
- array_slice_strs, explode, get_class, implode, in_array_strict, is_string, php_regex,
- str_replace, str_starts_with, strpos, strtolower, var_export,
+ array_slice_strs, explode, get_class, implode, is_string, php_regex, str_replace,
+ str_starts_with, strpos, strtolower, var_export,
};
use shirabe_semver::constraint::SimpleConstraint;
-use shirabe_xdebug_handler::XdebugHandler;
use std::sync::{LazyLock, Mutex};
static LAST_SEEN_PLATFORM_PHP: LazyLock<Mutex<Option<String>>> = LazyLock::new(|| Mutex::new(None));
@@ -303,17 +302,10 @@ impl PlatformRepository {
}
// Check for Xdebug in a restarted process
- if !in_array_strict(
- "xdebug".to_string(),
- &loaded_extensions
- .iter()
- .map(|s| PhpMixed::String(s.clone()))
- .collect::<Vec<_>>(),
- ) && let Some(xdebug_pretty_version) = XdebugHandler::get_skipped_version()
- && !xdebug_pretty_version.is_empty()
- {
- self.add_extension("xdebug", &xdebug_pretty_version)?;
- }
+ // PHP re-adds the extension `XdebugHandler::getSkippedVersion()` names, since its restart
+ // runs on an ini file with Xdebug commented out. The worker is instead started with
+ // `xdebug.mode=off` (see docs/dev/xdebug.md), which leaves the extension loaded, so it is
+ // already among $loadedExtensions.
// Another quick loop, just for possible libraries
// Doing it this way to know that functions or constants exist before
diff --git a/crates/shirabe/src/util/ini_helper.rs b/crates/shirabe/src/util/ini_helper.rs
index 4ff9a5e2..08f5c816 100644
--- a/crates/shirabe/src/util/ini_helper.rs
+++ b/crates/shirabe/src/util/ini_helper.rs
@@ -4,10 +4,12 @@ pub struct IniHelper;
impl IniHelper {
/// Returns an array of php.ini locations with at least one entry.
+ ///
+ /// PHP asks `XdebugHandler::getAllIniFiles()`, which answers from `COMPOSER_ORIGINAL_INIS`
+ /// when it is set, because a restarted process runs on a generated ini file and has to name
+ /// the ones it replaced. Nothing here restarts PHP, so the worker always runs on the machine's
+ /// own ini files and the variable is not consulted.
pub fn get_all() -> Vec<String> {
- // PHP: XdebugHandler::getAllIniFiles(). shirabe_xdebug_handler::XdebugHandler's port
- // of that method is a stub that returns the PHP default, so query the real PHP
- // runtime's loaded/scanned ini files here instead.
shirabe_php_rpc::get_all_ini_files()
}
diff --git a/crates/shirabe/tests/util/ini_helper_test.rs b/crates/shirabe/tests/util/ini_helper_test.rs
index 90015169..f9ef3219 100644
--- a/crates/shirabe/tests/util/ini_helper_test.rs
+++ b/crates/shirabe/tests/util/ini_helper_test.rs
@@ -1,104 +1,7 @@
//! ref: composer/tests/Composer/Test/Util/IniHelperTest.php
-
-use shirabe::util::ini_helper::IniHelper;
-use shirabe::util::platform::Platform;
-use shirabe_php_shim::{getenv, putenv};
-
-#[allow(dead_code)]
-fn set_up() -> TearDown {
- // Register our name with XdebugHandler.
- // The PHP test constructs `new XdebugHandler('composer')` so that
- // getAllIniFiles() reads the COMPOSER_ORIGINAL_INIS env var. The Rust
- // XdebugHandler is a unit struct with no name-registration API, so this
- // step is a no-op here.
- // Save current state
- let env_original =
- getenv("COMPOSER_ORIGINAL_INIS").map(|value| value.to_string_lossy().into_owned());
- TearDown { env_original }
-}
-
-#[allow(dead_code)]
-fn tear_down(env_original: &Option<String>) {
- // Restore original state
- if let Some(env_original) = env_original {
- unsafe { putenv("COMPOSER_ORIGINAL_INIS", env_original) };
- } else {
- Platform::clear_env("COMPOSER_ORIGINAL_INIS");
- }
-}
-
-#[allow(dead_code)]
-struct TearDown {
- env_original: Option<String>,
-}
-
-impl Drop for TearDown {
- fn drop(&mut self) {
- tear_down(&self.env_original);
- }
-}
-
-fn set_env(paths: &[&str]) {
- unsafe {
- putenv(
- "COMPOSER_ORIGINAL_INIS",
- std::env::join_paths(paths).unwrap(),
- )
- };
-}
-
-#[test]
-#[ignore = "XdebugHandler is never constructed with a name (the bin/composer restart-without-Xdebug \
-bootstrap is unported, see TODO(phase-c) in shirabe's main.rs), so IniHelper::get_all() queries \
-the real PHP runtime directly instead of consulting COMPOSER_ORIGINAL_INIS"]
-fn test_with_no_ini() {
- let paths = [""];
-
- set_env(&paths);
- assert!(IniHelper::get_message().contains("does not exist"));
- assert_eq!(
- paths.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
- IniHelper::get_all()
- );
-}
-
-#[test]
-#[ignore = "XdebugHandler is never constructed with a name (the bin/composer restart-without-Xdebug \
-bootstrap is unported, see TODO(phase-c) in shirabe's main.rs), so IniHelper::get_all() queries \
-the real PHP runtime directly instead of consulting COMPOSER_ORIGINAL_INIS"]
-fn test_with_loaded_ini_only() {
- let paths = ["loaded.ini"];
-
- set_env(&paths);
- assert!(IniHelper::get_message().contains("loaded.ini"));
-}
-
-#[test]
-#[ignore = "XdebugHandler is never constructed with a name (the bin/composer restart-without-Xdebug \
-bootstrap is unported, see TODO(phase-c) in shirabe's main.rs), so IniHelper::get_all() queries \
-the real PHP runtime directly instead of consulting COMPOSER_ORIGINAL_INIS"]
-fn test_with_loaded_ini_and_additional() {
- let paths = ["loaded.ini", "one.ini", "two.ini"];
-
- set_env(&paths);
- assert!(IniHelper::get_message().contains("multiple ini files"));
- assert_eq!(
- paths.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
- IniHelper::get_all()
- );
-}
-
-#[test]
-#[ignore = "XdebugHandler is never constructed with a name (the bin/composer restart-without-Xdebug \
-bootstrap is unported, see TODO(phase-c) in shirabe's main.rs), so IniHelper::get_all() queries \
-the real PHP runtime directly instead of consulting COMPOSER_ORIGINAL_INIS"]
-fn test_without_loaded_ini_and_additional() {
- let paths = ["", "one.ini", "two.ini"];
-
- set_env(&paths);
- assert!(IniHelper::get_message().contains("multiple ini files"));
- assert_eq!(
- paths.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
- IniHelper::get_all()
- );
-}
+//!
+//! All four cases of this test feed IniHelper made-up ini paths through COMPOSER_ORIGINAL_INIS.
+//! That variable exists so a process restarted without Xdebug can name the ini files it replaced;
+//! nothing here restarts PHP, so IniHelper::get_all() reports the ini files the PHP worker loaded
+//! and never reads it (see docs/dev/xdebug.md). With no way to feed the helper, none of the cases
+//! are ported.
diff --git a/docs/dev/php-rpc.md b/docs/dev/php-rpc.md
index 314386d8..8418db88 100644
--- a/docs/dev/php-rpc.md
+++ b/docs/dev/php-rpc.md
@@ -11,7 +11,8 @@ domain socket. There is exactly one child process per Shirabe process, shared by
The existing `PhpExecutableFinder` class resolves the PHP binary. The child is started with
`-d serialize_precision=-1` so the wire codec's float formatting is pinned to the default PHP
-behavior.
+behavior, and with `-d xdebug.mode=off` unless `COMPOSER_ALLOW_XDEBUG` asks for Xdebug to stay
+(see `xdebug.md`).
## Transport
diff --git a/docs/dev/xdebug.md b/docs/dev/xdebug.md
new file mode 100644
index 00000000..be198cca
--- /dev/null
+++ b/docs/dev/xdebug.md
@@ -0,0 +1,28 @@
+# Xdebug
+
+Composer restarts itself without Xdebug loaded, because Xdebug makes PHP
+several times slower. Shirabe is written in Rust, where Xdebug does not exist,
+but it invokes a PHP command for plugins, scripts and platform queries.
+When Shirabe spawns a PHP worker, Xdebug is disabled as Composer does.
+
+The PHP worker is spawned with the `-d xdebug.mode=off` flag and the
+`XDEBUG_MODE=off` environment variable. The way to disable Xdebug in Shirabe is
+different from Composer: Composer restarts its own process with a temporary
+INI file, where Xdebug extension is disabled. The difference probably does not
+matter, both for users and for plugin authors.
+
+## Enable Xdebug in Shirabe's PHP worker
+
+It is the same as Composer: setting `COMPOSER_ALLOW_XDEBUG` to 1 makes Shirabe
+leave Xdebug enabled.
+
+```
+$ COMPOSER_ALLOW_XDEBUG=1 shirabe install
+```
+
+## Xdebug 2 support
+
+Shirabe does not try to disable Xdebug version 2 because Xdebug 2 has no
+`xdebug.mode` or `XDEBUG_MODE`, while Composer disables Xdebug 2 too. The
+performance penalty seems to be small as Shirabe's CPU-heavy workloads are
+written in Rust.
diff --git a/docs/known-incompatibilities.md b/docs/known-incompatibilities.md
index 99c0e322..daaa8bcd 100644
--- a/docs/known-incompatibilities.md
+++ b/docs/known-incompatibilities.md
@@ -32,6 +32,13 @@ The following are intentionally left unchanged for ecosystem compatibility:
TODO: a CLI flag or an environment variable to force Shirabe to use compatible paths.
+## Xdebug
+
+Composer disables Xdebug on startup. Shirabe emulates it, but the exact
+behavior is slightly different. See [docs/dev/xdebug.md](./dev/xdebug.md)
+for details.
+
+
## Plugins
### Reflection