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.
This commit is contained in:
l0ng-ai
2026-11-05 23:46:28 +08:00
parent f35026f31c
commit 3c5bcf39bd
8 changed files with 54 additions and 3 deletions
+9
View File
@@ -52,6 +52,14 @@ pub fn sdk_python_effective(host: &Host, window: &str) -> IdeResult<Option<ide_c
Ok(ide_sdk::apply::python_interpreter(&project.root, &project.settings.effective()).map(Into::into))
}
/// The PHP interpreter chosen in Settings › Languages & Frameworks › PHP; `None`: the `php` on `PATH`.
pub fn sdk_php_effective(host: &Host, window: &str) -> IdeResult<Option<ide_core::IpcPath>> {
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 },
+1
View File
@@ -398,6 +398,7 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
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,
+7
View File
@@ -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<Option<ide_core::IpcPath>> {
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]
+2
View File
@@ -772,6 +772,8 @@ export const commands = {
sdkDetect: (kind: SdkKind) => typedError<Sdk[], IdeError>(__TAURI_INVOKE("sdk_detect", { kind })),
/** The interpreter the project uses: the configured one, else its virtualenv. */
sdkPythonEffective: () => typedError<string | null, IdeError>(__TAURI_INVOKE("sdk_python_effective")),
/** The PHP interpreter chosen in Settings › Languages & Frameworks › PHP; `None`: the `php` on `PATH`. */
sdkPhpEffective: () => typedError<string | null, IdeError>(__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<PhpLanguageLevels, IdeError>(__TAURI_INVOKE("sdk_php_language_level", { path })),
/**
+7 -1
View File
@@ -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);
}
+9
View File
@@ -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<?php echo 1;"), { ...ALL, php: php81 })).toMatchObject({
command: php81,
args: ["tool"],
});
expect(produceContext(scratch("a.php"), env({}))!.missing!.body).toMatch(/Settings › Languages & Frameworks › PHP/);
expect(run(scratch("a.pl"))).toMatchObject({ command: "/bin/perl" });
expect(run(scratch("a.lua"))).toMatchObject({ command: "/bin/lua" });
expect(run(scratch("a.R"))).toMatchObject({ command: "/bin/Rscript", args: ["a.R"] });
+7 -1
View File
@@ -38,6 +38,8 @@ export interface ToolEnv {
windows?: boolean;
/** The project interpreter from settings (Python). */
python?: string | null;
/** The PHP interpreter chosen in settings (else the `php` on `PATH`). */
php?: string | null;
/** The project root has `deno.json(c)` / `bun.lock(b)`. */
deno?: boolean;
bun?: boolean;
@@ -288,7 +290,7 @@ const INSTALL: Record<string, string> = {
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":
+12 -1
View File
@@ -86,6 +86,14 @@ async function projectPython(): Promise<string | null> {
}
}
async function projectPhp(): Promise<string | null> {
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<ToolEnv> {
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,
};
}