fix(bootstrap): scrub PYTHONHOME/PYTHONPATH before uv so AppImage venv build succeeds (#144, #127) (#158)

On the Linux AppImage, the bundled runtime exports PYTHONHOME / PYTHONPATH
(and sometimes LD_LIBRARY_PATH) pointing at the AppImage's *own* bundled
Python. When first-run bootstrap shells out to `uv` to create/sync the venv,
that build subprocess inherits those vars, so the freshly-built managed
interpreter resolves its stdlib against the wrong (AppImage) Python and dies
with `ModuleNotFoundError: No module named 'encodings'` while compiling a
transitive dep (dora-search/demucs). This surfaces downstream as
"Backend process exited (never started) — no error output captured" (#144).

The backend spawn in backend.rs already scrubs these vars before launching
uvicorn; the uv/venv/pip subprocesses in bootstrap.rs were scrubbing them too
but via five inline copies, which is drift-prone. Factor the scrub into a
single `scrub_python_env(cmd)` helper documenting the #144 root cause, and
apply it at every uv/venv/pip call site (uvicorn import check, repair sync,
venv create, sync, ROCm reinstall). Add a unit test asserting the helper
queues removals for all three vars.

Safe cross-platform: those vars are normally unset on macOS/Windows and
`env_remove` on an unset var is a no-op, so default behavior is unchanged
everywhere. Compile-validated (cargo check + cargo test pass); no AppImage in
CI to reproduce the original failure end-to-end.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Palash Debnath
2026-05-30 10:36:53 +05:30
committed by GitHub
co-authored by Claude Opus 4.8
parent 8fa54ccae4
commit b4f238aeb1
+42 -5
View File
@@ -276,6 +276,26 @@ Fix: install Python 3.11+ from https://www.python.org/downloads/ (tick \"Add to
then relaunch — OmniVoice will use your system Python. Advanced: set \
UV_PYTHON_INSTALL_MIRROR to a reachable mirror (see docs/install/troubleshooting.md).";
/// Strip the bundled-runtime Python env vars before spawning any `uv`/venv/pip
/// or venv-python subprocess (#144). On the Linux AppImage, the bundled runtime
/// exports PYTHONHOME / PYTHONPATH (and sometimes LD_LIBRARY_PATH) pointing at
/// the AppImage's *own* bundled Python. Those leak into the `uv` build
/// subprocess, so the freshly-built managed interpreter resolves its stdlib
/// against the wrong (AppImage) Python and dies with
/// `ModuleNotFoundError: No module named 'encodings'` while compiling a
/// transitive dep (e.g. dora-search/demucs) — surfacing downstream as
/// "Backend process exited (never started)". This mirrors the same scrub the
/// backend spawn already does in `backend.rs` before launching uvicorn.
///
/// Safe on every platform: these vars are normally unset on macOS/Windows, and
/// `env_remove` on an unset var is a no-op — so there's no cross-platform
/// divergence in default behavior.
fn scrub_python_env(cmd: &mut Command) {
cmd.env_remove("PYTHONHOME")
.env_remove("PYTHONPATH")
.env_remove("LD_LIBRARY_PATH");
}
/// Longer timeouts + more retries so a slow/flaky mirror or PyPI doesn't kill
/// the first-run install on its first hiccup (#130 step 2).
fn apply_uv_http_env(cmd: &mut Command) {
@@ -343,7 +363,7 @@ pub fn ensure_venv_ready<R: tauri::Runtime>(app: &tauri::AppHandle<R>, progress:
if venv_py.is_file() && backend_dir.is_dir() {
let mut uvicorn_check_cmd = Command::new(&venv_py);
uvicorn_check_cmd.env_remove("PYTHONHOME").env_remove("PYTHONPATH").env_remove("LD_LIBRARY_PATH");
scrub_python_env(&mut uvicorn_check_cmd); // #144: don't inherit AppImage's bundled Python
let uvicorn_check = uvicorn_check_cmd
.args(["-c", "import uvicorn"])
.stdout(Stdio::null())
@@ -393,7 +413,7 @@ pub fn ensure_venv_ready<R: tauri::Runtime>(app: &tauri::AppHandle<R>, progress:
Err(e) => { fail(progress, &e); return None; }
};
let mut repair_cmd = Command::new(&uv_path);
repair_cmd.env_remove("PYTHONHOME").env_remove("PYTHONPATH").env_remove("LD_LIBRARY_PATH");
scrub_python_env(&mut repair_cmd); // #144: don't inherit AppImage's bundled Python
let has_lockfile = project_dir.join("uv.lock").is_file();
if has_lockfile {
repair_cmd.args(["sync", "--frozen", "--no-dev", "--verbose"]);
@@ -503,7 +523,7 @@ pub fn ensure_venv_ready<R: tauri::Runtime>(app: &tauri::AppHandle<R>, progress:
let mut venv_ok = false;
for (label, args, envs) in &venv_attempts {
let mut venv_cmd = Command::new(&uv_path);
venv_cmd.env_remove("PYTHONHOME").env_remove("PYTHONPATH").env_remove("LD_LIBRARY_PATH");
scrub_python_env(&mut venv_cmd); // #144: don't inherit AppImage's bundled Python
apply_uv_http_env(&mut venv_cmd);
for &(k, v) in envs {
venv_cmd.env(k, v);
@@ -525,7 +545,7 @@ pub fn ensure_venv_ready<R: tauri::Runtime>(app: &tauri::AppHandle<R>, progress:
set_stage(p, BootstrapStage::InstallingDeps);
}
let mut sync_cmd = Command::new(&uv_path);
sync_cmd.env_remove("PYTHONHOME").env_remove("PYTHONPATH").env_remove("LD_LIBRARY_PATH");
scrub_python_env(&mut sync_cmd); // #144: don't inherit AppImage's bundled Python
apply_uv_http_env(&mut sync_cmd);
let has_lockfile = project_dir.join("uv.lock").is_file();
if has_lockfile {
@@ -562,7 +582,7 @@ docs/install/troubleshooting.md).",
if let Some(rocm_url) = rocm_opt_in() {
log::info!("OMNIVOICE_TORCH_VARIANT=rocm → reinstalling torch from {}", rocm_url);
let mut rocm_cmd = Command::new(&uv_path);
rocm_cmd.env_remove("PYTHONHOME").env_remove("PYTHONPATH").env_remove("LD_LIBRARY_PATH");
scrub_python_env(&mut rocm_cmd); // #144: don't inherit AppImage's bundled Python
apply_uv_http_env(&mut rocm_cmd);
rocm_cmd.args(rocm_torch_reinstall_args(&rocm_url)).current_dir(&project_dir);
let rocm_status = run_streaming(app, "installing_deps", &mut rocm_cmd);
@@ -584,6 +604,23 @@ mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn scrub_python_env_removes_bundled_runtime_vars() {
// #144: every uv/venv/pip subprocess must drop the AppImage's bundled
// Python env vars so the managed interpreter resolves its own stdlib.
// `env_remove` queues a removal that `get_envs()` reports as (key, None).
let mut cmd = Command::new("uv");
scrub_python_env(&mut cmd);
let removed: std::collections::HashSet<String> = cmd
.get_envs()
.filter(|(_, v)| v.is_none())
.map(|(k, _)| k.to_string_lossy().into_owned())
.collect();
assert!(removed.contains("PYTHONHOME"), "PYTHONHOME must be scrubbed");
assert!(removed.contains("PYTHONPATH"), "PYTHONPATH must be scrubbed");
assert!(removed.contains("LD_LIBRARY_PATH"), "LD_LIBRARY_PATH must be scrubbed");
}
#[test]
fn apply_uv_http_env_sets_timeouts_and_retries() {
let mut cmd = Command::new("uv");