Skip to content

Fedora Atomic (Silverblue/Bazzite/Bluefin) support, plus installer and whisper fixes - #517

Open
Sallvainian wants to merge 10 commits into
mbailey:masterfrom
Sallvainian:bazzite-atomic-support
Open

Fedora Atomic (Silverblue/Bazzite/Bluefin) support, plus installer and whisper fixes#517
Sallvainian wants to merge 10 commits into
mbailey:masterfrom
Sallvainian:bazzite-atomic-support

Conversation

@Sallvainian

Copy link
Copy Markdown
Contributor

Adds Fedora Atomic (Silverblue, Kinoite, Bazzite, Bluefin) support to the
installer, plus several fixes that are not Atomic-specific.

Why

The installer ran sudo dnf install for anything reporting ID=fedora. On
Atomic variants that can never succeed — /usr is read-only and Bazzite ships a
dnf shim that refuses install outright — so the dependency step failed and
took the rest of the run with it. The dependency checks used rpm -q, so a
fully working install reported 7 of 7 core deps missing.

Fixes that apply to everyone, not just Atomic

  • get_package_manager() tried Homebrew first on every platform. Any
    Fedora/Debian user with Homebrew installed got brew handed distro package
    names it cannot resolve. Native manager now wins.
  • voicemode service install exited 0 on failure, so the installer's
    subprocess.run(..., check=True) never raised — it printed
    Whisper installation failed and Whisper STT service installed back to back.
  • The error handler crashed instead of reporting. e.stderr.decode() on a
    text=True subprocess raises AttributeError inside the handler; a sibling
    except Exception cannot catch that, so it escaped and destroyed the cmake
    output at the one moment it mattered. Two more handlers could raise over their
    own error (sys imported inside the try that needed it; process unbound on
    Ctrl-C during Popen).
  • CUDA builds failed wherever the distro's GCC outpaces nvcc — Fedora 44
    ships GCC 16, CUDA 13.3 accepts ≤15, so -DGGML_CUDA=ON aborted at configure
    with unsupported GNU version. The ceiling is now read from CUDA's own
    crt/host_config.h and a compatible g++ is selected.
  • Homebrew had the shortest install timeout (300s vs 600s) despite the
    heaviest work; the rust bottle exceeded it and was killed mid-download.
  • --model and --no-gpu did not exist on service install, though the
    installer shells out with --model and the CUDA error told users to pass
    --no-gpu.
  • A whisper GPU build hard-failed when the GPU was only auto-detected,
    stranding anyone with an NVIDIA card and no toolkit. Now falls back to CPU.
  • Selection tests were host-dependent — they mocked manager availability but
    not the OS probes, so test_get_package_manager_dnf failed for any
    Silverblue/Bazzite contributor while passing in CI.

Atomic support

Detects /run/ostree-booted, routes installs through Homebrew (no root, no
reboot) with an RPM→formula map, and replaces rpm -q with capability probes
(pkg-config, loadable-library, Python.h). python3-devel is skipped because
uv tool install builds against uv's managed CPython, which ships its own
headers. Anything with no Homebrew equivalent prints the rpm-ostree install
command and reboot rather than failing with a bare dnf error.

Testing

pytest: 1925 passed, 56 skipped, 0 failed.

Verified end to end on Bazzite 44 (Fedora Atomic, GNOME/Wayland, RTX 4070 Ti):
voice-mode-install goes from 7 missing core deps to 0, installs cmake,
portaudio-devel, cargo and rust itself, builds whisper.cpp with CUDA
(ARCHS = 890), and a full Kokoro→whisper round trip transcribes correctly.

Sallvainian and others added 10 commits August 12, 2026 23:03
The installer ran `sudo dnf install` for anything reporting ID=fedora in
/etc/os-release. Atomic variants report exactly that but have a read-only
/usr, and Bazzite ships a dnf shim that refuses `install` outright, so the
dependency step always failed and aborted the run.

Detect ostree systems via /run/ostree-booted and route package installation
through Homebrew, which needs neither root nor a reboot and is the normal way
to get development headers on an immutable OS. Packages with no homebrew-core
equivalent now print the `rpm-ostree install` command, the reboot it requires,
and the distrobox alternative, rather than a bare dnf error. python3-devel is
skipped entirely: voice-mode installs with `uv tool install`, which builds
against uv's managed CPython and ships its own headers.

Also fix the dependency checks themselves, which asked `rpm -q` whether a
package was present and so reported missing whenever it came from anywhere
other than an RPM. They now test for the capability instead of the packaging:
pkg-config for alsa and portaudio-2.0, a Python.h probe for python3-devel, a
loadable-library probe for portaudio (sounddevice dlopens it via cffi and
never needs the headers), and pactl for the PulseAudio utilities, which
PipeWire provides on modern Fedora. Homebrew's pkgconfig directories are added
to PKG_CONFIG_PATH for check commands, since Homebrew is not on pkg-config's
default search path on Linux.

