Порівняти коміти

...
Автор SHA1 Повідомлення Дата
arookieofc 504bbdf259 fix(debug): load run configuration .env files for debug launches
Debug launches only copied the explicit env map into the DAP launch
arguments and dropped the configuration's .env files, which Run loads
via ide_run::envfile. Add a run_env_files command that loads the files
with the same loader over the system environment, and merge the result
under the explicit variables when building debug launches (generic,
plugin-hosted and java-debug paths); explicit variables still win and a
missing file fails the launch with the same error Run reports.

Fixes #753 (comment): .env applied in Run but ignored in Debug.
2026-11-11 10:04:02 +08:00
9 змінених файлів з 95 додано та 7 видалено

No files matched your search

+12
Переглянути файл
@@ -130,6 +130,17 @@ pub fn run_system_env(_host: &Host, _window: &str) -> std::collections::BTreeMap
ide_run::system_env()
}
/// Variables of the `.env` files a debug launch names, in order (later files win):
/// the same files a run loads, over the system environment debug adapters inherit.
pub fn run_env_files(
_host: &Host,
_window: &str,
files: Vec<std::path::PathBuf>,
) -> IdeResult<std::collections::BTreeMap<String, String>> {
ide_run::envfile::load(&files, &ide_run::system_env())
.map_err(|e| IdeError::not_found(format!("cannot read .env file: {e}")))
}
/// Writes the Gradle init script that adds `jvm_args` to the JVMs the build forks
/// (`bootRun`, `run`, tests) and returns its path, for `--init-script`.
pub async fn run_gradle_jvm_args_script(_host: &Host, _window: &str, jvm_args: Vec<String>) -> IdeResult<String> {
@@ -332,6 +343,7 @@ pub(crate) fn register(d: &mut Dispatcher) {
run_import_configs(&c.host, &c.window, c.arg("source")?, c.arg("existing")?)
});
d.add("run_system_env", |c| async move { Ok(run_system_env(&c.host, &c.window)) });
d.add("run_env_files", |c| async move { run_env_files(&c.host, &c.window, c.arg("files")?) });
d.add("run_gradle_jvm_args_script", |c| async move {
run_gradle_jvm_args_script(&c.host, &c.window, c.arg("jvmArgs")?).await
});
+19
Переглянути файл
@@ -157,3 +157,22 @@ async fn gradle_init_scripts_are_written_by_the_host_that_runs_the_build() {
let escaped = ops::run::run_gradle_init_script(&host, "main", "../escape".into(), String::new()).await;
assert!(escaped.is_err(), "{escaped:?}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn debug_env_files_load_in_order_like_a_run() {
let tmp = tempfile::tempdir().unwrap();
let host = host(tmp.path());
let first = tmp.path().join("first.env");
let second = tmp.path().join("second.env");
std::fs::write(&first, "SHARED=one\nONLY_FIRST=1\nREF=${SHARED}-suffix\n").unwrap();
std::fs::write(&second, "SHARED=two\nONLY_SECOND=2\n").unwrap();
let vars = ops::run::run_env_files(&host, "main", vec![first, second]).unwrap();
// Later files win; references resolve against earlier files, then the system environment.
assert_eq!(vars.get("SHARED").map(String::as_str), Some("two"));
assert_eq!(vars.get("ONLY_FIRST").map(String::as_str), Some("1"));
assert_eq!(vars.get("ONLY_SECOND").map(String::as_str), Some("2"));
assert_eq!(vars.get("REF").map(String::as_str), Some("one-suffix"));
// An unreadable file is an error, like a run that names a missing `.env`.
let missing = ops::run::run_env_files(&host, "main", vec![tmp.path().join("nope.env")]);
assert!(missing.is_err(), "{missing:?}");
}
+1
Переглянути файл
@@ -370,6 +370,7 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
run::run_import_sources,
run::run_import_configs,
run::run_system_env,
run::run_env_files,
run::run_start,
run::run_stop,
run::run_stdin,
+11
Переглянути файл
@@ -179,6 +179,17 @@ pub fn run_system_env(state: State<'_, AppState>) -> std::collections::BTreeMap<
ide_host::ops::run::run_system_env(&state.host, "")
}
/// Variables of the `.env` files a debug launch names, for the debugger to start with.
#[tauri::command]
#[specta::specta]
pub fn run_env_files(
window: Window,
state: State<'_, AppState>,
files: Vec<std::path::PathBuf>,
) -> IdeResult<std::collections::BTreeMap<String, String>> {
ide_host::ops::run::run_env_files(&state.host, window.label(), files)
}
/// Writes the Gradle init script adding `jvm_args` to the JVMs the build forks; returns its path.
#[tauri::command]
#[specta::specta]
+2
Переглянути файл
@@ -708,6 +708,8 @@ export const commands = {
runImportConfigs: (source: RunImportSource, existing: RunConfig_Deserialize[]) => typedError<RunImport_Serialize, IdeError>(__TAURI_INVOKE("run_import_configs", { source, existing })),
/** The environment runs inherit when "Include system environment variables" is on. */
runSystemEnv: () => __TAURI_INVOKE<{ [key in string]: string }>("run_system_env"),
/** Variables of the `.env` files a debug launch names, for the debugger to start with. */
runEnvFiles: (files: string[]) => typedError<{ [key in string]: string }, IdeError>(__TAURI_INVOKE("run_env_files", { files })),
runStart: (config: RunConfig_Deserialize, onOutput: Channel<OutputChunk>, request: number | null) => typedError<RunInfo, IdeError>(__TAURI_INVOKE("run_start", { config, onOutput, request })),
runStop: (id: RunId) => typedError<null, IdeError>(__TAURI_INVOKE("run_stop", { id })),
runStdin: (id: RunId, data: string) => typedError<null, IdeError>(__TAURI_INVOKE("run_stdin", { id, data })),
+32 -3
Переглянути файл
@@ -1,7 +1,18 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { RunConfig } from "@/platform/ipc";
import { usePlatform } from "@/platform/platformInfo";
import { debugUnsupportedReason, launchConfigFor } from "../launch";
import { debugUnsupportedReason, launchConfigFor, withEnvFiles } from "../launch";
const ipc = vi.hoisted(() => {
const ok = <T>(data: T) => Promise.resolve({ status: "ok" as const, data });
return { ok, runEnvFiles: vi.fn((_files: string[]) => ok({} as Record<string, string>)) };
});
vi.mock("@/platform/ipc", async (importOriginal) => {
const real = await importOriginal<typeof import("@/platform/ipc")>();
const { ok: _ok, ...cmds } = ipc;
return { ...real, commands: { ...real.commands, ...cmds } };
});
const rc = (p: Partial<RunConfig>): RunConfig => ({
id: "x",
@@ -20,7 +31,10 @@ const rc = (p: Partial<RunConfig>): RunConfig => ({
const extra = (l: { extraJson: string | null } | null) => JSON.parse(l?.extraJson ?? "{}");
const originalPlatform = usePlatform.getState();
beforeEach(() => usePlatform.setState({ os: "linux" }));
beforeEach(() => {
usePlatform.setState({ os: "linux" });
vi.clearAllMocks();
});
afterEach(() => usePlatform.setState(originalPlatform));
describe("run config → launch config", () => {
@@ -484,3 +498,18 @@ it("raw and typed Go debug configurations inherit custom tags unless explicitly
}
}
});
describe("withEnvFiles", () => {
it("leaves configurations without .env files alone (no backend call)", async () => {
const config = rc({ env: { A: "1" } });
await expect(withEnvFiles(config, "/p")).resolves.toBe(config);
expect(ipc.runEnvFiles).not.toHaveBeenCalled();
});
it("loads .env files under the explicit variables, resolved against the project root", async () => {
ipc.runEnvFiles.mockResolvedValueOnce({ status: "ok", data: { FROM_FILE: "file", A: "file" } });
const out = await withEnvFiles(rc({ env: { A: "explicit" }, envFiles: [".env", "env/local.env"] }), "/p");
expect(ipc.runEnvFiles).toHaveBeenCalledWith(["/p/.env", "/p/env/local.env"]);
expect(out.env).toEqual({ FROM_FILE: "file", A: "explicit" });
});
});
+3 -3
Переглянути файл
@@ -31,7 +31,7 @@ import { fieldAt, methodAt } from "./breakpoints/symbols";
import { toggleBreakpointSmart } from "./breakpoints/toggle";
import { isJavaAttachTarget, isJavaDebuggable, javaAttachConfig, javaLaunchConfig } from "./java";
import { cancelHotSwapBuild, reloadChangedClasses } from "./hotswap";
import { debugUnsupportedReason, launchConfigFor } from "./launch";
import { debugUnsupportedReason, launchConfigFor, withEnvFiles } from "./launch";
import {
isPythonAttachTarget,
isRemoteConfig,
@@ -331,7 +331,7 @@ async function startConfig(arg: unknown) {
// 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);
const launch = launchConfigFor(await withEnvFiles(prepared, root), root, php);
if (!launch) {
notifications().notify({
group: "run",
@@ -417,7 +417,7 @@ export function registerDebugActions(): () => void {
const config = await call(commands.pluginPrepareDebugRun(a.config));
const root = useProject.getState().project?.root;
if (!root) throw new Error("No project");
const launch = launchConfigFor(config, root);
const launch = launchConfigFor(await withEnvFiles(config, root), root);
if (!launch) throw new Error("This configuration has no debugger");
let id: number | null = null;
let stopped = false;
+2
Переглянути файл
@@ -16,6 +16,7 @@ import {
type JavaPreparation,
} from "@/platform/javaLaunch";
import { maybeEditorService, notifications } from "@/workbench/contributions";
import { withEnvFiles } from "./launch";
export const JAVA_ADAPTER = "java-debug";
export const KOTLIN_ADAPTER = "kotlin-debug-adapter";
@@ -111,6 +112,7 @@ export async function javaLaunchConfig(
preparation?: JavaPreparation,
): Promise<LaunchConfig> {
const cwd = rc.cwd ? join(root, rc.cwd) : root;
rc = await withEnvFiles(rc, root);
const parsed = parseJavaArgs(rc.args);
if (!parsed.mainClass) throw new Error(`No main class in '${rc.name}'`);
preparation?.signal.throwIfAborted();
+13 -1
Переглянути файл
@@ -1,7 +1,7 @@
import { parseArgs, formatArgs } from "@/platform/commandLine";
/** Run configuration → DAP launch configuration, per adapter. */
import { workflowSettings } from "@/platform/workflowSettings";
import type { LaunchConfig, RunConfig } from "@/platform/ipc";
import { call, commands, type LaunchConfig, type RunConfig } from "@/platform/ipc";
import { hostOs } from "@/platform/remote";
function isAbsolute(p: string) {
@@ -37,6 +37,18 @@ function base(rc: RunConfig, adapter: string, cwd: string): LaunchConfig {
};
}
/**
* `rc` with its `.env` files loaded underneath its explicit variables (mirrors Run):
* Debug used to drop them because `base` only copies `env`. Absolute paths go to the
* backend, which parses with the same loader runs use.
*/
export async function withEnvFiles(rc: RunConfig, root: string): Promise<RunConfig> {
const files = rc.envFiles?.filter((f) => f.trim()) ?? [];
if (!files.length) return rc;
const loaded = await call(commands.runEnvFiles(files.map((f) => join(root, f))));
return { ...rc, env: { ...loaded, ...rc.env } };
}
function cargoLaunch(rc: RunConfig, cwd: string, adapter: string): LaunchConfig {
const [cargoArgs, programArgs] = splitAtDashDash(rc.args);
const [sub, ...rest] = cargoArgs;