fix(updates): augment spawned PATH so npm/cargo resolve from Electron
The backend spawned by Electron inherits the desktop launcher's environment, not a login shell: ~/.cargo/bin and the mise/asdf shim dirs are missing, so 'Check for updates' failed with "'npm' was not found" even though both tools exist in a terminal. run() now sets PATH to the inherited value plus the well-known per-user tool dirs (~/.cargo/bin, ~/.local/bin, mise + asdf shims, /usr/local/bin), appended after the inherited entries so existing resolutions keep priority. The NotFound hint now says what to do. Verified live: update_check under env -i PATH=/usr/bin:/bin returns a full report (npm advisories + cargo updates) where it previously errored. Unit tests cover the pure PATH builder incl. missing-env fallback.
This commit is contained in:
parent
adbc7c2d75
commit
fa59b63a17
1 changed files with 79 additions and 2 deletions
|
|
@ -72,6 +72,51 @@ fn frontend_dir() -> PathBuf {
|
|||
source_dir().join("frontend")
|
||||
}
|
||||
|
||||
/// PATH used for package-manager commands.
|
||||
///
|
||||
/// When the backend is spawned by Electron its environment is the desktop
|
||||
/// launcher's, NOT a login shell: `~/.cargo/bin` (cargo) and the node
|
||||
/// version-manager bins/shims (npm) are missing, so the update commands
|
||||
/// failed with "'npm' was not found" even though both exist in a terminal.
|
||||
/// Augment the inherited PATH with the well-known per-user tool locations,
|
||||
/// appended after the inherited entries: where a tool is already resolvable
|
||||
/// on the inherited PATH that version wins, and entries that do not exist
|
||||
/// are simply ignored by exec.
|
||||
fn augmented_path() -> std::ffi::OsString {
|
||||
let inherited = std::env::var_os("PATH");
|
||||
let home = std::env::var_os("HOME");
|
||||
build_augmented_path(inherited.as_deref(), home.as_deref())
|
||||
}
|
||||
|
||||
/// Pure form of [`augmented_path`], testable without mutating the process
|
||||
/// environment.
|
||||
fn build_augmented_path(
|
||||
inherited: Option<&std::ffi::OsStr>,
|
||||
home: Option<&std::ffi::OsStr>,
|
||||
) -> std::ffi::OsString {
|
||||
let inherited_parts: Vec<PathBuf> = inherited
|
||||
.map(std::env::split_paths)
|
||||
.map(|p| p.collect())
|
||||
.unwrap_or_default();
|
||||
let home = home.map(PathBuf::from);
|
||||
let extra: Vec<PathBuf> = [
|
||||
".cargo/bin",
|
||||
".local/bin",
|
||||
".local/share/mise/shims",
|
||||
".asdf/shims",
|
||||
"/usr/local/bin",
|
||||
]
|
||||
.into_iter()
|
||||
.filter_map(|rel| match rel.strip_prefix('/') {
|
||||
// A leading slash marks an absolute system entry: use it verbatim.
|
||||
Some(_) => Some(PathBuf::from(rel)),
|
||||
None => home.as_ref().map(|h| h.join(rel)),
|
||||
})
|
||||
.collect();
|
||||
std::env::join_paths(inherited_parts.into_iter().chain(extra))
|
||||
.unwrap_or_else(|_| std::ffi::OsString::from("/usr/local/bin:/usr/bin:/bin"))
|
||||
}
|
||||
|
||||
/// Verify the app is running from its source checkout before shelling out.
|
||||
fn require_source_checkout() -> Result<(), AppError> {
|
||||
let manifest = source_dir().join("Cargo.toml");
|
||||
|
|
@ -88,13 +133,22 @@ fn require_source_checkout() -> Result<(), AppError> {
|
|||
/// Run a command with a timeout, capturing stdout/stderr separately.
|
||||
async fn run(dir: &Path, program: &str, args: &[&str]) -> Result<std::process::Output, AppError> {
|
||||
let mut command = Command::new(program);
|
||||
command.current_dir(dir).args(args).kill_on_drop(true);
|
||||
command
|
||||
.current_dir(dir)
|
||||
.args(args)
|
||||
// See augmented_path(): Electron-spawned backends inherit a desktop
|
||||
// PATH without the per-user tool dirs, and npm/cargo "don't exist".
|
||||
.env("PATH", augmented_path())
|
||||
.kill_on_drop(true);
|
||||
let future = command.output();
|
||||
match tokio::time::timeout(COMMAND_TIMEOUT, future).await {
|
||||
Ok(Ok(output)) => Ok(output),
|
||||
Ok(Err(err)) => {
|
||||
let hint = if err.kind() == std::io::ErrorKind::NotFound {
|
||||
format!("'{program}' was not found on this computer.")
|
||||
format!(
|
||||
"'{program}' was not found on this computer. \
|
||||
Install it (or add it to the app's PATH) and try again."
|
||||
)
|
||||
} else {
|
||||
format!("Could not run '{program}': {err}")
|
||||
};
|
||||
|
|
@ -355,6 +409,29 @@ pub async fn apply() -> Result<UpdateApplyReport, AppError> {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn augmented_path_appends_tool_dirs_after_inherited() {
|
||||
let joined = build_augmented_path(
|
||||
Some(std::ffi::OsStr::new("/usr/bin:/bin")),
|
||||
Some(std::ffi::OsStr::new("/home/tester")),
|
||||
);
|
||||
let joined = joined.to_string_lossy().into_owned();
|
||||
// Inherited entries keep priority.
|
||||
assert!(joined.starts_with("/usr/bin:/bin:"));
|
||||
// The dirs an Electron-spawned process is missing are now present.
|
||||
assert!(joined.contains("/home/tester/.cargo/bin"));
|
||||
assert!(joined.contains("/home/tester/.local/share/mise/shims"));
|
||||
assert!(joined.contains("/usr/local/bin"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn augmented_path_survives_missing_env() {
|
||||
// No PATH and no HOME: still a usable absolute system path.
|
||||
let joined = build_augmented_path(None, None);
|
||||
let joined = joined.to_string_lossy().into_owned();
|
||||
assert!(joined.contains("/usr/local/bin"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_npm_outdated_entries() {
|
||||
let json = r#"{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue