feat(release): split backend out of installer (download on first run, ~1.8GB → ~50MB)#73
feat(release): split backend out of installer (download on first run, ~1.8GB → ~50MB)#73debpalash wants to merge 1 commit into
Conversation
…first run All three desktop platforms previously built a single asset — installer + PyInstaller backend bundled together — that overshot GH Releases' 2 GB per-asset cap on Linux and Windows. The mac DMG got under the cap thanks to HFS compression, but Linux .deb and Windows MSI couldn't. NSIS and WiX both failed during their own size-bounded packaging steps too. Split the two: - Tauri installer ships WITHOUT the PyInstaller backend (`tauri.conf.json` bundle.resources is now empty). Installer sizes drop from ~1.8 GB to ~50 MB. - CI packages the frozen backend as `omnivoice-backend_<version>_<triple>.tar.gz` after tauri-action, and uploads it to the same draft release via `gh release upload`. Each tarball is gz-compressed + comfortably under 2 GB with the CPU-only torch wheel + strip=True from earlier PRs. - On first launch, `ensure_backend_ready()` checks three locations in order: resource dir (legacy), app_local_data_dir (new home for the downloaded backend), and the dev-mode `dist/` fallback. If none match, it downloads the tarball matching the current platform + app version from the GH Release and extracts into app_local_data_dir. Blocking on first run, no-op thereafter. - find_bundled_backend + backend_exe_name are platform-aware — they append .exe on Windows and scan all three roots. Dependencies added to src-tauri/Cargo.toml: ureq (HTTP), tar + flate2 (archive extract). No tokio — ureq is synchronous, which matches the existing setup() flow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe PR shifts backend distribution from static Tauri bundling to first-run download-and-extract from GitHub releases. The release CI now creates and uploads versioned backend tarballs. The Tauri app adds three dependencies for HTTP download and tarball extraction, removes static resource bundling, and implements platform-aware backend discovery and conditional bootstrap before spawning. ChangesBackend Bootstrap from Release Assets
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
frontend/src-tauri/src/lib.rs (1)
440-452:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDon’t ignore backend bootstrap failure in packaged startup.
Line 446 discards
ensure_backend_readyresult. If download fails, startup continues into fallback logic, which can degrade into an invalid dev-style path instead of a deterministic packaged failure mode.Suggested fix
- if !skip_spawn { + let bootstrap_ok = if !skip_spawn { // First-run bootstrap: the installer ships without the // PyInstaller backend so it fits under GH Releases' 2 GB // per-asset cap. Download + extract it into the per-user // app data dir if we don't have it yet. Blocking — the // webview stays on the initial loader until this resolves. - let _ = ensure_backend_ready(app); - } + ensure_backend_ready(app) + } else { + true + }; let has_bundled = find_bundled_backend(app).is_some(); + if !skip_spawn && !bootstrap_ok && !cfg!(debug_assertions) { + log::error!("Backend bootstrap failed; skipping backend spawn in packaged mode."); + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src-tauri/src/lib.rs` around lines 440 - 452, The call to ensure_backend_ready(app) is ignored, so download/extract failures let startup continue into the dev-like fallback; change the code around ensure_backend_ready(app) to inspect its Result/Option (the same symbol ensure_backend_ready) and, on error, log the failure with context and abort startup (return Err/exit early) instead of proceeding to the find_bundled_backend/backend_healthy(BACKEND_PORT) logic; ensure the code path respects skip_spawn and uses the actual outcome of ensure_backend_ready to prevent entering an invalid packaged startup path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/release.yml:
- Around line 240-257: Guard the release upload steps so they run only for tag
builds: update the steps named "Package backend tarball" / "Upload backend
tarball to draft release" to check that the workflow is running for a tag (e.g.,
use a conditional like startsWith(github.ref, 'refs/tags/')) before creating
BACKEND_TARBALL and invoking gh release upload; ensure the upload step that
calls gh release upload "$BACKEND_TARBALL" is skipped for workflow_dispatch or
non-tag refs to avoid failing when no release exists.
In `@frontend/src-tauri/src/lib.rs`:
- Around line 126-133: The code unconditionally pushes
PathBuf::from("../../dist") into roots (near the roots variable and exe_name
usage), causing release builds to probe a dev-relative binary; restrict that
push to dev/debug builds by wrapping it in a build-config check (e.g., if
cfg!(debug_assertions) { roots.push(PathBuf::from("../../dist")); }) or use an
equivalent #[cfg(debug_assertions)] guard so only non-release builds include the
"../../dist" candidate.
- Around line 187-203: The download path currently reads resp into reader and
directly creates GzDecoder/Archive and calls archive.unpack(&dest_root); add an
integrity check step before extracting: read the response bytes (or stream to a
temp file) instead of piping straight into GzDecoder, compute and verify a
checksum (e.g., SHA-256) or verify a cryptographic signature with the project's
public key against a bundled/remote expected value, and only proceed to create
flate2::read::GzDecoder::new(...) and call archive.unpack(&dest_root) if the
verification succeeds; ensure you update the variables referenced here (url,
resp, reader, gz, archive.unpack) to use the verified temp file/byte buffer and
return an io::Error when verification fails.
---
Outside diff comments:
In `@frontend/src-tauri/src/lib.rs`:
- Around line 440-452: The call to ensure_backend_ready(app) is ignored, so
download/extract failures let startup continue into the dev-like fallback;
change the code around ensure_backend_ready(app) to inspect its Result/Option
(the same symbol ensure_backend_ready) and, on error, log the failure with
context and abort startup (return Err/exit early) instead of proceeding to the
find_bundled_backend/backend_healthy(BACKEND_PORT) logic; ensure the code path
respects skip_spawn and uses the actual outcome of ensure_backend_ready to
prevent entering an invalid packaged startup path.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 68280e5d-3c60-43a5-8f68-b0d30bb2d5ff
📒 Files selected for processing (4)
.github/workflows/release.ymlfrontend/src-tauri/Cargo.tomlfrontend/src-tauri/src/lib.rsfrontend/src-tauri/tauri.conf.json
| - name: Package backend tarball | ||
| shell: bash | ||
| run: | | ||
| VER="${GITHUB_REF_NAME#v}" | ||
| OUT="omnivoice-backend_${VER}_${{ matrix.rust_target }}.tar.gz" | ||
| tar -czf "$OUT" -C dist omnivoice-backend | ||
| ls -lah "$OUT" | ||
| echo "BACKEND_TARBALL=$OUT" >> "$GITHUB_ENV" | ||
|
|
||
| - name: Upload backend tarball to draft release | ||
| shell: bash | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| gh release upload "${{ github.ref_name }}" \ | ||
| "$BACKEND_TARBALL" \ | ||
| --repo "$GITHUB_REPOSITORY" \ | ||
| --clobber |
There was a problem hiding this comment.
Guard release-upload steps to tag builds only.
These steps currently run for workflow_dispatch too. If no release exists for github.ref_name, Line 254 (gh release upload) fails and breaks manual runs.
Suggested fix
- name: Package backend tarball
+ if: startsWith(github.ref, 'refs/tags/v')
shell: bash
run: |
VER="${GITHUB_REF_NAME#v}"
OUT="omnivoice-backend_${VER}_${{ matrix.rust_target }}.tar.gz"
tar -czf "$OUT" -C dist omnivoice-backend
ls -lah "$OUT"
echo "BACKEND_TARBALL=$OUT" >> "$GITHUB_ENV"
- name: Upload backend tarball to draft release
+ if: startsWith(github.ref, 'refs/tags/v')
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/release.yml around lines 240 - 257, Guard the release
upload steps so they run only for tag builds: update the steps named "Package
backend tarball" / "Upload backend tarball to draft release" to check that the
workflow is running for a tag (e.g., use a conditional like
startsWith(github.ref, 'refs/tags/')) before creating BACKEND_TARBALL and
invoking gh release upload; ensure the upload step that calls gh release upload
"$BACKEND_TARBALL" is skipped for workflow_dispatch or non-tag refs to avoid
failing when no release exists.
| roots.push(PathBuf::from("../../dist")); | ||
| for root in roots { | ||
| let candidates = [ | ||
| root.join(format!("backend/omnivoice-backend/{}", exe_name)), | ||
| root.join("backend/omnivoice-backend").join(&exe_name), | ||
| root.join("omnivoice-backend").join(&exe_name), | ||
| root.join(format!("omnivoice-backend/{}", exe_name)), | ||
| ]; |
There was a problem hiding this comment.
Restrict ../../dist probing to dev builds only.
Including this path unconditionally lets release builds pick up a relative dev binary if present, which is risky and can bypass intended bootstrap behavior.
Suggested fix
- roots.push(PathBuf::from("../../dist"));
+ if cfg!(debug_assertions) {
+ roots.push(PathBuf::from("../../dist"));
+ }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src-tauri/src/lib.rs` around lines 126 - 133, The code
unconditionally pushes PathBuf::from("../../dist") into roots (near the roots
variable and exe_name usage), causing release builds to probe a dev-relative
binary; restrict that push to dev/debug builds by wrapping it in a build-config
check (e.g., if cfg!(debug_assertions) {
roots.push(PathBuf::from("../../dist")); }) or use an equivalent
#[cfg(debug_assertions)] guard so only non-release builds include the
"../../dist" candidate.
| log::info!("Fetching backend tarball: {}", url); | ||
| let resp = ureq::get(&url) | ||
| .timeout(Duration::from_secs(60 * 30)) | ||
| .call() | ||
| .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("download: {}", e)))?; | ||
| if resp.status() != 200 { | ||
| return Err(io::Error::new( | ||
| io::ErrorKind::Other, | ||
| format!("download HTTP {} from {}", resp.status(), url), | ||
| )); | ||
| } | ||
|
|
||
| let mut reader = resp.into_reader(); | ||
| let gz = flate2::read::GzDecoder::new(LogReader::new(&mut reader)); | ||
| let mut archive = tar::Archive::new(gz); | ||
| archive.unpack(&dest_root)?; | ||
| log::info!("Backend extracted under {}", dest_root.display()); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Release workflow: checksum/signature generation =="
rg -n "sha256|checksum|minisign|cosign|\\.sig|signature|digest" .github/workflows/release.yml
echo
echo "== Bootstrap code: checksum/signature verification =="
rg -n "sha256|checksum|minisign|cosign|verify|signature|digest" frontend/src-tauri/src/lib.rsRepository: debpalash/OmniVoice-Studio
Length of output: 249
🏁 Script executed:
# Get context around the signature mention in release.yml
sed -n '200,220p' .github/workflows/release.ymlRepository: debpalash/OmniVoice-Studio
Length of output: 1032
🏁 Script executed:
# Search for latest.json usage in the codebase
rg -n "latest\.json" --type yaml --type ts --type rs --type tomlRepository: debpalash/OmniVoice-Studio
Length of output: 97
🏁 Script executed:
# Look for where the downloaded backend is executed
rg -n "execute|run|spawn|Command" frontend/src-tauri/src/lib.rs -A 2 -B 2Repository: debpalash/OmniVoice-Studio
Length of output: 6369
🏁 Script executed:
# Check if there's any verification function in the codebase
rg -n "fn.*verif|fn.*check.*sig|fn.*validate" frontend/src-tauri/src/ -iRepository: debpalash/OmniVoice-Studio
Length of output: 52
🏁 Script executed:
# Find and show the full download_and_extract_backend function
rg -n "fn download_and_extract_backend" frontend/src-tauri/src/lib.rs -A 50Repository: debpalash/OmniVoice-Studio
Length of output: 2138
🏁 Script executed:
# Search for latest.json usage in bootstrap/download code
rg -n "latest\.json" frontend/src-tauri/src/Repository: debpalash/OmniVoice-Studio
Length of output: 52
🏁 Script executed:
# Check the full context of the download function - look for any hash/signature variables
rg -n "download_and_extract_backend|tarball|backend.*url" frontend/src-tauri/src/lib.rs -B 2 -A 10Repository: debpalash/OmniVoice-Studio
Length of output: 3102
🏁 Script executed:
# Search entire codebase for any verification of downloaded files
rg -n "verify|signature|digest|checksum|sha" frontend/src-tauri/src/ -iRepository: debpalash/OmniVoice-Studio
Length of output: 52
Add integrity verification before extracting/executing downloaded backend.
The app downloads and executes an untrusted backend tarball on first run, but provides only HTTP 200 status verification. There is no checksum or signature verification in this path, weakening supply-chain guarantees for the bootstrap flow.
The backend URL is hardcoded and downloaded directly from GitHub releases (lines 179–197), then immediately extracted (line 202) and executed (line 347) without any integrity checks. The Tauri updater has signature support, but that is not wired for the backend tarball.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src-tauri/src/lib.rs` around lines 187 - 203, The download path
currently reads resp into reader and directly creates GzDecoder/Archive and
calls archive.unpack(&dest_root); add an integrity check step before extracting:
read the response bytes (or stream to a temp file) instead of piping straight
into GzDecoder, compute and verify a checksum (e.g., SHA-256) or verify a
cryptographic signature with the project's public key against a bundled/remote
expected value, and only proceed to create flate2::read::GzDecoder::new(...) and
call archive.unpack(&dest_root) if the verification succeeds; ensure you update
the variables referenced here (url, resp, reader, gz, archive.unpack) to use the
verified temp file/byte buffer and return an io::Error when verification fails.
* docs(phase-5): research opt-in bug reporting Phase 5 research: prefilled-URL GitHub Issues pattern, default-deny payload, redaction layer, two-step consent UX, rate/dedup/recursion safeguards, aggregation across Python/Rust/React error producers. Builds on Phase 1's links.py + errorDocsMap deeplink infrastructure; uses already-installed @tauri-apps/plugin-opener (^2.5.4). No new packages required. Covers REPORT-01..12 with confidence levels, 8 pitfalls, subprocess-engine error capture handoff to Phase 2, security domain mapped to ASVS, and 3-wave delivery plan (redactor + payload, consent UI, aggregation + pre-submit search). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(phases): research for Phases 2, 3, 4, 6 (Engine + Supertonic + Spikes + Release) * docs(stack): bump supertonic pin 1.2.3 → 1.3.1 (Phase 3 research finding) * docs(phases): plan Phases 2-6 for v0.3.0 fat-milestone release 15 new plan files + 2 ADR decision docs across 5 phases. Combined with Phase 1's 3 plans, the v0.3.0 milestone now has 18 PLAN.md files covering all 7 phases (Phase 0 already complete via PR #71). PHASE 2 (Engine Isolation — 4 plans): - 02-01: SubprocessBackend primitive + echo sidecar POC + graceful is_available wrap (ENGINE-01/05) - 02-02: _safe_torchaudio_save helper + migrate 11 WAV write sites + #48 regression (BUG-01) - 02-03: IndexTTS sidecar entry + venv-probe bootstrap + IndexTTS2Backend rewire (ENGINE-02/03/04/07, closes #42) - 02-04: Engine Compatibility Matrix UI + /engines/{id}/health route (ENGINE-06) PHASE 3 (Supertonic-3 + Mirror — 2 plans): - 03-01: Supertonic-3 engine on SubprocessBackend + SHA pin + license gate (TTS-01..06) - 03-02: bootstrap.rs mirror cascade + UV_DEFAULT_INDEX migration + frozen enforcement + docs (INST-07..11) PHASE 4 (Spike-first Adaptive & Specialty — 2 plans + 2 ADRs): - 04-01: OmniVoice-GGUF hardware-adaptive engine + quant_map + bundled binaries (SPIKE-01, GGUF-01..06) - 04-02: OmniVoice-Singing subclass + dub pipeline singing mode + segment detector (SPIKE-02, SING-01..05) - SPIKE-01-gguf.md + SPIKE-02-singing.md ADRs in .planning/decisions/ PHASE 5 (Opt-in Bug Reporting — 3 plans): - 05-01: Redactor + BugReporter + URL builder + rate/dedup/recursion safeguards + FastAPI router (REPORT-01/02/03/05/06/07/08/10/11) - 05-02: BugReportDialog two-step consent + PrivacyPanel + ErrorBoundary integration + Rust panic hook (chained) (REPORT-01-Rust/04/09/12) - 05-03: Dry-run vs 3 historical issues + cross-platform openUrl smoke + Phase 2 subprocess-errors handoff (REPORT-02 smoke, REPORT-03 expansion, REPORT-09) PHASE 6 (Release + Retro — 4 plans): - 06-01: rc1 prep — version bump across 4 sources + CHANGELOG + retro stub + PR-73-strategy doc (REL-01/03/06) - 06-02: CI guards — workflow-parity actionlint + tag-shaped dry-run (Phase 0 retro options B + C; closes release-engineer gap) - 06-03: PR #73 reimplementation (NOT rebase) — backend-split installer with mirror-cascade integration + pill-mode regression checkpoint - 06-04: Execute the release — pre-tag gates + 4-OS clean-VM + 48h soak + tag + retro + 3 v0.4 deferral tracking issues (REL-01/02/03/04/05/06) Scope decisions locked in plans (council session): - SoniTranslate refactor DEFERRED to v0.4 (Phase 2 ships SubprocessBackend without migrating Soni) - macOS notarization DEFERRED to v0.4 (Phase 6 ships xattr -cr automation per CLAUDE.md Key Decision #7) - supertonic pin 1.2.3 → 1.3.1 (already committed in ba63733) - SPIKE-01 and SPIKE-02 both GO; 13/13 Phase 4 reqs stay in scope - PR #73 reimplemented, not rebased (93 commits behind main) All 18 plans validated via gsd-sdk frontmatter.validate + verify.plan-structure. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Splits the PyInstaller backend out of the Tauri installer so the installer drops from ~1.8 GB to ~50 MB. Backend is downloaded + extracted on first launch from a separate GitHub Release asset.
Originally branched 3 weeks ago (Apr 23) — opening now after stabilization milestone planning surfaced installer size as a recurring user-friction point.
Why
GitHub Releases enforces a 2 GB per-asset cap. Linux
.deband Windows MSI both overshot this when shipping PyInstaller's frozen backend (~1.8 GB after CPU-torch slim) bundled inside the installer. NSIS and WiX both failed during their own size-bounded packaging steps too. The macOS DMG snuck under via HFS compression but the cross-platform parity broke.Changes
Tauri installer ships WITHOUT the PyInstaller backend —
tauri.conf.jsonbundle.resourcesis now empty for the backend path. Installer drops from ~1.8 GB to ~50 MB.CI packages backend separately —
release.ymladds a step aftertauri-actionthat tarballs the frozen backend as `omnivoice-backend__.tar.gz` and uploads it to the same draft release viagh release upload. Each tarball is gz-compressed, comfortably under 2 GB.First-launch downloader — new
ensure_backend_ready()infrontend/src-tauri/src/lib.rschecks three locations in order:app_local_data_dir(new home for downloaded backend)dist/fallbackIf none match, it downloads the platform-version-matching tarball from the GH Release and extracts into
app_local_data_dir. Blocking on first run, no-op thereafter.find_bundled_backend+backend_exe_nameare now platform-aware — append `.exe` on Windows, scan all three roots.New Rust deps: `ureq` (HTTP), `tar` + `flate2` (archive extract). No tokio — ureq is synchronous, matches existing
setup()flow.Branch is 93 commits behind main. Conflicts expected in:
Auto-merges cleanly:
Reviewer should rebase against latest main in the GitHub UI or locally — resolve the lib.rs conflicts carefully to preserve both the split-backend logic and the new `WebviewWindowBuilder` widget creation from `b479f9b`.
Test plan
Relation to milestone
Not in original v0.3.x stabilization scope per PROJECT.md, but solves a real install-friction concern. Decision: defer to v0.4 unless installer-size complaints land soon.
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Chores