Verified on Bazzite (Fedora Atomic 44, ostree image bazzite-gnome-nvidia-open).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous commit fixed installer/voicemode_install/, but `voicemode deps`
does not use it. The repo carries two independent dependency systems with
separate copies of dependencies.yaml, so the CLI still reported every core
dependency missing on Fedora Atomic.

Port the same capability-based checks to voice_mode/utils/dependencies/ and
voice_mode/dependencies.yaml, and add ostree/Homebrew awareness there.

Two additional fixes specific to this checker:

- Check commands now run with shell=True. They were passed through
  str.split(), so a command containing quoted arguments or a `||` fallback was
  silently mangled into the wrong argv. The standalone installer's checker
  already used shell=True; this aligns them. Commands come from the bundled
  YAML, not user input.

- python3-devel is probed against the interpreter that would actually build
  the extensions, exported as VOICEMODE_PYTHON, instead of PATH's python3.
  Under `uv tool install` these differ -- uv's managed CPython ships headers
  while the system python3 often has none -- which made a working install
  report python3-devel missing.

On Bazzite this takes `voicemode deps` from 7 missing core dependencies to 0.
The three still reported (portaudio-devel, cargo, rust) are genuinely absent
and are build dependencies of the local Whisper/Kokoro engines, not of the
cloud path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`voicemode service install whisper` failed on Fedora Atomic with "No available
formula with the name portaudio-devel". dependencies.yaml lists Fedora RPM
names and they were passed to Homebrew verbatim. `cargo` is worse than a
rename: it has no formula at all, since cargo ships inside `rust`, so
`brew install cargo rust` dies on the first name.

Add OstreeBrewManager, which maps RPM names to homebrew-core formulae and
de-duplicates the result (cargo + rust collapse to one `rust`). Names with no
formula no longer produce a bare brew error -- they report the rpm-ostree
install command with its reboot, and the distrobox alternative. Add
RpmOstreeManager for Atomic systems with no Homebrew, which explains both
options and never shells out to dnf.

Also fix package manager selection, which is a bug beyond Atomic:
get_package_manager() tried BrewManager first on every platform, so any Fedora
or Debian user with Homebrew installed got brew selected and fed distro package
names it cannot resolve. Prefer the native manager on Linux and fall back to
Homebrew; Atomic systems now route to Homebrew deliberately instead of by
accident.

The previous commit fixed dependency *checking* in this package but left
*installation* using the old path -- the same two-systems split that motivated
that commit. Verified by running the failing flow on Bazzite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`voicemode service install whisper` printed "❌ Whisper installation failed"
and then exited 0. The installer invokes it with subprocess.run(check=True),
so nothing raised and it printed "✅ Whisper STT service installed" on the very
next line. Users were told a service installed when it had not. Add sys.exit(1)
to the whisper, kokoro, voicemode and mlx-audio failure branches, and to the
unknown-service branch.

The Whisper dependency check also suggested `sudo dnf install cuda-toolkit`
and `sudo apt-get install build-essential` based only on which binary exists.
Fedora Atomic ships dnf but cannot install with it, so that suggestion sends
the user down a path that always fails. On ostree systems, CUDA now suggests
--no-gpu or a distrobox container (there is no Homebrew formula for the CUDA
toolkit and layering it is heavy), and build tools suggest Homebrew or
rpm-ostree with its reboot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… install

Two failures hit while installing Kokoro's dependencies on Bazzite.

Homebrew had the shortest install timeout of the three package managers -- 300s
against 600s for apt and dnf -- despite doing the heaviest work, since it
downloads large bottles and can fall back to building from source. The `rust`
bottle Kokoro needs is ~400MB, which took roughly 20 minutes on an ordinary
connection and was killed at 5 with only a timeout to show for it. Raised to
1800s. Both halves matter: without the name dedup the install dies instantly on
`cargo`, and without the timeout it dies five minutes in.

Separately, `voicemode service install` accepted only --force, while
whisper_install() has always taken `model` and `use_gpu`. So the CUDA error
message instructing users to "use --no-gpu for CPU-only" referred to a flag
that did not exist, leaving no way to build whisper without the CUDA toolkit --
which on Fedora Atomic cannot be installed at all. The standalone installer
also shells out to `service install whisper --model <name>`, which would have
failed as an unknown option for any non-default model.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…toolkit

whisper_install() auto-detects the GPU when use_gpu is not specified, then
required nvcc and aborted the install without it. So any machine with an NVIDIA
card but no CUDA toolkit could not install Whisper at all -- despite nothing
having asked for GPU support, and despite whisper.cpp building and running
perfectly well on CPU. On Fedora Atomic it is a dead end, since the CUDA
toolkit cannot be installed there and the error suggested `sudo dnf install
cuda-toolkit`, which cannot work.

