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")
|
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.
|
/// Verify the app is running from its source checkout before shelling out.
|
||||||
fn require_source_checkout() -> Result<(), AppError> {
|
fn require_source_checkout() -> Result<(), AppError> {
|
||||||
let manifest = source_dir().join("Cargo.toml");
|
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.
|
/// Run a command with a timeout, capturing stdout/stderr separately.
|
||||||
async fn run(dir: &Path, program: &str, args: &[&str]) -> Result<std::process::Output, AppError> {
|
async fn run(dir: &Path, program: &str, args: &[&str]) -> Result<std::process::Output, AppError> {
|
||||||
let mut command = Command::new(program);
|
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();
|
let future = command.output();
|
||||||
match tokio::time::timeout(COMMAND_TIMEOUT, future).await {
|
match tokio::time::timeout(COMMAND_TIMEOUT, future).await {
|
||||||
Ok(Ok(output)) => Ok(output),
|
Ok(Ok(output)) => Ok(output),
|
||||||
Ok(Err(err)) => {
|
Ok(Err(err)) => {
|
||||||
let hint = if err.kind() == std::io::ErrorKind::NotFound {
|
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 {
|
} else {
|
||||||
format!("Could not run '{program}': {err}")
|
format!("Could not run '{program}': {err}")
|
||||||
};
|
};
|
||||||
|
|
@ -355,6 +409,29 @@ pub async fn apply() -> Result<UpdateApplyReport, AppError> {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn parses_npm_outdated_entries() {
|
fn parses_npm_outdated_entries() {
|
||||||
let json = r#"{
|
let json = r#"{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue