From 3c5bcf39bd92a922440ad21103b2f70cb7da5fbf Mon Sep 17 00:00:00 2001
From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Date: Thu, 5 Nov 2026 23:46:28 +0800
Subject: [PATCH] fix(run): run PHP files with the interpreter chosen in
Settings
Running a .php file (or a #!/usr/bin/env php script) produced a
configuration with the PATH php: the file producer looked tools up on
PATH, and the settings pass only replaces a bare "php" command, never
the absolute path the producer had resolved. With php@8.1 selected in
Settings and php 8.4 on PATH, the file ran on 8.4, and with no php on
PATH it failed to start although an interpreter was selected.
A new sdk_php_effective command returns the configured interpreter, the
way sdk_python_effective does for Python, and the producer prefers it.
The not-found message now also points to the PHP settings page.
---
crates/ide-host/src/ops/sdk.rs | 9 +++++++++
src-tauri/src/commands/mod.rs | 1 +
src-tauri/src/commands/sdk.rs | 7 +++++++
src/bindings.ts | 2 ++
src/features/run/contextRun.ts | 8 +++++++-
src/features/run/producers.test.ts | 9 +++++++++
src/features/run/producers.ts | 8 +++++++-
src/features/run/runtimes.ts | 13 ++++++++++++-
8 files changed, 54 insertions(+), 3 deletions(-)
diff --git a/crates/ide-host/src/ops/sdk.rs b/crates/ide-host/src/ops/sdk.rs
index 702c35c2..78c5aee0 100644
--- a/crates/ide-host/src/ops/sdk.rs
+++ b/crates/ide-host/src/ops/sdk.rs
@@ -52,6 +52,14 @@ pub fn sdk_python_effective(host: &Host, window: &str) -> IdeResult IdeResult > {
+ host.require_trust(window)?;
+ let project = host.project(window)?;
+ let settings = project.settings.effective();
+ Ok(ide_sdk::apply::setting_path(&project.root, &settings.languages.php.interpreter).map(Into::into))
+}
+
/// The PHP language level of the project `path` belongs to (a workspace member, by path, as
/// the language server configuration picks it).
#[derive(Clone, Debug, serde::Serialize, specta::Type)]
@@ -198,6 +206,7 @@ pub async fn sdk_python_pip(
pub(crate) fn register(d: &mut Dispatcher) {
d.add("sdk_detect", |c| async move { sdk_detect(&c.host, &c.window, c.arg("kind")?).await });
d.add("sdk_python_effective", |c| async move { sdk_python_effective(&c.host, &c.window) });
+ d.add("sdk_php_effective", |c| async move { sdk_php_effective(&c.host, &c.window) });
d.add(
"sdk_php_language_level",
|c| async move { sdk_php_language_level(&c.host, &c.window, c.arg("path")?).await },
diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs
index aca0338a..72ed8e0e 100644
--- a/src-tauri/src/commands/mod.rs
+++ b/src-tauri/src/commands/mod.rs
@@ -398,6 +398,7 @@ pub fn builder() -> tauri_specta::Builder {
tools::jdk_download,
sdk::sdk_detect,
sdk::sdk_python_effective,
+ sdk::sdk_php_effective,
sdk::sdk_php_language_level,
sdk::sdk_project_libraries,
sdk::sdk_locate_tools,
diff --git a/src-tauri/src/commands/sdk.rs b/src-tauri/src/commands/sdk.rs
index db2692f5..bb3db7d3 100644
--- a/src-tauri/src/commands/sdk.rs
+++ b/src-tauri/src/commands/sdk.rs
@@ -24,6 +24,13 @@ pub fn sdk_python_effective(window: Window, state: State<'_, AppState>) -> IdeRe
ide_host::ops::sdk::sdk_python_effective(&state.host, window.label())
}
+/// The PHP interpreter chosen in Settings › Languages & Frameworks › PHP; `None`: the `php` on `PATH`.
+#[tauri::command]
+#[specta::specta]
+pub fn sdk_php_effective(window: Window, state: State<'_, AppState>) -> IdeResult> {
+ ide_host::ops::sdk::sdk_php_effective(&state.host, window.label())
+}
+
/// The PHP language level of the project `path` belongs to: the effective one and Auto's.
#[tauri::command]
#[specta::specta]
diff --git a/src/bindings.ts b/src/bindings.ts
index 8fba6499..2e9cb6dc 100644
--- a/src/bindings.ts
+++ b/src/bindings.ts
@@ -772,6 +772,8 @@ export const commands = {
sdkDetect: (kind: SdkKind) => typedError(__TAURI_INVOKE("sdk_detect", { kind })),
/** The interpreter the project uses: the configured one, else its virtualenv. */
sdkPythonEffective: () => typedError(__TAURI_INVOKE("sdk_python_effective")),
+ /** The PHP interpreter chosen in Settings › Languages & Frameworks › PHP; `None`: the `php` on `PATH`. */
+ sdkPhpEffective: () => typedError(__TAURI_INVOKE("sdk_php_effective")),
/** The PHP language level of the project `path` belongs to: the effective one and Auto's. */
sdkPhpLanguageLevel: (path: string) => typedError(__TAURI_INVOKE("sdk_php_language_level", { path })),
/**
diff --git a/src/features/run/contextRun.ts b/src/features/run/contextRun.ts
index 8e7f1bed..f600cddb 100644
--- a/src/features/run/contextRun.ts
+++ b/src/features/run/contextRun.ts
@@ -98,7 +98,13 @@ export async function produceResolved(input: ContextInput, dir = false): Promise
// Project configurations (tests, cargo, gradle…) need no runtime lookup.
if (named.config && !named.config.id.startsWith("context:file:")) return named;
const lang = scriptLanguage(input.path, input.content);
- const env = await toolEnv(neededTools(fresh), input.path, fresh.root, lang === "python" || lang === "shebang");
+ const env = await toolEnv(
+ neededTools(fresh),
+ input.path,
+ fresh.root,
+ lang === "python" || lang === "shebang",
+ lang === "php" || lang === "shebang",
+ );
return produceContext(fresh, env);
}
diff --git a/src/features/run/producers.test.ts b/src/features/run/producers.test.ts
index 4c3812cf..194982d7 100644
--- a/src/features/run/producers.test.ts
+++ b/src/features/run/producers.test.ts
@@ -226,6 +226,15 @@ 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"] });
+ // 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(run(scratch("a.php"), env({}, { php: php81 }))).toMatchObject({ command: php81, args: ["a.php"] });
+ expect(run(scratch("tool", "#!/usr/bin/env php\n = {
js: "Install Node.js from https://nodejs.org (or `brew install node`).",
python: "Install Python (`brew install python`) or choose an interpreter in Settings › Python Interpreter.",
ruby: "Install Ruby (`brew install ruby`).",
- php: "Install PHP (`brew install php`).",
+ php: "Install PHP (`brew install php`) or choose an interpreter in Settings › Languages & Frameworks › PHP.",
perl: "Install Perl (`brew install perl`).",
lua: "Install Lua (`brew install lua`).",
r: "Install R (`brew install r`) so that `Rscript` is on the PATH.",
@@ -762,6 +764,8 @@ export function produceFile(input: ContextInput, env: ToolEnv): Produced | null
config: fileConfig(s, "python", py.command, [...py.prefix, s.arg], "debugpy"),
};
}
+ if (interp === "php" && env.php)
+ return { name: s.file, scope: "file", config: fileConfig(s, "php", env.php, [...sb.slice(1), s.arg]) };
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;
@@ -805,6 +809,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]);
case "perl":
case "lua":
case "swift":
diff --git a/src/features/run/runtimes.ts b/src/features/run/runtimes.ts
index b6d21d7b..883d2c7b 100644
--- a/src/features/run/runtimes.ts
+++ b/src/features/run/runtimes.ts
@@ -86,6 +86,14 @@ async function projectPython(): Promise {
}
}
+async function projectPhp(): Promise {
+ try {
+ return await call(commands.sdkPhpEffective());
+ } catch {
+ return null;
+ }
+}
+
async function rootMarkers(root: string | null): Promise<{ deno: boolean; bun: boolean }> {
if (!root) return { deno: false, bun: false };
try {
@@ -106,10 +114,12 @@ export async function toolEnv(
file: string | null,
root: string | null,
python: boolean,
+ php = false,
): Promise {
- const [tools, py, markers] = await Promise.all([
+ const [tools, py, phpInterpreter, markers] = await Promise.all([
locateTools(names, file),
python ? projectPython() : Promise.resolve(null),
+ php ? projectPhp() : Promise.resolve(null),
names.includes("deno") || names.includes("bun") ? rootMarkers(root) : Promise.resolve({ deno: false, bun: false }),
]);
return {
@@ -117,6 +127,7 @@ export async function toolEnv(
compileDir: compileDir ?? "/tmp/openidea-run",
windows: isWindows(),
python: py,
+ php: phpInterpreter,
...markers,
};
}