From 34edae313a7919ae483ba7fb3edcc6ad73c1fe89 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Sun, 9 Aug 2026 20:41:02 +0000 Subject: [PATCH 1/3] fix(bootstrap): validate uv installer postcondition --- CHANGELOG.md | 1 + frontend/src-tauri/src/tools.rs | 245 +++++++++++++++++++++++++------- 2 files changed, 195 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ace25833..17085e50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ The bundled TTS model package (`pyproject.toml`) is versioned independently. ### Fixed +- First-run source builds no longer stop after uv was successfully downloaded just because its installer failed during a later shell-profile step; app-private uv installs no longer touch shell profiles at all. (#1438) — thanks @AdrianoCahete! - Sidecar engines no longer break when a library they load prints to the console. Those bytes landed in the middle of the engine's data stream, failing the generation and leaving the connection scrambled for every request after it. (#1428) — thanks @1335-Group! - A generation abandoned while stuck on an internal lock now says so, instead of blaming your hardware and suggesting shorter text. Nothing had been computed, so none of that advice applied. (#1416, #1419) - A machine with a GPU that ends up on CPU now says why — a missing device node, a permissions problem, a card newer than the installed ROCm, an `HSA_OVERRIDE_GFX_VERSION` that is doing more harm than good, or an NVIDIA driver the container can't reach each read differently. Before, all of them looked identical to having no GPU at all. (#1274, #1228) diff --git a/frontend/src-tauri/src/tools.rs b/frontend/src-tauri/src/tools.rs index 974afaf6..c90fd7f8 100644 --- a/frontend/src-tauri/src/tools.rs +++ b/frontend/src-tauri/src/tools.rs @@ -3,7 +3,7 @@ use std::fs; use std::io; use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; +use std::process::{Command, Output, Stdio}; use std::sync::{Arc, Mutex}; use std::time::Duration; @@ -410,7 +410,7 @@ pub fn resolve_uv( log::info!("Using bundled uv at {}", p.display()); return Ok(p); } - if no_window(Command::new("uv").arg("--version")).output().is_ok() { + if uv_is_usable(Path::new("uv")) { log::info!("Using system uv from PATH"); return Ok(PathBuf::from("uv")); } @@ -427,11 +427,11 @@ pub fn resolve_uv( /// Windows: `powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/{version}/install.ps1 | iex"` /// /// The installer handles platform detection, checksums, and extraction -/// automatically. We control the install directory via `UV_INSTALL_DIR`. -/// Idempotent: if the binary is already present, returns its path immediately. +/// automatically. `UV_UNMANAGED_INSTALL` keeps this app-private tool out of +/// the user's PATH and shell profiles on every platform. fn install_uv_standalone(dest: &Path, _region: &str) -> io::Result { let uv_bin = dest.join(if cfg!(windows) { "uv.exe" } else { "uv" }); - if uv_bin.is_file() { + if uv_is_usable(&uv_bin) { return Ok(uv_bin); } fs::create_dir_all(dest)?; @@ -439,28 +439,24 @@ fn install_uv_standalone(dest: &Path, _region: &str) -> io::Result { #[cfg(unix)] { - let status = Command::new("sh") - .args([ + let output = configure_uv_installer( + Command::new("sh").args([ "-c", &format!( - "curl -LsSf https://astral.sh/uv/{}/install.sh | sh -s -- --no-modify-path", + "curl -LsSf https://astral.sh/uv/{}/install.sh | sh", UV_VERSION ), - ]) - .env("UV_INSTALL_DIR", dest) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .status() - .map_err(|e| io::Error::new( + ]), + dest, + ) + .output() + .map_err(|e| { + io::Error::new( io::ErrorKind::Other, format!("uv installer launch failed (is curl installed?): {}", e), - ))?; - if !status.success() { - return Err(io::Error::new( - io::ErrorKind::Other, - format!("uv installer exited with code {:?}", status.code()), - )); - } + ) + })?; + return finish_uv_install(dest, &uv_bin, output); } #[cfg(windows)] @@ -472,39 +468,186 @@ fn install_uv_standalone(dest: &Path, _region: &str) -> io::Result { // Windows: `CREATE_NO_WINDOW` so the uv installer's PowerShell doesn't // flash a console window during first-run bootstrap. stdout/stderr are // piped, so nothing is lost. - let status = no_window( - Command::new("powershell") - .args(["-ExecutionPolicy", "ByPass", "-c", &script]) - .env("UV_INSTALL_DIR", dest) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()), - ) - .status() - .map_err(|e| io::Error::new( - io::ErrorKind::Other, - format!("uv PowerShell installer failed: {}", e), - ))?; - if !status.success() { - return Err(io::Error::new( + let mut command = Command::new("powershell"); + command.args([ + "-NoProfile", + "-NonInteractive", + "-ExecutionPolicy", + "ByPass", + "-c", + &script, + ]); + configure_uv_installer(&mut command, dest); + let output = no_window(&mut command).output().map_err(|e| { + io::Error::new( io::ErrorKind::Other, - format!("uv installer exited with code {:?}", status.code()), - )); - } + format!("uv PowerShell installer failed: {}", e), + ) + })?; + return finish_uv_install(dest, &uv_bin, output); } - if uv_bin.is_file() { - log::info!("uv installed successfully at {}", uv_bin.display()); - Ok(uv_bin) - } else { - let alt = dest.join("bin").join(if cfg!(windows) { "uv.exe" } else { "uv" }); - if alt.is_file() { - fs::rename(&alt, &uv_bin)?; - log::info!("uv moved from bin/ to {}", uv_bin.display()); - return Ok(uv_bin); + #[allow(unreachable_code)] + Err(io::Error::new( + io::ErrorKind::Unsupported, + "unsupported uv install platform", + )) +} + +fn configure_uv_installer<'a>(command: &'a mut Command, dest: &Path) -> &'a mut Command { + // The official unmanaged mode is designed for app-private/CI installs: it + // selects the destination and disables PATH, profile, and self-update + // mutations. Explicitly remove the legacy variable so a parent shell + // cannot leave the installer in two conflicting modes. + command + .env_remove("UV_INSTALL_DIR") + .env("UV_UNMANAGED_INSTALL", dest) + .env("UV_NO_MODIFY_PATH", "1") + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) +} + +fn uv_is_usable(path: &Path) -> bool { + no_window( + Command::new(path) + .arg("--version") + .stdout(Stdio::null()) + .stderr(Stdio::null()), + ) + .status() + .map(|status| status.success()) + .unwrap_or(false) +} + +fn finish_uv_install(dest: &Path, uv_bin: &Path, output: Output) -> io::Result { + finish_uv_install_with_probe(dest, uv_bin, output, uv_is_usable) +} + +fn finish_uv_install_with_probe( + dest: &Path, + uv_bin: &Path, + output: Output, + is_usable: F, +) -> io::Result +where + F: Fn(&Path) -> bool, +{ + let alt = dest.join("bin").join(if cfg!(windows) { "uv.exe" } else { "uv" }); + if !is_usable(uv_bin) && is_usable(&alt) { + fs::rename(&alt, uv_bin).or_else(|_| fs::copy(&alt, uv_bin).map(|_| ()))?; + } + + // Some installer failures happen after extraction (for example while + // editing a Windows shell profile). The installed executable is the real + // postcondition: accepting a verified binary makes first run self-heal in + // this process instead of requiring a restart. Never accept a partial or + // corrupt file merely because it exists. + if is_usable(uv_bin) { + if output.status.success() { + log::info!("uv installed successfully at {}", uv_bin.display()); + } else { + log::warn!( + "uv installer exited with {:?}, but the installed binary passed validation at {}", + output.status.code(), + uv_bin.display() + ); } - Err(io::Error::new( - io::ErrorKind::NotFound, - format!("uv binary not found at {} after installer completed", uv_bin.display()), - )) + return Ok(uv_bin.to_path_buf()); + } + + let detail = installer_output_detail(&output); + Err(io::Error::new( + io::ErrorKind::Other, + if output.status.success() { + format!( + "uv installer completed but no usable binary was found at {}{}", + uv_bin.display(), + detail + ) + } else { + format!("uv installer exited with code {:?}{}", output.status.code(), detail) + }, + )) +} + +fn installer_output_detail(output: &Output) -> String { + let bytes = if output.stderr.is_empty() { + &output.stdout + } else { + &output.stderr + }; + let text = String::from_utf8_lossy(bytes); + let text = text.trim(); + if text.is_empty() { + return String::new(); + } + let start = text + .char_indices() + .rev() + .nth(1999) + .map(|(index, _)| index) + .unwrap_or(0); + format!(": {}", &text[start..]) +} + +#[cfg(test)] +mod uv_tests { + use super::*; + use std::ffi::OsStr; + + #[test] + fn installer_uses_app_private_unmanaged_mode() { + let mut command = Command::new("installer"); + configure_uv_installer(&mut command, Path::new("private-tools")); + let envs: std::collections::HashMap<_, _> = command.get_envs().collect(); + + assert_eq!(envs.get(OsStr::new("UV_INSTALL_DIR")), Some(&None)); + assert_eq!( + envs.get(OsStr::new("UV_UNMANAGED_INSTALL")).and_then(|value| *value), + Some(OsStr::new("private-tools")) + ); + assert_eq!( + envs.get(OsStr::new("UV_NO_MODIFY_PATH")).and_then(|value| *value), + Some(OsStr::new("1")) + ); + } + + #[test] + fn installer_error_includes_captured_stderr() { + let output = Output { + status: failure_status(), + stdout: Vec::new(), + stderr: b"profile update denied".to_vec(), + }; + assert_eq!(installer_output_detail(&output), ": profile update denied"); + } + + #[test] + fn installer_exit_one_is_accepted_when_downloaded_uv_is_usable() { + let dest = Path::new("private-tools"); + let uv_bin = dest.join(if cfg!(windows) { "uv.exe" } else { "uv" }); + let output = Output { + status: failure_status(), + stdout: Vec::new(), + stderr: b"later installer step failed".to_vec(), + }; + + let result = finish_uv_install_with_probe(dest, &uv_bin, output, |candidate| { + candidate == uv_bin + }); + + assert_eq!(result.unwrap(), uv_bin); + } + + #[cfg(unix)] + fn failure_status() -> std::process::ExitStatus { + use std::os::unix::process::ExitStatusExt; + std::process::ExitStatus::from_raw(1 << 8) + } + + #[cfg(windows)] + fn failure_status() -> std::process::ExitStatus { + use std::os::windows::process::ExitStatusExt; + std::process::ExitStatus::from_raw(1) } } From fcdbac9683b8d7ab3d46fcea2020576b86c1437b Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Mon, 10 Aug 2026 02:49:25 +0000 Subject: [PATCH 2/3] fix(bootstrap): require the pinned uv version --- frontend/src-tauri/src/tools.rs | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/frontend/src-tauri/src/tools.rs b/frontend/src-tauri/src/tools.rs index 8b83398e..ddc7231e 100644 --- a/frontend/src-tauri/src/tools.rs +++ b/frontend/src-tauri/src/tools.rs @@ -511,14 +511,22 @@ fn uv_is_usable(path: &Path) -> bool { no_window( Command::new(path) .arg("--version") - .stdout(Stdio::null()) + .stdout(Stdio::piped()) .stderr(Stdio::null()), ) - .status() - .map(|status| status.success()) + .output() + .map(|output| output.status.success() && uv_version_matches(&output.stdout)) .unwrap_or(false) } +fn uv_version_matches(output: &[u8]) -> bool { + let Ok(text) = std::str::from_utf8(output) else { + return false; + }; + let mut fields = text.split_whitespace(); + fields.next() == Some("uv") && fields.next() == Some(UV_VERSION) +} + fn finish_uv_install(dest: &Path, uv_bin: &Path, output: Output) -> io::Result { finish_uv_install_with_probe(dest, uv_bin, output, uv_is_usable) } @@ -612,6 +620,17 @@ mod uv_tests { ); } + #[test] + fn uv_version_probe_requires_the_pinned_version() { + assert!(uv_version_matches( + format!("uv {} (build-id)\n", UV_VERSION).as_bytes() + )); + assert!(!uv_version_matches(b"uv 0.10.0 (older)\n")); + assert!(!uv_version_matches(b"not-uv 0.11.7\n")); + assert!(!uv_version_matches(b"uv\n")); + assert!(!uv_version_matches(&[0xff, 0xfe])); + } + #[test] fn installer_error_includes_captured_stderr() { let output = Output { From e7f6f3beb5021c5f8d6f80e780b6e199f4a50cd5 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Mon, 10 Aug 2026 02:52:44 +0000 Subject: [PATCH 3/3] fix(bootstrap): redact installer home paths --- frontend/src-tauri/src/tools.rs | 48 ++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/frontend/src-tauri/src/tools.rs b/frontend/src-tauri/src/tools.rs index ddc7231e..4ebab9ef 100644 --- a/frontend/src-tauri/src/tools.rs +++ b/frontend/src-tauri/src/tools.rs @@ -585,10 +585,15 @@ fn installer_output_detail(output: &Output) -> String { &output.stderr }; let text = String::from_utf8_lossy(bytes); - let text = text.trim(); + let mut text = text.trim().to_string(); if text.is_empty() { return String::new(); } + for key in ["USERPROFILE", "HOME"] { + if let Some(home) = std::env::var_os(key).and_then(|value| value.into_string().ok()) { + text = redact_home_prefix(&text, &home); + } + } let start = text .char_indices() .rev() @@ -598,6 +603,22 @@ fn installer_output_detail(output: &Output) -> String { format!(": {}", &text[start..]) } +fn redact_home_prefix(text: &str, home: &str) -> String { + if home.len() < 3 { + return text.to_string(); + } + let mut redacted = text.replace(home, "~"); + let forward = home.replace('\\', "/"); + let backward = home.replace('/', "\\"); + if forward != home { + redacted = redacted.replace(&forward, "~"); + } + if backward != home { + redacted = redacted.replace(&backward, "~"); + } + redacted +} + #[cfg(test)] mod uv_tests { use super::*; @@ -641,6 +662,31 @@ mod uv_tests { assert_eq!(installer_output_detail(&output), ": profile update denied"); } + #[test] + fn installer_error_redacts_unix_and_windows_home_paths() { + assert_eq!( + redact_home_prefix( + "installed into /Users/alice/.local/bin", + "/Users/alice" + ), + "installed into ~/.local/bin" + ); + assert_eq!( + redact_home_prefix( + r"installed into C:\Users\alice\.local\bin", + r"C:\Users\alice" + ), + r"installed into ~\.local\bin" + ); + assert_eq!( + redact_home_prefix( + "installed into C:/Users/alice/.local/bin", + r"C:\Users\alice" + ), + "installed into ~/.local/bin" + ); + } + #[test] fn installer_exit_one_is_accepted_when_downloaded_uv_is_usable() { let dest = Path::new("private-tools");