From 96be40cd840a9635fc91a6b194204b98a16ae0a1 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Wed, 28 Oct 2026 13:50:20 +0800 Subject: [PATCH] fix(process): Give child processes the host environment inside an AppImage (#118) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #117 Inside an AppImage, AppRun and the linuxdeploy GTK/GStreamer hooks point LD_LIBRARY_PATH, PATH, XDG_DATA_DIRS, GIO_MODULE_DIR, GTK_PATH, GST_PLUGIN_* … into the mounted image, and every child inherited them, so host programs loaded the image's older libraries (curl + bundled nghttp2 → symbol lookup error when installing Kotlin LSP). ide_core::process::prepare (formerly no_window) now gives children the host environment back: entries inside $APPDIR are dropped (a _ORIG wins), emptied variables and the runtime's launch variables are removed, explicit call-site values are kept; the IDE process itself is unchanged. Covers all spawn sites that used no_window plus terminal/run PTYs, run configuration environments and stop commands, login-shell probes, attach ps/kill, docker PATH, tool search PATH and the Linux browser opener. No-op outside an AppImage. Not verified on a Linux AppImage. Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn --- crates/ide-build/src/process.rs | 2 +- crates/ide-core/src/process.rs | 215 ++++++++++++++++++++++++++++++- crates/ide-coverage/src/lib.rs | 2 +- crates/ide-dap/src/adapter.rs | 4 +- crates/ide-dap/src/attach.rs | 6 +- crates/ide-dap/src/cargo.rs | 2 +- crates/ide-dap/src/session.rs | 2 +- crates/ide-docker/src/lib.rs | 2 +- crates/ide-docker/src/process.rs | 2 +- crates/ide-github/src/process.rs | 2 +- crates/ide-host/src/shell_env.rs | 2 +- crates/ide-jsonrpc/src/lib.rs | 2 +- crates/ide-lsp/src/launch.rs | 4 +- crates/ide-lsp/src/tcp.rs | 2 +- crates/ide-profiler/src/lib.rs | 2 +- crates/ide-run/src/lib.rs | 2 +- crates/ide-run/src/process.rs | 12 +- crates/ide-run/src/wrap.rs | 2 +- crates/ide-sdk/src/exec.rs | 4 +- crates/ide-terminal/src/lib.rs | 6 + crates/ide-tools/src/archive.rs | 2 +- crates/ide-tools/src/lib.rs | 4 +- crates/ide-tools/src/process.rs | 6 +- crates/ide-vcs/src/cli.rs | 4 +- crates/ide-web/src/browsers.rs | 36 +++++- 25 files changed, 287 insertions(+), 42 deletions(-) diff --git a/crates/ide-build/src/process.rs b/crates/ide-build/src/process.rs index 1e129fd3..ac59e8e9 100644 --- a/crates/ide-build/src/process.rs +++ b/crates/ide-build/src/process.rs @@ -52,7 +52,7 @@ pub(crate) fn run( use std::os::unix::process::CommandExt; command.process_group(0); } - ide_core::process::no_window(&mut command); + ide_core::process::prepare(&mut command); let mut child = command.spawn().map_err(|e| IdeError::unavailable(format!("cannot start {program}: {e}")))?; let (tx, rx) = mpsc::channel::<(Stream, String)>(); let pump = |stream: Box, kind: Stream, tx: mpsc::Sender<(Stream, String)>| { diff --git a/crates/ide-core/src/process.rs b/crates/ide-core/src/process.rs index 3ccb3853..dd5a06cb 100644 --- a/crates/ide-core/src/process.rs +++ b/crates/ide-core/src/process.rs @@ -5,22 +5,121 @@ use std::process::Command; use std::sync::OnceLock; /// Windows `CREATE_NO_WINDOW` creation flag, for call sites that combine it -/// with other flags. Prefer [`no_window`] otherwise. +/// with other flags (and then call [`host_env`] themselves). Prefer [`prepare`] otherwise. pub const CREATE_NO_WINDOW: u32 = 0x0800_0000; -/// Keeps a console child (java, node, curl, a language server, ...) from -/// opening its own console window on Windows. Release builds use the GUI -/// subsystem, so without this every spawn flashes a `cmd` window. A no-op -/// elsewhere. For a `tokio::process::Command`, pass `cmd.as_std_mut()`. -pub fn no_window(cmd: &mut Command) -> &mut Command { +/// Readies `cmd` for spawning any child (java, node, curl, git, a language server, ...). +/// For a `tokio::process::Command`, pass `cmd.as_std_mut()`. +/// +/// - Windows: release builds use the GUI subsystem, so without `CREATE_NO_WINDOW` every +/// console child flashes a `cmd` window. +/// - Linux AppImage: see [`host_env`]. +/// +/// Variables the call site has already set are kept; call it before `env_clear`. +pub fn prepare(cmd: &mut Command) -> &mut Command { #[cfg(windows)] { use std::os::windows::process::CommandExt; cmd.creation_flags(CREATE_NO_WINDOW); } + host_env(cmd) +} + +/// Gives `cmd` the host's environment back when the IDE runs from an AppImage. `AppRun` and +/// its hooks point `LD_LIBRARY_PATH`, `PATH`, `XDG_DATA_DIRS`, `GIO_MODULE_DIR`, `GTK_PATH`, +/// `GST_PLUGIN_*`, ... into the mounted image for the IDE itself; a host program that +/// inherits them loads the image's (older) libraries and plugins next to its own, e.g. +/// `curl: symbol lookup error ... nghttp2_...`. The IDE process keeps its environment +/// (WebKit and GTK still need it). Variables the call site has already set are kept. +/// A no-op outside an AppImage. +pub fn host_env(cmd: &mut Command) -> &mut Command { + let fixes = appimage_fixes(); + if fixes.is_empty() { + return cmd; + } + let explicit: Vec = cmd.get_envs().map(|(k, _)| k.to_owned()).collect(); + for (key, value) in fixes.into_iter().filter(|(k, _)| !explicit.contains(k)) { + match value { + Some(v) => cmd.env(key, v), + None => cmd.env_remove(key), + }; + } cmd } +/// The [`host_env`] changes as `(name, value)` pairs, `None` meaning "remove", for spawners +/// that are not a `std::process::Command` (a PTY `CommandBuilder`). Apply before the call +/// site's own variables. Empty outside an AppImage. +pub fn host_env_fixes() -> Vec<(OsString, Option)> { + appimage_fixes() +} + +/// This process's environment as a child should inherit it (see [`host_env`]), for call +/// sites that build a child's environment from the IDE's own instead of inheriting it. +pub fn host_vars() -> impl Iterator { + let fixes = appimage_fixes(); + std::env::vars_os().filter_map(move |(k, v)| match fixes.iter().find(|(f, _)| *f == k) { + Some((_, fixed)) => fixed.clone().map(|v| (k, v)), + None => Some((k, v)), + }) +} + +/// [`std::env::var_os`] as a child should see it (see [`host_env`]). +pub fn host_var_os(key: impl AsRef) -> Option { + let key = key.as_ref(); + match appimage_fixes().into_iter().find(|(k, _)| k == key) { + Some((_, fixed)) => fixed, + None => std::env::var_os(key), + } +} + +/// Read on every call, not cached: startup rewrites `PATH` (login shell, managed tools). +fn appimage_fixes() -> Vec<(OsString, Option)> { + if !cfg!(target_os = "linux") || std::env::var_os("APPDIR").is_none() { + return Vec::new(); + } + appimage_env_fixes(&std::env::vars_os().collect::>()) +} + +/// Variables the AppImage runtime sets to describe the IDE's own launch. +const APPIMAGE_LAUNCH_VARS: [&str; 4] = ["APPDIR", "APPIMAGE", "ARGV0", "OWD"]; + +/// What to change in `vars` (an environment) so a child sees the host's environment rather +/// than the AppImage's: every `:`-separated entry inside `$APPDIR` is dropped (a saved +/// `_ORIG` wins when present), variables left empty are removed, and so are the +/// runtime's own launch variables. Nothing changes when `APPDIR` is not set. +fn appimage_env_fixes(vars: &[(OsString, OsString)]) -> Vec<(OsString, Option)> { + let get = |name: &str| vars.iter().find(|(k, _)| k == name).map(|(_, v)| v); + let Some(appdir) = + get("APPDIR").and_then(|v| v.to_str()).map(|v| v.trim_end_matches('/')).filter(|v| !v.is_empty()) + else { + return Vec::new(); + }; + let inside = |entry: &str| entry.strip_prefix(appdir).is_some_and(|rest| rest.is_empty() || rest.starts_with('/')); + let mut fixes = Vec::new(); + for (key, value) in vars { + let (Some(name), Some(value)) = (key.to_str(), value.to_str()) else { continue }; + let fixed = if APPIMAGE_LAUNCH_VARS.contains(&name) { + None + } else if value.split(':').any(inside) { + match get(&format!("{name}_ORIG")) { + Some(orig) => Some(orig.clone()), + None => { + let kept: Vec<&str> = value.split(':').filter(|e| !e.is_empty() && !inside(e)).collect(); + Some(OsString::from(kept.join(":"))) + } + } + } else if name == "GTK_THEME" && value.is_empty() { + // linuxdeploy-plugin-gtk exports `GTK_THEME="$APPIMAGE_GTK_THEME"`, usually empty. + None + } else { + continue; + }; + fixes.push((key.clone(), fixed.filter(|v| !v.is_empty()))); + } + fixes +} + static SHELL_ENV: OnceLock> = OnceLock::new(); /// Records the login shell's environment, read once at startup off the main thread. @@ -50,4 +149,108 @@ mod tests { let vars: Vec<_> = shell_env().collect(); assert_eq!(vars, [(OsStr::new("OPENIDEA_SHELL_ONLY_VAR"), OsStr::new("1"))]); } + + fn env(pairs: &[(&str, &str)]) -> Vec<(OsString, OsString)> { + pairs.iter().map(|(k, v)| (OsString::from(k), OsString::from(v))).collect() + } + + fn fixes(pairs: &[(&str, &str)]) -> Vec<(String, Option)> { + appimage_env_fixes(&env(pairs)) + .into_iter() + .map(|(k, v)| (k.into_string().unwrap(), v.map(|v| v.into_string().unwrap()))) + .collect() + } + + fn fix(name: &str, value: Option<&str>) -> (String, Option) { + (name.to_owned(), value.map(str::to_owned)) + } + + #[test] + fn appimage_entries_are_dropped_and_host_entries_kept() { + let got = fixes(&[ + ("APPDIR", "/tmp/.mount_OpenIDabc"), + ( + "LD_LIBRARY_PATH", + "/tmp/.mount_OpenIDabc/usr/lib/:/tmp/.mount_OpenIDabc/usr/lib/x86_64-linux-gnu/:/opt/cuda/lib64:/usr/local/lib", + ), + ("PATH", "/tmp/.mount_OpenIDabc/usr/bin/:/tmp/.mount_OpenIDabc/usr/sbin/:/usr/local/bin:/usr/bin"), + ("XDG_DATA_DIRS", "/tmp/.mount_OpenIDabc/usr/share:/usr/share:/usr/local/share:/usr/share"), + ("HOME", "/home/me"), + ("JAVA_HOME", "/tmp/.mount_OpenIDabcdef/jdk"), + ]); + assert_eq!( + got, + [ + fix("APPDIR", None), + fix("LD_LIBRARY_PATH", Some("/opt/cuda/lib64:/usr/local/lib")), + fix("PATH", Some("/usr/local/bin:/usr/bin")), + fix("XDG_DATA_DIRS", Some("/usr/share:/usr/local/share:/usr/share")), + ] + ); + } + + #[test] + fn variables_left_empty_are_removed() { + // AppRun prepends to an unset variable, leaving a trailing `:`; hooks set single paths. + let got = fixes(&[ + ("APPIMAGE", "/opt/openidea/OpenIDEA.AppImage"), + ("APPDIR", "/tmp/.mount_x/"), + ("ARGV0", "/opt/openidea/OpenIDEA.AppImage"), + ("OWD", "/home/me"), + ("LD_LIBRARY_PATH", "/tmp/.mount_x/usr/lib/:/tmp/.mount_x/lib/:"), + ("GIO_MODULE_DIR", "/tmp/.mount_x/usr/lib/gio/modules"), + ("GTK_DATA_PREFIX", "/tmp/.mount_x"), + ("GDK_PIXBUF_MODULE_FILE", "/tmp/.mount_x/usr/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache"), + ("GST_PLUGIN_SYSTEM_PATH_1_0", "/tmp/.mount_x/usr/lib/gstreamer-1.0"), + ("GTK_THEME", ""), + ]); + let names: Vec<_> = got.iter().filter(|(_, v)| v.is_none()).map(|(k, _)| k.as_str()).collect(); + assert_eq!( + names, + [ + "APPIMAGE", + "APPDIR", + "ARGV0", + "OWD", + "LD_LIBRARY_PATH", + "GIO_MODULE_DIR", + "GTK_DATA_PREFIX", + "GDK_PIXBUF_MODULE_FILE", + "GST_PLUGIN_SYSTEM_PATH_1_0", + "GTK_THEME", + ] + ); + assert_eq!(names.len(), got.len()); + } + + #[test] + fn a_saved_original_value_wins() { + let got = fixes(&[ + ("APPDIR", "/tmp/.mount_x"), + ("LD_LIBRARY_PATH", "/tmp/.mount_x/usr/lib:/opt/lib"), + ("LD_LIBRARY_PATH_ORIG", "/opt/lib:/home/me/lib"), + ("PYTHONHOME", "/tmp/.mount_x/usr"), + ("PYTHONHOME_ORIG", ""), + ]); + assert_eq!( + got, + [fix("APPDIR", None), fix("LD_LIBRARY_PATH", Some("/opt/lib:/home/me/lib")), fix("PYTHONHOME", None)] + ); + } + + #[test] + fn outside_an_appimage_nothing_changes() { + let plain = [("LD_LIBRARY_PATH", "/opt/lib:"), ("PATH", "/usr/bin"), ("GTK_THEME", "")]; + assert!(fixes(&plain).is_empty()); + assert!(fixes(&[("APPDIR", ""), ("PATH", "/usr/bin")]).is_empty()); + assert!(fixes(&[("APPDIR", "/"), ("PATH", "/usr/bin")]).is_empty()); + } + + #[test] + fn host_env_keeps_what_the_call_site_set() { + let mut cmd = Command::new("true"); + cmd.env("PATH", "/custom"); + prepare(&mut cmd); + assert_eq!(cmd.get_envs().find(|(k, _)| *k == "PATH").and_then(|(_, v)| v), Some(OsStr::new("/custom"))); + } } diff --git a/crates/ide-coverage/src/lib.rs b/crates/ide-coverage/src/lib.rs index 0df488bf..1fc48db2 100644 --- a/crates/ide-coverage/src/lib.rs +++ b/crates/ide-coverage/src/lib.rs @@ -206,7 +206,7 @@ pub fn collect(spec: &CoverageReportSpec) -> IdeResult { CoverageReportSpec::Exported { runner, argv, cwd, env, lcov } => { let (program, args) = argv.split_first().ok_or_else(|| IdeError::invalid("empty coverage export command"))?; - let out = ide_core::process::no_window(&mut Command::new(program)) + let out = ide_core::process::prepare(&mut Command::new(program)) .args(args) .current_dir(cwd) .envs(env) diff --git a/crates/ide-dap/src/adapter.rs b/crates/ide-dap/src/adapter.rs index c9537256..5aa690ec 100644 --- a/crates/ide-dap/src/adapter.rs +++ b/crates/ide-dap/src/adapter.rs @@ -257,7 +257,7 @@ pub async fn prepare_debugpy(spec: &mut AdapterSpec) -> IdeResult<()> { return Ok(()); // Reported as a missing adapter when it is started. }; let mut probe = Command::new(&python); - ide_core::process::no_window(probe.as_std_mut()); + ide_core::process::prepare(probe.as_std_mut()); let probe = probe .args(["-c", "import debugpy.adapter"]) .current_dir(&spec.cwd) @@ -396,7 +396,7 @@ impl AdapterLauncher for ProcessLauncher { None => args, }; let mut cmd = Command::new(&binary); - ide_core::process::no_window(cmd.as_std_mut()); + ide_core::process::prepare(cmd.as_std_mut()); cmd.args(&args) .envs(spec.env.iter().map(|(k, v)| (k, v))) .current_dir(&spec.cwd) diff --git a/crates/ide-dap/src/attach.rs b/crates/ide-dap/src/attach.rs index 9eb9edfa..c2ab2417 100644 --- a/crates/ide-dap/src/attach.rs +++ b/crates/ide-dap/src/attach.rs @@ -170,7 +170,7 @@ pub fn is_go_binary(exe: &str) -> bool { pub fn list_processes() -> IdeResult> { use std::collections::HashMap; let ps = |fields: &str| -> IdeResult { - let out = std::process::Command::new("ps") + let out = ide_core::process::prepare(&mut std::process::Command::new("ps")) .args(["-x", "-ww", "-o", fields]) .output() .map_err(|e| IdeError::unavailable(format!("cannot list processes: {e}")))?; @@ -212,7 +212,9 @@ pub const NODE_INSPECTOR_PORT: u16 = 9229; /// picker does) and waits until it listens. Returns the inspector port. #[cfg(unix)] pub async fn enable_node_inspector(pid: u32) -> IdeResult { - let status = tokio::process::Command::new("kill") + let mut kill = tokio::process::Command::new("kill"); + ide_core::process::prepare(kill.as_std_mut()); + let status = kill .args(["-USR1", &pid.to_string()]) .status() .await diff --git a/crates/ide-dap/src/cargo.rs b/crates/ide-dap/src/cargo.rs index a96ca00a..6de0813b 100644 --- a/crates/ide-dap/src/cargo.rs +++ b/crates/ide-dap/src/cargo.rs @@ -55,7 +55,7 @@ pub async fn build( ) -> IdeResult { let cargo = find_binary("cargo", cwd).ok_or_else(|| IdeError::unavailable("`cargo` not found on PATH"))?; let mut cmd = tokio::process::Command::new(cargo); - ide_core::process::no_window(cmd.as_std_mut()); + ide_core::process::prepare(cmd.as_std_mut()); cmd.args(args) .arg("--message-format=json-render-diagnostics") .current_dir(cwd) diff --git a/crates/ide-dap/src/session.rs b/crates/ide-dap/src/session.rs index 6219f4bf..d48e14b0 100644 --- a/crates/ide-dap/src/session.rs +++ b/crates/ide-dap/src/session.rs @@ -421,7 +421,7 @@ pub(crate) fn run_in_terminal(s: &Arc, args: &Value) -> Result Option { pub fn run_env(ep: &DockerEndpoint) -> std::collections::BTreeMap { let mut env = ep.env_vars(); if let Some(dir) = docker_executable().as_deref().and_then(Path::parent) { - let current = std::env::var_os("PATH").unwrap_or_default(); + let current = ide_core::process::host_var_os("PATH").unwrap_or_default(); let mut dirs: Vec = vec![dir.to_path_buf()]; dirs.extend(std::env::split_paths(¤t).filter(|d| d != dir)); if let Ok(path) = std::env::join_paths(dirs) { diff --git a/crates/ide-docker/src/process.rs b/crates/ide-docker/src/process.rs index 5deb05e3..e9dc25a3 100644 --- a/crates/ide-docker/src/process.rs +++ b/crates/ide-docker/src/process.rs @@ -25,7 +25,7 @@ fn prepare(cmd: &mut Command) { use std::os::unix::process::CommandExt; cmd.process_group(0); } - ide_core::process::no_window(cmd); + ide_core::process::prepare(cmd); } fn spawn(cmd: &mut Command) -> IdeResult { diff --git a/crates/ide-github/src/process.rs b/crates/ide-github/src/process.rs index 4a574fcb..f1dc29f4 100644 --- a/crates/ide-github/src/process.rs +++ b/crates/ide-github/src/process.rs @@ -25,7 +25,7 @@ fn prepare(cmd: &mut Command) { use std::os::unix::process::CommandExt; cmd.process_group(0); } - ide_core::process::no_window(cmd); + ide_core::process::prepare(cmd); } fn spawn(cmd: &mut Command) -> IdeResult { diff --git a/crates/ide-host/src/shell_env.rs b/crates/ide-host/src/shell_env.rs index d00d2a67..6d0e2dee 100644 --- a/crates/ide-host/src/shell_env.rs +++ b/crates/ide-host/src/shell_env.rs @@ -79,7 +79,7 @@ fn read_login_env(timeout: std::time::Duration) -> Option std::io::Result { use std::process::Stdio; let mut cmd = tokio::process::Command::new(command); - ide_core::process::no_window(cmd.as_std_mut()); + ide_core::process::prepare(cmd.as_std_mut()); cmd.args(args) .current_dir(cwd) .envs(ide_core::process::shell_env()) diff --git a/crates/ide-lsp/src/launch.rs b/crates/ide-lsp/src/launch.rs index 232eec69..12fa92fe 100644 --- a/crates/ide-lsp/src/launch.rs +++ b/crates/ide-lsp/src/launch.rs @@ -116,7 +116,7 @@ fn resolve_rustup_proxy(command: &str, binary: PathBuf) -> Option { if !is_proxy || command.contains('/') { return Some(binary); } - let works = ide_core::process::no_window(&mut std::process::Command::new(&binary)) + let works = ide_core::process::prepare(&mut std::process::Command::new(&binary)) .arg("--version") .stdin(std::process::Stdio::null()) .stdout(std::process::Stdio::null()) @@ -190,7 +190,7 @@ fn login_shell_path() -> Option { use std::time::{Duration, Instant}; let shell = std::env::var_os("SHELL")?; const MARK: &str = "__OPENIDEA_PATH__"; - let mut child = Command::new(shell) + let mut child = ide_core::process::prepare(&mut Command::new(shell)) .args(["-l", "-c", &format!("printf '{MARK}%s{MARK}' \"$PATH\"")]) .stdin(Stdio::null()) .stdout(Stdio::piped()) diff --git a/crates/ide-lsp/src/tcp.rs b/crates/ide-lsp/src/tcp.rs index 9775570d..66e48d3e 100644 --- a/crates/ide-lsp/src/tcp.rs +++ b/crates/ide-lsp/src/tcp.rs @@ -94,7 +94,7 @@ pub(crate) fn connect_or_start( let args = substitute_args(&config.args, port, root); let mut cmd = tokio::process::Command::new(binary); - ide_core::process::no_window(cmd.as_std_mut()); + ide_core::process::prepare(cmd.as_std_mut()); cmd.args(&args) .current_dir(root) .envs(env.iter().map(|(k, v)| (k.as_str(), v.as_str()))) diff --git a/crates/ide-profiler/src/lib.rs b/crates/ide-profiler/src/lib.rs index ae4f7872..e3d5090a 100644 --- a/crates/ide-profiler/src/lib.rs +++ b/crates/ide-profiler/src/lib.rs @@ -161,7 +161,7 @@ pub fn collect(spec: &ProfileSpec) -> IdeResult { if !data.is_file() { return Err(nothing_recorded("perf wrote no data")); } - let out = ide_core::process::no_window(&mut Command::new(perf)) + let out = ide_core::process::prepare(&mut Command::new(perf)) .args(["script", "-i"]) .arg(data.as_ref() as &Path) .current_dir(cwd) diff --git a/crates/ide-run/src/lib.rs b/crates/ide-run/src/lib.rs index f4642380..f4a698a7 100644 --- a/crates/ide-run/src/lib.rs +++ b/crates/ide-run/src/lib.rs @@ -209,7 +209,7 @@ pub fn save_templates(root: &Path, templates: &[RunConfig]) -> IdeResult<()> { /// The environment runs inherit (unless "Include system environment variables" is off). pub fn system_env() -> BTreeMap { - std::env::vars().collect() + ide_core::process::host_vars().filter_map(|(k, v)| Some((k.into_string().ok()?, v.into_string().ok()?))).collect() } /// Path of the user run configuration file for `root`. diff --git a/crates/ide-run/src/process.rs b/crates/ide-run/src/process.rs index 729f757f..4d326bc8 100644 --- a/crates/ide-run/src/process.rs +++ b/crates/ide-run/src/process.rs @@ -407,6 +407,12 @@ impl ProcessManager { if env.clear { cmd.env_clear(); } else { + for (k, v) in ide_core::process::host_env_fixes() { + match v { + Some(v) => cmd.env(k, v), + None => cmd.env_remove(k), + } + } for (k, v) in ide_core::process::shell_env() { cmd.env(k, v); } @@ -602,7 +608,7 @@ fn launch_env(root: &Path, config: &RunConfig) -> IdeResult { BTreeMap::new() } else { let base: BTreeMap = - if extras.include_system_env { std::env::vars().collect() } else { BTreeMap::new() }; + if extras.include_system_env { crate::system_env() } else { BTreeMap::new() }; let paths: Vec = extras.env_files.iter().map(|f| resolve_cwd(root, Some(Path::new(f)))).collect(); crate::envfile::load(&paths, &base).map_err(|e| IdeError::not_found(format!("cannot read .env file: {e}")))? }; @@ -620,7 +626,7 @@ fn apply_env(cmd: &mut Command, env: &LaunchEnv) { if env.clear { cmd.env_clear(); } else { - cmd.envs(ide_core::process::shell_env()); + ide_core::process::host_env(cmd).envs(ide_core::process::shell_env()); } cmd.envs(&env.vars); } @@ -631,6 +637,7 @@ fn run_stop_command(stop: &StopCommand, output: &OutputFn) { output(OutputChunk::System { text: format!("{}\n", stop.line) }); let inv = shell_invocation(&stop.line, cfg!(windows), std::env::var("COMSPEC").ok()); let mut cmd = Command::new(&inv.program); + ide_core::process::prepare(&mut cmd); if inv.raw_args { add_raw_args(&mut cmd, &inv.args); } else { @@ -638,7 +645,6 @@ fn run_stop_command(stop: &StopCommand, output: &OutputFn) { } cmd.current_dir(&stop.cwd).stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::piped()); apply_env(&mut cmd, &stop.env); - ide_core::process::no_window(&mut cmd); let mut child = match cmd.spawn() { Ok(c) => c, Err(e) => { diff --git a/crates/ide-run/src/wrap.rs b/crates/ide-run/src/wrap.rs index 0e8aabc5..f3b73ea7 100644 --- a/crates/ide-run/src/wrap.rs +++ b/crates/ide-run/src/wrap.rs @@ -93,7 +93,7 @@ pub fn find_tool(name: &str, config: &RunConfig, dir: &Path, root: &Path) -> Opt pub fn probe(argv: &[String], dir: &Path, config: &RunConfig) -> bool { let Some((program, args)) = argv.split_first() else { return false }; let program = resolve(program, config); - ide_core::process::no_window(&mut Command::new(program)) + ide_core::process::prepare(&mut Command::new(program)) .args(args) .current_dir(dir) .envs(&config.env) diff --git a/crates/ide-sdk/src/exec.rs b/crates/ide-sdk/src/exec.rs index fd3806ae..4b0dd7af 100644 --- a/crates/ide-sdk/src/exec.rs +++ b/crates/ide-sdk/src/exec.rs @@ -59,7 +59,7 @@ pub(crate) fn query_in( if let Some(cwd) = cwd { cmd.current_dir(cwd); } - let mut child = ide_core::process::no_window(&mut cmd) + let mut child = ide_core::process::prepare(&mut cmd) .args(args) .env("PATH", search_path()) .envs(env.iter().copied()) @@ -112,7 +112,7 @@ pub(crate) fn stream( on_line: &dyn Fn(String), ) -> IdeResult { let mut cmd = Command::new(program); - ide_core::process::no_window(&mut cmd); + ide_core::process::prepare(&mut cmd); cmd.args(args).env("PATH", search_path()).stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()); cmd.envs(env.iter().map(|(k, v)| (k, v))); if let Some(cwd) = cwd { diff --git a/crates/ide-terminal/src/lib.rs b/crates/ide-terminal/src/lib.rs index 18814cb4..d651b23b 100644 --- a/crates/ide-terminal/src/lib.rs +++ b/crates/ide-terminal/src/lib.rs @@ -97,6 +97,12 @@ impl TerminalManager { let mut cmd = CommandBuilder::new(&shell); cmd.args(&args); cmd.cwd(resolve_cwd(&options.cwd)); + for (k, v) in ide_core::process::host_env_fixes() { + match v { + Some(v) => cmd.env(k, v), + None => cmd.env_remove(k), + } + } for (k, v) in env { cmd.env(k, v); } diff --git a/crates/ide-tools/src/archive.rs b/crates/ide-tools/src/archive.rs index 99dfc57e..a3e8e3cf 100644 --- a/crates/ide-tools/src/archive.rs +++ b/crates/ide-tools/src/archive.rs @@ -113,7 +113,7 @@ fn extract_zip(archive: &Path, dest: &Path) -> IdeResult<()> { /// `.tar.xz` goes through the system `tar` (bsdtar on macOS/Windows, GNU tar + xz on Linux). fn extract_tar_xz(archive: &Path, dest: &Path) -> IdeResult<()> { let tar = which::which("tar").map_err(|_| IdeError::unavailable("`tar` is required to unpack .tar.xz archives"))?; - let out = ide_core::process::no_window(&mut std::process::Command::new(tar)) + let out = ide_core::process::prepare(&mut std::process::Command::new(tar)) .arg("-xJf") .arg(archive) .arg("-C") diff --git a/crates/ide-tools/src/lib.rs b/crates/ide-tools/src/lib.rs index 6ac47b35..aad31ce0 100644 --- a/crates/ide-tools/src/lib.rs +++ b/crates/ide-tools/src/lib.rs @@ -165,7 +165,7 @@ pub fn extend_process_path(app_data: &Path) { /// `PATH` plus the usual per-user tool locations (GUI apps on macOS start with a minimal `PATH`). pub fn default_search_path() -> OsString { let mut dirs: Vec = - std::env::var_os("PATH").map(|p| std::env::split_paths(&p).collect()).unwrap_or_default(); + ide_core::process::host_var_os("PATH").map(|p| std::env::split_paths(&p).collect()).unwrap_or_default(); if let Some(home) = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")).map(PathBuf::from) { for rel in [".cargo/bin", ".local/bin", "go/bin", ".bun/bin", ".volta/bin", ".npm-global/bin", "Library/pnpm"] { dirs.push(home.join(rel)); @@ -343,7 +343,7 @@ impl Tools { fn command(&self, program: impl AsRef) -> Command { let mut cmd = Command::new(program); - ide_core::process::no_window(&mut cmd).env("PATH", &self.child_path); + ide_core::process::prepare(&mut cmd).env("PATH", &self.child_path); cmd } diff --git a/crates/ide-tools/src/process.rs b/crates/ide-tools/src/process.rs index 42a1a9d1..a5c994fc 100644 --- a/crates/ide-tools/src/process.rs +++ b/crates/ide-tools/src/process.rs @@ -48,7 +48,7 @@ pub fn run_streamed(cmd: Command, cancel: &CancellationToken, emit: Emit) -> Ide /// Like [`run_streamed`] but returns `(success, output, status text)` instead of failing on a non-zero exit. pub fn run_collect(mut cmd: Command, cancel: &CancellationToken, emit: Emit) -> IdeResult<(bool, String, String)> { emit(InstallEvent::Line { text: format!("$ {}", describe(&cmd)) }); - ide_core::process::no_window(&mut cmd).stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()); + ide_core::process::prepare(&mut cmd).stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()); let mut child = cmd.spawn().map_err(|e| IdeError::unavailable(format!("cannot run {}: {e}", describe(&cmd))))?; let (tx, rx) = mpsc::channel::(); let pipes: [Option>; 2] = [ @@ -94,7 +94,7 @@ pub fn run_collect(mut cmd: Command, cancel: &CancellationToken, emit: Emit) -> /// Runs `cmd` to completion within `timeout`; returns (success, stdout + stderr). pub fn run_quiet(mut cmd: Command, timeout: Duration) -> Option<(bool, String)> { - ide_core::process::no_window(&mut cmd).stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()); + ide_core::process::prepare(&mut cmd).stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped()); let mut child = cmd.spawn().ok()?; let mut stdout = child.stdout.take()?; let mut stderr = child.stderr.take()?; @@ -161,7 +161,7 @@ impl Http { fn base(&self, url: &str) -> Command { let mut cmd = Command::new(&self.curl); - ide_core::process::no_window(&mut cmd); + ide_core::process::prepare(&mut cmd); cmd.args(["-fsSL", "--retry", "2", "--connect-timeout", "20", "-A", "openidea"]); if url.starts_with("https://api.github.com/") && let Ok(token) = std::env::var("GITHUB_TOKEN").or_else(|_| std::env::var("GH_TOKEN")) diff --git a/crates/ide-vcs/src/cli.rs b/crates/ide-vcs/src/cli.rs index b80ef4c0..1f2e9f08 100644 --- a/crates/ide-vcs/src/cli.rs +++ b/crates/ide-vcs/src/cli.rs @@ -72,7 +72,7 @@ pub fn git_version(executable: Option<&Path>) -> IdeResult { }; let mut cmd = Command::new(&exe); cmd.arg("--version").stdin(Stdio::null()).env("LC_ALL", "C"); - ide_core::process::no_window(&mut cmd); + ide_core::process::prepare(&mut cmd); let out = cmd.output().map_err(|e| IdeError::unavailable(format!("cannot run {}: {e}", exe.display())))?; let text = String::from_utf8_lossy(&out.stdout).trim().to_string(); if !out.status.success() || !text.starts_with("git version") { @@ -106,7 +106,7 @@ pub(crate) fn command(root: &Path) -> IdeResult { for (k, v) in ASKPASS.read().unwrap_or_else(|e| e.into_inner()).iter() { cmd.env(k, v); } - ide_core::process::no_window(&mut cmd); + ide_core::process::prepare(&mut cmd); Ok(cmd) } diff --git a/crates/ide-web/src/browsers.rs b/crates/ide-web/src/browsers.rs index 1d8cfa91..45dbbf1c 100644 --- a/crates/ide-web/src/browsers.rs +++ b/crates/ide-web/src/browsers.rs @@ -96,18 +96,46 @@ pub fn open(url: &str, id: Option<&str>) -> IdeResult<()> { if !(url.starts_with("http://") || url.starts_with("https://") || url.starts_with("file://")) { return Err(IdeError::invalid(format!("not a web URL: {url}"))); } - let result = match id { - None => open::that_detached(url), + let app = match id { + None => None, Some(id) => { let known = KNOWN.iter().find(|k| k.id == id).ok_or_else(|| IdeError::invalid(format!("unknown browser: {id}")))?; - let app = locate(known).ok_or_else(|| IdeError::unavailable(format!("{} is not installed", known.name)))?; - open::with_detached(url, app.to_string_lossy().as_ref()) + Some(locate(known).ok_or_else(|| IdeError::unavailable(format!("{} is not installed", known.name)))?) } }; + #[cfg(target_os = "linux")] + let result = spawn_with_host_env(match &app { + None => open::commands(url), + Some(app) => vec![open::with_command(url, app.to_string_lossy())], + }); + #[cfg(not(target_os = "linux"))] + let result = match &app { + None => open::that_detached(url), + Some(app) => open::with_detached(url, app.to_string_lossy().as_ref()), + }; result.map_err(|e| IdeError::unavailable(format!("cannot open the browser: {e}"))) } +/// Starts the first launcher that runs (`xdg-open`, `gio`, ...), with the host's environment +/// rather than an AppImage's, and reaps it in the background. +#[cfg(target_os = "linux")] +fn spawn_with_host_env(commands: Vec) -> std::io::Result<()> { + use std::process::Stdio; + let mut last_err = None; + for mut cmd in commands { + ide_core::process::prepare(&mut cmd).stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null()); + match cmd.spawn() { + Ok(mut child) => { + std::thread::spawn(move || child.wait()); + return Ok(()); + } + Err(e) => last_err = Some(e), + } + } + Err(last_err.unwrap_or_else(|| std::io::Error::other("no launcher available"))) +} + #[cfg(test)] mod tests { use super::*;