From 4b88b1379a2af0403ea3d2ee9f49a81707f48098 Mon Sep 17 00:00:00 2001 From: Ed Rogers Date: Mon, 18 Aug 2025 13:48:34 -0500 Subject: [PATCH] Add fallback parent process detection to `uv tool update-shell` (#15356) ## Summary Closes #15355 This PR adds a fallback mechanism to `Shell::from_env()` that inspects the parent process when shell environment variables are not available on Unix-like systems. Currently, `uv tool update-shell` fails with "the current shell could not be determined" when environment variables like `ZSH_VERSION`, `BASH_VERSION`, or `SHELL` are not exported. This commonly occurs in automated environments such as GitHub Actions runners. The fallback approach: 1. Uses `nix::unistd::getppid()` to get the parent process ID 2. Reads `/proc//exe` to determine the parent executable path 3. Falls back to `/proc//comm` if the exe symlink fails 4. Uses existing `parse_shell_from_path()` to identify the shell type This maintains full backward compatibility - the fallback only activates when environment variables are unavailable and an error would otherwise occur. ## Test Plan Tested locally with: ```bash env -u ZSH_VERSION -u SHELL PATH="/usr/bin:/bin" $(which cargo) run -- tool update-shell --verbose ``` ```text Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s Running `target/debug/uv tool update-shell --verbose` DEBUG uv 0.8.11 DEBUG Ensuring that the executable directory is in PATH: /home/user/.local/bin DEBUG Detected parent process ID: 4147396 DEBUG Parent process executable: /usr/bin/zsh Updated configuration file: /home/user/.zshenv Restart your shell to apply changes ``` --- Cargo.lock | 1 + crates/uv-shell/Cargo.toml | 4 ++++ crates/uv-shell/src/lib.rs | 48 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 001055e45..29afe8699 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6272,6 +6272,7 @@ dependencies = [ "anyhow", "fs-err", "home", + "nix 0.30.1", "same-file", "tempfile", "tracing", diff --git a/crates/uv-shell/Cargo.toml b/crates/uv-shell/Cargo.toml index e0c75f8e4..f70fffa00 100644 --- a/crates/uv-shell/Cargo.toml +++ b/crates/uv-shell/Cargo.toml @@ -15,10 +15,14 @@ uv-fs = { workspace = true } uv-static = { workspace = true } anyhow = { workspace = true } +fs-err = { workspace = true } home = { workspace = true } same-file = { workspace = true } tracing = { workspace = true } +[target.'cfg(unix)'.dependencies] +nix = { workspace = true } + [target.'cfg(windows)'.dependencies] windows-registry = { workspace = true } windows-result = { workspace = true } diff --git a/crates/uv-shell/src/lib.rs b/crates/uv-shell/src/lib.rs index 5593a4947..a18e04b03 100644 --- a/crates/uv-shell/src/lib.rs +++ b/crates/uv-shell/src/lib.rs @@ -8,6 +8,9 @@ use std::path::{Path, PathBuf}; use uv_fs::Simplified; use uv_static::EnvVars; +#[cfg(unix)] +use tracing::debug; + /// Shells for which virtualenv activation scripts are available. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[allow(clippy::doc_markdown)] @@ -64,6 +67,51 @@ impl Shell { Some(Self::Powershell) } } else { + // Fallback to detecting the shell from the parent process + Self::from_parent_process() + } + } + + /// Attempt to determine the shell from the parent process. + /// + /// This is a fallback method for when environment variables don't provide + /// enough information about the current shell. It looks at the parent process + /// to try to identify which shell is running. + /// + /// This method currently only works on Unix-like systems. On other platforms, + /// it returns `None`. + fn from_parent_process() -> Option { + #[cfg(unix)] + { + // Get the parent process ID + let ppid = nix::unistd::getppid(); + debug!("Detected parent process ID: {ppid}"); + + // Try to read the parent process executable path + let proc_exe_path = format!("/proc/{ppid}/exe"); + if let Ok(exe_path) = fs_err::read_link(&proc_exe_path) { + debug!("Parent process executable: {}", exe_path.display()); + if let Some(shell) = Self::from_shell_path(&exe_path) { + return Some(shell); + } + } + + // If reading exe fails, try reading the comm file + let proc_comm_path = format!("/proc/{ppid}/comm"); + if let Ok(comm) = fs_err::read_to_string(&proc_comm_path) { + let comm = comm.trim(); + debug!("Parent process comm: {comm}"); + if let Some(shell) = parse_shell_from_path(Path::new(comm)) { + return Some(shell); + } + } + + debug!("Could not determine shell from parent process"); + None + } + + #[cfg(not(unix))] + { None } }