mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 00:07:50 +00:00
fix(debugger): debug PHP with php-debug on the configured PHP (#1641)
php-debug had no launch template: the generic one sent the configuration's command as the program, so artisan serve failed or ran the PHP binary as a script, and PHPUnit skipped breakpoints unless php.ini enabled Xdebug's debug mode. The template runs the script on the PHP the command names or the one from Settings, enables debug mode with -d options on the port php-debug picks, and PHP files and php scripts become debuggable. php-debug returns to the configuration form's Debug adapter list.
This commit is contained in:
@@ -23,6 +23,42 @@ describe("run config → launch config", () => {
|
||||
expect(launchConfigFor(rc({}), "/p")).toBeNull();
|
||||
});
|
||||
|
||||
it("php-debug: the script runs on the PHP from Settings with Xdebug's debug mode, on a free port", () => {
|
||||
const xdebug = ["-dxdebug.mode=debug", "-dxdebug.start_with_request=yes", "-dxdebug.client_port=${port}"];
|
||||
// `artisan serve` with PHP at Default: the script is a file of the project.
|
||||
const serve = launchConfigFor(
|
||||
rc({ kind: "php", command: "php", args: ["artisan", "serve"], debugAdapter: "php-debug" }),
|
||||
"/app",
|
||||
"/opt/php@8.3/bin/php",
|
||||
)!;
|
||||
expect(serve).toMatchObject({ adapter: "php-debug", program: "/app/artisan", args: ["serve"], cwd: "/app" });
|
||||
expect(extra(serve)).toEqual({ port: 0, runtimeArgs: xdebug, runtimeExecutable: "/opt/php@8.3/bin/php" });
|
||||
// The PHP the command names wins, and its options stay with it.
|
||||
const file = launchConfigFor(
|
||||
rc({
|
||||
kind: "php",
|
||||
command: "/usr/bin/php8.4",
|
||||
args: ["-dmemory_limit=1G", "src/a.php", "x"],
|
||||
debugAdapter: "php-debug",
|
||||
}),
|
||||
"/app",
|
||||
"/opt/php@8.3/bin/php",
|
||||
)!;
|
||||
expect(file).toMatchObject({ program: "/app/src/a.php", args: ["x"] });
|
||||
expect(extra(file)).toEqual({
|
||||
port: 0,
|
||||
runtimeArgs: [...xdebug, "-dmemory_limit=1G"],
|
||||
runtimeExecutable: "/usr/bin/php8.4",
|
||||
});
|
||||
// PHPUnit runs its script directly; without a PHP in Settings, php-debug uses the `php` on PATH.
|
||||
const unit = launchConfigFor(
|
||||
rc({ kind: "phpunit", command: "vendor/bin/phpunit", args: ["--filter", "testA"], debugAdapter: "php-debug" }),
|
||||
"/app",
|
||||
)!;
|
||||
expect(unit).toMatchObject({ program: "/app/vendor/bin/phpunit", args: ["--filter", "testA"] });
|
||||
expect(extra(unit)).toEqual({ port: 0, runtimeArgs: xdebug });
|
||||
});
|
||||
|
||||
it("cargo run builds the binary through codelldb's cargo integration", () => {
|
||||
const l = launchConfigFor(
|
||||
rc({
|
||||
|
||||
@@ -299,7 +299,10 @@ async function startConfig(arg: unknown) {
|
||||
}
|
||||
const dotnet = config.kind === "dotnet" && config.debugAdapter === "netcoredbg";
|
||||
const prepared = dotnet ? await call(commands.runDotnetResolve(config, true)) : config;
|
||||
const launch = launchConfigFor(prepared, root);
|
||||
// php-debug starts the PHP chosen in Settings unless the command names one.
|
||||
const php =
|
||||
config.debugAdapter === "php-debug" && isTauri() ? await call(commands.sdkPhpEffective()).catch(() => null) : null;
|
||||
const launch = launchConfigFor(prepared, root, php);
|
||||
if (!launch) {
|
||||
notifications().notify({
|
||||
group: "run",
|
||||
|
||||
@@ -356,11 +356,44 @@ function flutterAttach(rc: RunConfig, cwd: string, adapter: string): LaunchConfi
|
||||
};
|
||||
}
|
||||
|
||||
const PHP_NAME = /^php(\d+(\.\d+)*)?(\.exe)?$/i;
|
||||
|
||||
/**
|
||||
* php-debug runs `runtimeExecutable runtimeArgs program args` and doesn't enable Xdebug: debug mode and
|
||||
* the connection back come from `-d` options, to the free port php-debug picks for `port: 0` (another
|
||||
* IDE may listen on 9003). `php` is the PHP chosen in Settings, used when the command doesn't name one.
|
||||
*/
|
||||
function phpDebugLaunch(rc: RunConfig, cwd: string, php: string | null): LaunchConfig {
|
||||
const name = rc.command.split(/[\\/]/).pop() ?? rc.command;
|
||||
const runtimeArgs = ["-dxdebug.mode=debug", "-dxdebug.start_with_request=yes", "-dxdebug.client_port=${port}"];
|
||||
let runtime = php;
|
||||
let program: string;
|
||||
let args: string[];
|
||||
if (PHP_NAME.test(name)) {
|
||||
// `php [options] script args`: the options stay with PHP; the script is a file (`artisan`).
|
||||
if (name !== rc.command) runtime = resolveCommand(rc.command, cwd);
|
||||
const at = rc.args.findIndex((a) => !a.startsWith("-"));
|
||||
runtimeArgs.push(...(at < 0 ? rc.args : rc.args.slice(0, at)));
|
||||
program = at < 0 ? "" : join(cwd, rc.args[at]!);
|
||||
args = at < 0 ? [] : rc.args.slice(at + 1);
|
||||
} else {
|
||||
// A PHP script run directly (`vendor/bin/phpunit`; on Windows its `.bat` shim sits beside it).
|
||||
program = resolveCommand(rc.command.replace(/\.bat$/i, ""), cwd);
|
||||
args = [...rc.args];
|
||||
}
|
||||
return {
|
||||
...base(rc, "php-debug", cwd),
|
||||
program,
|
||||
args,
|
||||
extraJson: JSON.stringify({ port: 0, runtimeArgs, ...(runtime ? { runtimeExecutable: runtime } : {}) }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The launch configuration that debugs `rc`, or `null` when it has no adapter.
|
||||
* `root` is the project root (default working directory).
|
||||
*/
|
||||
export function launchConfigFor(rc: RunConfig, root: string): LaunchConfig | null {
|
||||
export function launchConfigFor(rc: RunConfig, root: string, php: string | null = null): LaunchConfig | null {
|
||||
const adapter = rc.debugAdapter;
|
||||
if (!adapter || debugUnsupportedReason(rc)) return null;
|
||||
const cwd = rc.cwd ? join(root, rc.cwd) : root;
|
||||
@@ -374,6 +407,7 @@ export function launchConfigFor(rc: RunConfig, root: string): LaunchConfig | nul
|
||||
if (adapter === "js-debug") return jsDebugLaunch(rc, cwd);
|
||||
if (adapter === "delve") return delveLaunch(rc, cwd, root);
|
||||
if (adapter === "godot") return godotLaunch(rc, cwd);
|
||||
if (adapter === "php-debug") return phpDebugLaunch(rc, cwd, php);
|
||||
if (adapter === "netcoredbg" && rc.kind === "dotnet") {
|
||||
if (!rc.params?.targetPath) return null;
|
||||
return {
|
||||
|
||||
@@ -50,7 +50,7 @@ import styles from "./Run.module.css";
|
||||
|
||||
/**
|
||||
* Debug adapters with a real launch template (`debugger/launch.ts`, `debugger/java.ts`),
|
||||
* verified against small programs. Others (dart, php-debug, …) only got
|
||||
* verified against small programs. Others (dart, …) only got
|
||||
* generic launch arguments and are left out until they have one; a configuration that
|
||||
* already names one keeps it.
|
||||
*/
|
||||
@@ -63,6 +63,7 @@ const ADAPTERS: ReadonlyArray<{ id: string; label: string }> = [
|
||||
{ id: "delve", label: "Delve (Go)" },
|
||||
{ id: "codelldb", label: "CodeLLDB (Rust, C, C++)" },
|
||||
{ id: "lldb-dap", label: "lldb-dap (C, C++, Swift)" },
|
||||
{ id: "php-debug", label: "php-debug (PHP)" },
|
||||
];
|
||||
|
||||
/** Options "Modify options" adds to the form: value fields (shown once added) and flags (chips). */
|
||||
|
||||
@@ -226,9 +226,12 @@ describe("single-file producers (scratches)", () => {
|
||||
it("Ruby, PHP, Perl, Lua, R, Swift, Dart, Elixir, Scala CLI, Kotlin script", () => {
|
||||
expect(run(scratch("a.rb"))).toMatchObject({ command: "/bin/ruby", args: ["a.rb"] });
|
||||
expect(run(scratch("a.php"))).toMatchObject({ command: "/bin/php", args: ["a.php"] });
|
||||
// Debuggable with php-debug, like a PHP file in PhpStorm.
|
||||
expect(produceContext(scratch("a.php"), ALL)!.config!.debugAdapter).toBe("php-debug");
|
||||
// The PHP chosen in Settings wins over the PATH one, also with no PHP on PATH (#1555).
|
||||
const php81 = "/opt/homebrew/opt/php@8.1/bin/php";
|
||||
expect(run(scratch("a.php"), { ...ALL, php: php81 })).toMatchObject({ command: php81, args: ["a.php"] });
|
||||
expect(produceContext(scratch("a.php"), { ...ALL, php: php81 })!.config!.debugAdapter).toBe("php-debug");
|
||||
expect(run(scratch("a.php"), env({}, { php: php81 }))).toMatchObject({ command: php81, args: ["a.php"] });
|
||||
expect(run(scratch("tool", "#!/usr/bin/env php\n<?php echo 1;"), { ...ALL, php: php81 })).toMatchObject({
|
||||
command: php81,
|
||||
|
||||
@@ -765,7 +765,11 @@ export function produceFile(input: ContextInput, env: ToolEnv): Produced | null
|
||||
};
|
||||
}
|
||||
if (interp === "php" && env.php)
|
||||
return { name: s.file, scope: "file", config: fileConfig(s, "php", env.php, [...sb.slice(1), s.arg]) };
|
||||
return {
|
||||
name: s.file,
|
||||
scope: "file",
|
||||
config: fileConfig(s, "php", env.php, [...sb.slice(1), s.arg], "php-debug"),
|
||||
};
|
||||
const t = sb[0]!.startsWith("/") ? { command: sb[0]!, name: interp } : pick(env, [interp]);
|
||||
if (!t) return { name: s.file, scope: "file", missing: missing(lang, s.file, [interp]) };
|
||||
const kind = /^(ba|z|k|da|fi)?sh$/.test(interp) ? "shell" : interp === "node" ? "node" : interp;
|
||||
@@ -809,8 +813,8 @@ export function produceFile(input: ContextInput, env: ToolEnv): Produced | null
|
||||
// Sources roots go on the load path (`-Ilib`), as `rake test` does; paths are project-relative.
|
||||
return simple(lang, s, env, lang, () => [...(s.inProject ? rubyLoadPath(currentRoots()) : []), s.arg]);
|
||||
case "php":
|
||||
if (env.php) return { name: s.file, scope: "file", config: fileConfig(s, "php", env.php, [s.arg]) };
|
||||
return simple(lang, s, env, lang, () => [s.arg]);
|
||||
if (env.php) return { name: s.file, scope: "file", config: fileConfig(s, "php", env.php, [s.arg], "php-debug") };
|
||||
return simple(lang, s, env, lang, () => [s.arg], "php-debug");
|
||||
case "perl":
|
||||
case "lua":
|
||||
case "swift":
|
||||
|
||||
@@ -86,6 +86,7 @@ const BUILT_AT_RUNTIME = new Set<string>([]);
|
||||
const UNTRANSLATED = new Set<string>([
|
||||
"dotnet test",
|
||||
"netcoredbg (.NET)",
|
||||
"php-debug (PHP)",
|
||||
// Product names
|
||||
"Cargo",
|
||||
"CMake",
|
||||
|
||||
Reference in New Issue
Block a user