Skip to content

Install ALSA on first run - #44

Open
Nicolas Palpacuer (NickPPC) wants to merge 4 commits into
mainfrom
add-first-run-system-deps
Open

Install ALSA on first run#44
Nicolas Palpacuer (NickPPC) wants to merge 4 commits into
mainfrom
add-first-run-system-deps

Conversation

@NickPPC

@NickPPC Nicolas Palpacuer (NickPPC) commented Aug 3, 2026

Copy link
Copy Markdown

Summary

On a host without ALSA the audio-module binary fails to start with libasound.so.2: cannot open shared object file and exits 127. Desktop images ship it and server or minimal container images do not, so the gap only appears on freshly provisioned machines — where it surfaces as model not registered on every resource depending on the speaker or microphone, reading like a config error rather than a missing .so. The tarball already bundles JACK and libdb; this adds a first_run.sh that installs ALSA the same way.

⚠️ first_run.sh exits non-zero when it cannot install the libraries, which aborts the machine's reconfiguration by design (local_robot.go). The machine keeps its previous working config and already-running modules keep running, rather than applying a config it cannot serve. No success marker is written on failure, so first_run retries automatically once the host is fixed.

Because the RDK only advances its stored config after the first_run phase succeeds, an aborted reconfiguration re-runs this script every cloud-config refresh (~10s). The install attempt is therefore rate-limited to once per 10 minutes so it does not hold the package-manager lock against an operator fixing the host by hand — the diagnostic and the non-zero exit still happen on every run. The stamp recording that rate limit sits beside the module directory (root-owned), not in /tmp, so it cannot be pre-created as a symlink by a local user and truncated by a root-run script.

Review guide

  • first_run.sh is the entire change; everything else is packaging wiring. Start at main() at the bottom.
  • The packaging needs all three hops to line up: exports_sources → CMake install → conan deploy(). Miss one and the script silently never reaches the machine.

Changes

  • first_run.sh: checks for libasound.so.2 via ldconfig, installs the right package for the distro, re-verifies, and prints a copy-pasteable command if it is still missing
  • meta.json: adds the first_run key, which the module did not previously declare
  • CMakeLists.txt: install(PROGRAMS ...) rather than FILES, so the script keeps its executable bit — the RDK execs it directly
  • conanfile.py: exports the script and copies it into module.tar.gz alongside run.sh
  • README.md: documents the dependency and what to do if auto-install can't proceed

Testing

Verified in throwaway containers (all with empty package caches, i.e. the fresh-host case):

  • ubuntu:24.04 — installs libasound2t64 and re-verifies the soname
  • second run on the same host — no-op; macOS — no-op
  • non-root with no passwordless sudo — exits 1, prints the manual command and the consequence
  • two consecutive failing runs — the second still exits 1 but skips the package manager (throttle)

The script handles the Debian/Ubuntu 64-bit time_t rename by trying libasound2t64 then libasound2, so it works on noble and on jammy/bookworm without pinning a release. The soname is looked up through ldconfig, whose cache is architecture-aware, so no /usr/lib/<triplet> paths are hardcoded.

The packaging is confirmed against this PR's own CI artifact rather than by inspection — module-darwin-arm64 from the build job unpacks to:

drwx------  ./
-rwxr-xr-x  ./audio-module
-rwxr-xr-x  ./first_run.sh
-rw-r--r--  ./meta.json
-rwxr-xr-x  ./run.sh

i.e. first_run.sh lands at the tarball root next to meta.json with its executable bit intact, which is what the RDK needs to exec it.

Claude Code prompts used
  • "Can you then chweck the error logs for Viam machine part <redacted> and tell me what needs to be installed on the machine to make it work?"
  • "For the missing system libraries we identified previously, coould you make a PR against those modules to create or extend the frist_run file such that it installs those dependencies correctly (depending on the OS and architecture if applicable). Make sure to surface clear error messages for easier troubleshooting if something cannot be installed"

On a host without ALSA the audio-module binary fails to start with
"libasound.so.2: cannot open shared object file" and exits 127. A desktop image
happens to ship it, so the gap only shows up on a server or minimal container
image -- and it surfaces as "model not registered" on every resource that
depends on the speaker or microphone, which reads like a config error rather
than a missing .so. The tarball already bundles JACK and libdb, but ALSA has to
come from the host.

Add a first_run.sh that installs it, and wire it into the packaging: meta.json
declares it, CMake installs it with PROGRAMS so it keeps its executable bit (the
RDK execs it directly), and the conan deployer copies it into module.tar.gz.

The script:

  - checks whether libasound.so.2 is already present before touching a package
    manager, so it is a no-op on a host that is already set up, and does nothing
    on macOS
  - supports apt, dnf/yum, zypper, pacman and apk, including the Debian/Ubuntu
    64-bit time_t rename (libasound2t64 vs libasound2)
  - looks the soname up through ldconfig, whose cache is architecture-aware, so
    no /usr/lib/<triplet> paths are hardcoded
  - re-verifies afterwards and, if it is still missing, logs the exact command
    an operator should run by hand

It always exits 0, even when it cannot install anything. A non-zero exit from a
first_run script makes the RDK abort the whole machine's reconfiguration and
roll back to the previous config -- not just this module -- so a host we cannot
install on would take unrelated resources down with it. Exiting 0 leaves the
module to fail on its own, exactly as it does today, with actionable guidance in
the logs.

Tested in containers on ubuntu:24.04 (installs libasound2t64 and verifies),
twice over on the same host (idempotent), and as a non-root user with no sudo
(exits 0 and prints the manual command).
first_run.sh now exits non-zero when the required libraries are missing and
could not be installed, instead of exiting 0 and leaving the module to fail on
its own.

That deliberately aborts the machine's reconfiguration. The RDK keeps the
previous, working config, leaves already-running modules running, and marks this
module's package as failed -- which is preferable to applying a config the
machine cannot actually serve. Since no success marker is written on failure,
first_run is retried automatically once the host is fixed.

One consequence needs handling: the RDK only advances its stored config after
the first_run phase succeeds (robot/impl/local_robot.go), and the cloud config
watcher pushes a config every refresh interval, so an aborted reconfiguration
re-runs this script roughly every 10 seconds. Re-running apt that often would
hold the package-manager lock and fight an operator trying to fix the host by
hand, so the install attempt is rate-limited to once per 10 minutes via a stamp
file under /tmp. The diagnostic is still printed and the exit code is still
non-zero on every run -- only the install attempt is skipped.

The failure message now states the actual consequence: that the machine will
keep its previous configuration until the libraries are installed, and that it
retries by itself afterwards.
The install-attempt stamp lived under /tmp, which any local user can write to.
That let an unprivileged user pre-create the path as a symlink and have this
script truncate the target, since first_run normally runs as root.

Record it as a sibling of the module directory instead, mirroring where the RDK
keeps its own .first_run_succeeded marker -- that directory is root-owned. When
VIAM_MODULE_ROOT is unset, which means someone is running the script by hand
outside the RDK, throttling is disabled rather than falling back to an unsafe
path; a manual run should attempt the install every time anyway.
The soname lookup table listed every library the OpenCV-based modules need
across all five package managers, even though this module only ever asks about
libasound.so.2. REQUIRED_SONAMES was already correct, so nothing was ever
checked or installed that shouldn't be, but the dead rows made the table look
like a shared list rather than this module's own dependency.

Drop the rows this module cannot reach, and reword the two comments that used
glib as their example. 46 lines lighter and the table now reads as exactly what
the module requires.
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