fix(setup): repair fresh-clone desktop and ROCm source installs (#1664, #1665)

Fix fresh-clone desktop development by creating the required dist placeholder before Tauri starts, and make source setup install the selected CUDA or ROCm PyTorch stack consistently. Adds behavior-level cross-platform regression coverage.\n\nFixes #1664.\nFixes #1665.\n\nThanks @uberclokr for the contribution.
This commit is contained in:
Parker Hill
2026-08-27 18:33:55 +05:30
committed by GitHub
parent 032c5aba86
commit 078bad8e0f
8 changed files with 231 additions and 9 deletions
+2
View File
@@ -44,6 +44,8 @@ the frozen-backend fallback mirror it for their toolchains.
- The OmniVoice guide now covers combining style attributes with a reference clip (consistent instruct stabilizes cloning; the reference wins conflicts), inline pronunciation control (pinyin / CMU phonemes), and corrects the claim that the default engine can't do voice design — it can, from attributes (#1565)
### Fixed
- Source installs on AMD GPUs honour `OMNIVOICE_TORCH_VARIANT=rocm`: `bun run desktop` now swaps in the ROCm torch wheel after `uv sync` and launches the backend without re-syncing, instead of silently reverting to the CPU-only CUDA build on every start (#1665) — thanks @uberclokr!
- `bun run desktop` on a fresh clone no longer fails with "resource path `../../frontend/dist` doesn't exist" — the dev launcher creates the placeholder Tauri resource directory before compiling (#1664) — thanks @uberclokr!
- macOS no longer loses TTS after the first request when Python lacks `os.waitid`; subprocess ownership now uses a safe `waitpid` fallback without risking reused process groups (#1656) — thanks @paoloantinori!
- Desktop startup, Retry, reset, uninstall, shutdown, and crash recovery now share one backend lifecycle owner; quitting interrupts first-run installers and gracefully drains then force-cleans the full backend process tree, so overlaps cannot duplicate or orphan it (#1635) — thanks @Xohaibxobi!
- Large Stories and Audiobook projects now persist in IndexedDB instead of overflowing the `omnivoice.app` localStorage envelope, with quota-safe migration and orderly exit/reload flushing (#1636) — thanks @leodzai!
+7 -2
View File
@@ -378,9 +378,14 @@ dependency sync — matched to the app's pinned `torch==2.8.0` (the rocm6.2
index only ever published up to torch 2.5.1, so it silently failed the
reinstall and left the CPU-only CUDA build in place).
**2. Environment variable (existing installs / headless).** Set
**2. Environment variable (existing installs / headless / source).** Set
`OMNIVOICE_TORCH_VARIANT=rocm` before launching — the next bootstrap performs
the same ROCm reinstall. `OMNIVOICE_TORCH_INDEX=<url>` overrides the wheel
the same ROCm reinstall. Source installs honour it too:
`OMNIVOICE_TORCH_VARIANT=rocm bun run desktop` swaps torch right after
`uv sync` and launches the backend without re-syncing, so the wheel is not
reverted on the next start (#1665). Without the variable, `bun run desktop`
restores the lockfile's CUDA build — a hand-swapped ROCm wheel does not
survive it. `OMNIVOICE_TORCH_INDEX=<url>` overrides the wheel
index when you need a different ROCm version — e.g. AMD publishes newer
driver-matched builds (7.2.x) at `repo.radeon.com` as a `--find-links` page
rather than a PyPI-style index:
+19
View File
@@ -0,0 +1,19 @@
import { spawnSync } from "node:child_process";
import { mkdirSync } from "node:fs";
import { join } from "node:path";
import process from "node:process";
/** Create Tauri's required dev resource before starting the compiler. */
export function launchTauriDev({
cwd = process.cwd(),
args = process.argv.slice(2),
env = process.env,
mkdir = mkdirSync,
spawn = spawnSync,
} = {}) {
mkdir(join(cwd, "dist"), { recursive: true });
return spawn("bun", ["run", "tauri", "dev", ...args], {
stdio: "inherit",
env,
});
}
+5 -6
View File
@@ -23,6 +23,7 @@ import { join, delimiter } from "node:path";
import { homedir } from "node:os";
import process from "node:process";
import { DEV_APP_PROCESS_NAME } from "./desktop-common.mjs";
import { launchTauriDev } from "./desktop-dev-launch.mjs";
/** The env's PATH key — Windows uses "Path", others "PATH"; match case-insensitively. */
function pathKeyOf(env) {
@@ -114,12 +115,10 @@ if (!cargoResolvable(childEnv)) {
}
}
// Run the workspace-local Tauri CLI in dev mode (cwd is already frontend/),
// handing it the healed env so its `cargo` spawns inherit the fixed PATH.
const res = spawnSync("bun", ["run", "tauri", "dev", ...process.argv.slice(2)], {
stdio: "inherit",
env: childEnv,
});
// `frontend/dist` is a required bundle resource even under `tauri dev`.
// The helper creates it before spawning Tauri and is behavior-tested with an
// injected process runner (#1664). cwd is already frontend/.
const res = launchTauriDev({ env: childEnv });
if (res.error) {
console.error(`❌ failed to launch tauri dev: ${res.error.message}`);
process.exit(1);
+10 -1
View File
@@ -98,8 +98,17 @@ export function buildExitBanner({ code, signal, logTail, logPath, platform = pro
return lines.join("\n");
}
// `uv run` re-syncs the venv to uv.lock before launching — which would undo
// the opt-in ROCm torch swap `scripts/setup.py` just performed (the lock pins
// the CUDA build). `bun run setup:api` already did the sync, so skip it here
// whenever the ROCm variant is requested (#1665).
export function uvRunArgs(env = process.env) {
const rocm = (env.OMNIVOICE_TORCH_VARIANT || "").trim().toLowerCase() === "rocm";
return rocm ? [UVICORN_ARGS[0], "--no-sync", ...UVICORN_ARGS.slice(1)] : UVICORN_ARGS;
}
function main() {
const child = spawn("uv", UVICORN_ARGS, { stdio: "inherit" });
const child = spawn("uv", uvRunArgs(), { stdio: "inherit" });
// A Ctrl+C / concurrently teardown is a DELIBERATE stop — no scary banner.
let interrupted = false;
+59
View File
@@ -10,6 +10,12 @@
2. **CUDA: cuDNN 8 compat** Ensures cuDNN 8 libraries are available for
CTranslate2 (faster-whisper / WhisperX) alongside PyTorch 2.8+'s cuDNN 9.
3. **AMD ROCm torch (opt-in)** with `OMNIVOICE_TORCH_VARIANT=rocm` set,
replace the lockfile's CUDA torch build (CPU-only on AMD cards) with the
ROCm wheel the same swap the packaged app's bootstrap performs, so a
source install (`bun run desktop`) on an AMD GPU is not stuck on CPU. Runs
AFTER `uv sync`, which always restores the locked CUDA build (#1665).
Run automatically as part of `bun run setup:api` no user action required.
Cross-platform:
@@ -38,6 +44,55 @@ for _stream in (sys.stdout, sys.stderr):
pass
# ── AMD ROCm torch (opt-in) ────────────────────────────────────────────────
# Keep in sync with ROCM_TORCH_INDEX / rocm_torch_reinstall_args in
# frontend/src-tauri/src/bootstrap.rs and [tool.uv.constraint-dependencies].
ROCM_TORCH_INDEX = "https://download.pytorch.org/whl/rocm6.4"
ROCM_TORCH_PINS = ("torch==2.8.0", "torchaudio==2.8.0", "torchvision==0.23.0")
def _rocm_opt_in(environ=os.environ):
"""Return the ROCm wheel index when the user opted in, else None."""
if environ.get("OMNIVOICE_TORCH_VARIANT", "").strip().lower() != "rocm":
return None
return environ.get("OMNIVOICE_TORCH_INDEX") or ROCM_TORCH_INDEX
def _installed_torch_is_rocm():
try:
import torch # noqa: WPS433 — deliberately lazy; torch is heavy
except Exception:
return False
return bool(getattr(torch.version, "hip", None))
def rocm_torch_reinstall_cmd(index_url, python=None):
"""`uv pip install` argv targeting THIS venv (not whatever uv guesses)."""
return [
"uv", "pip", "install", "--reinstall",
"--python", python or sys.executable,
*ROCM_TORCH_PINS,
"--index-url", index_url,
]
def _ensure_rocm_torch():
index_url = _rocm_opt_in()
if index_url is None:
return
if _installed_torch_is_rocm():
print("✓ ROCm torch already installed")
return
print(f"⚙ OMNIVOICE_TORCH_VARIANT=rocm — swapping torch to the ROCm wheel ({index_url})")
try:
subprocess.check_call(rocm_torch_reinstall_cmd(index_url))
except (OSError, subprocess.CalledProcessError) as exc:
print(f"⚠ ROCm torch install failed ({exc}); keeping the default torch build")
return
print("✓ ROCm torch installed")
# ── Windows: VC++ Redistributable ─────────────────────────────────────────
def _ensure_vcredist_windows():
@@ -153,6 +208,10 @@ def main():
if sys.platform == "darwin":
return
# ── Step 2: opt-in AMD ROCm torch (Linux) ─────────────────────────────
if sys.platform.startswith("linux"):
_ensure_rocm_torch()
compat_dir = _find_compat_dir()
if compat_dir is None:
return
@@ -0,0 +1,46 @@
"""`bun run desktop` must create its Tauri resource before spawning (#1664)."""
import json
import subprocess
from pathlib import Path
_ROOT = Path(__file__).resolve().parents[1]
_LAUNCH = (_ROOT / "scripts" / "desktop-dev-launch.mjs").as_uri()
def test_desktop_dev_creates_dist_before_tauri_dev(tmp_path):
cwd = tmp_path / "workspace" / "frontend"
script = f"""
import {{ launchTauriDev }} from {json.dumps(_LAUNCH)};
const calls = [];
const result = launchTauriDev({{
cwd: {json.dumps(str(cwd))},
args: ["--features", "test-feature"],
env: {{ PATH: "test-path" }},
mkdir: (path, options) => calls.push(["mkdir", path, options]),
spawn: (command, args, options) => {{
calls.push(["spawn", command, args, options]);
return {{ status: 0 }};
}},
}});
console.log(JSON.stringify({{ calls, result }}));
"""
completed = subprocess.run(
["node", "--input-type=module", "--eval", script],
check=True,
capture_output=True,
text=True,
)
observed = json.loads(completed.stdout)
assert observed == {
"calls": [
["mkdir", str(cwd / "dist"), {"recursive": True}],
[
"spawn",
"bun",
["run", "tauri", "dev", "--features", "test-feature"],
{"stdio": "inherit", "env": {"PATH": "test-path"}},
],
],
"result": {"status": 0},
}
+83
View File
@@ -0,0 +1,83 @@
"""Source installs must honour `OMNIVOICE_TORCH_VARIANT=rocm` (#1665).
`uv sync` always restores the lockfile's CUDA torch build, which is CPU-only
on AMD cards, and `uv run` re-syncs before every launch so a hand-swapped
ROCm wheel was silently reverted by the next `bun run desktop`. The packaged
app's bootstrap (`bootstrap.rs`) already performs the swap on opt-in; these
tests pin the dev-flow equivalents: `scripts/setup.py` reinstalls the ROCm
wheel after the sync, and `scripts/dev-backend.mjs` launches with
`uv run --no-sync` so it sticks.
"""
import importlib.util
import json
import os
from pathlib import Path
import subprocess
_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
_SETUP = os.path.join(_ROOT, "scripts", "setup.py")
_DEV_BACKEND = os.path.join(_ROOT, "scripts", "dev-backend.mjs")
_BOOTSTRAP = os.path.join(_ROOT, "frontend", "src-tauri", "src", "bootstrap.rs")
def _load_setup():
spec = importlib.util.spec_from_file_location("vs_setup", _SETUP)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
def test_rocm_opt_in_requires_explicit_variant():
setup = _load_setup()
assert setup._rocm_opt_in({}) is None
assert setup._rocm_opt_in({"OMNIVOICE_TORCH_VARIANT": "auto"}) is None
assert setup._rocm_opt_in({"OMNIVOICE_TORCH_VARIANT": "ROCm"}) == setup.ROCM_TORCH_INDEX
assert (
setup._rocm_opt_in({"OMNIVOICE_TORCH_VARIANT": "rocm", "OMNIVOICE_TORCH_INDEX": "https://x/"})
== "https://x/"
)
def test_rocm_reinstall_targets_this_venv_with_bootstrap_pins():
setup = _load_setup()
cmd = setup.rocm_torch_reinstall_cmd("https://idx/", python="/venv/bin/python")
assert cmd == [
"uv",
"pip",
"install",
"--reinstall",
"--python",
"/venv/bin/python",
*setup.ROCM_TORCH_PINS,
"--index-url",
"https://idx/",
]
# Same pins + default index as the packaged app's bootstrap.
rs = open(_BOOTSTRAP, encoding="utf-8").read()
for pin in setup.ROCM_TORCH_PINS:
assert f'"{pin}"' in rs, f"{pin} drifted from bootstrap.rs"
assert f'"{setup.ROCM_TORCH_INDEX}"' in rs
def test_dev_backend_skips_resync_when_rocm_requested():
module_uri = Path(_DEV_BACKEND).as_uri()
script = f"""
const mod = await import({json.dumps(module_uri)});
console.log(JSON.stringify({{
base: mod.UVICORN_ARGS,
unset: mod.uvRunArgs({{}}),
auto: mod.uvRunArgs({{ OMNIVOICE_TORCH_VARIANT: "auto" }}),
rocm: mod.uvRunArgs({{ OMNIVOICE_TORCH_VARIANT: " ROCm " }}),
}}));
"""
completed = subprocess.run(
["node", "--input-type=module", "--eval", script],
check=True,
capture_output=True,
text=True,
)
observed = json.loads(completed.stdout)
base = observed["base"]
assert observed["unset"] == base
assert observed["auto"] == base
assert observed["rocm"] == [base[0], "--no-sync", *base[1:]]