Track whether GPU support was auto-detected or explicitly requested. When
auto-detected, a missing nvcc now warns and builds CPU-only. An explicit
--use-gpu still reports the missing toolkit as an error, so a deliberate
request is never silently downgraded to something slower than asked for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nvcc refuses any host compiler newer than the version its headers were built
against, and -DGGML_CUDA=ON was passed with an unmodified environment. On
distributions that move faster than CUDA the build aborted at configure time
with "#error -- unsupported GNU version!" -- Fedora 44 defaults to GCC 16
while CUDA 13.3 stops at 15.

Read the ceiling from CUDA's crt/host_config.h instead of hardcoding a table
that goes stale every release, then pick the newest installed g++ at or below
it and pass it as CMAKE_CUDA_HOST_COMPILER, with CUDAHOSTCXX and NVCC_CCBIN
set to match so CMake and ggml's own nvcc calls cannot disagree. Name the
package to install when nothing compatible is present.

Also correct the Atomic CUDA guidance: it suggested building in a distrobox
container, but voice-mode runs whisper as a host service and a binary built
there links the container's CUDA libraries. Point at NVIDIA's runfile instead
-- /usr/local is a symlink to /var/usrlocal, so a host-native toolkit installs
without layering or a reboot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019fWTAFm6Cjf8jpprtDd1YN
whisper_install's CalledProcessError handler called e.stderr.decode(), but the
configure and build steps run with text=True, so e.stderr is already str. The
handler raised AttributeError itself, and a sibling `except Exception` cannot
catch an exception raised by a preceding handler, so it escaped the function and
destroyed the cmake output at the one moment it mattered -- a failed GPU build
printed a wall of linker noise ending in an unrelated
"'str' object has no attribute 'decode'".

The identical line in the Kokoro installer avoids crashing today only because
none of its checked calls capture output, so it silently reports None for every
failure. Both now decode only when handed bytes.

Two more handlers could raise over their own error:
- disable_sounddevice_stderr_redirect() imported sys inside its try, making it
  function-local; when the sounddevice import failed -- the case the handler
  exists for -- sys was unbound and it raised UnboundLocalError.
- Exchange tailing called process.terminate() from its KeyboardInterrupt
  handler; a Ctrl-C during Popen() itself left process unbound.

Separately, the installer reported success after a component failed: a run where
Whisper failed and Kokoro succeeded logged the failure and then
"Installation completed" {"success": true}, exiting 0. log_complete took a
literal True, so no component result could reach it. Failures are now collected,
the status reflects them, the summary names the retry command, and it exits 1.
The log gains the error text it was discarding, and the unreachable post-check
`else` branches are removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019fWTAFm6Cjf8jpprtDd1YN
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…endent

TestPackageManagerSelection mocked each manager's check_available but not the
OS detection, so it asserted against whatever machine ran it. get_package_manager
returns on the Darwin branch on macOS and on the ostree branch on Fedora Atomic,
in both cases before the dnf/apt/brew list is consulted -- so
test_get_package_manager_dnf failed for any Silverblue/Bazzite contributor while
passing in CI.

Pin platform.system and is_ostree_system, and add coverage for the Atomic
branches: Homebrew when present, the explaining fallback when not, and that
DnfManager is never selected there even with dnf on PATH. Also cover the RPM to
Homebrew name translation -- stripping -devel and collapsing cargo/rust onto the
single rust formula.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Sallvainian

Copy link
Copy Markdown
Contributor Author

The red check on this PR is three pre-existing failures on Python 3.10, not
something this branch introduces. They reproduce on a clean master checkout
with none of this PR's code present:

$ git checkout 5c1b3e4          # master tip, this PR's base
$ uv run --python 3.10 -m pytest \
    tests/test_mcp_shutdown_patch.py::TestDispatchUntilClosed::test_cancels_on_iteration_error \
    tests/test_turns_discoverability.py

FAILED tests/test_mcp_shutdown_patch.py::TestDispatchUntilClosed::test_cancels_on_iteration_error
        NameError: name 'ExceptionGroup' is not defined
FAILED tests/test_turns_discoverability.py::test_turns_schema_property_carries_the_description
        KeyError: 'description'
FAILED tests/test_turns_discoverability.py::test_turns_schema_description_covers_key_contract_points
        KeyError: 'description'
3 failed, 4 passed
  • ExceptionGroup is a 3.11+ builtin, so mcp_shutdown_patch referencing it
    unguarded fails on 3.10. exceptiongroup is already an indirect dependency, so
    if sys.version_info < (3, 11): from exceptiongroup import ExceptionGroup
    would cover it.
  • test_turns_discoverability reads
    tool.parameters["properties"]["turns"]["description"], which the installed
    fastmcp no longer emits for that parameter.

These only surface on PRs — Test Voice Mode MCP is pull_request-triggered, so
master never runs it and CI stays green on the branch itself.

The CI job also runs fail-fast, so ubuntu 3.10 failing cancelled the other five
matrix legs before they finished. On the run for this PR, tests/test_dependencies.py
passed 25/25 (1937 passed overall) — that file is where this PR's changes live.

Happy to send the 3.10 fixes as a separate PR if that's useful, since they're
unrelated to the Atomic support here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant