Skip to main content
June 2026

0.41.0

VM hibernation

The biggest change in this release: avocado vm now hibernates an idle VM instead of burning host CPU, and wakes it transparently on the next connection.

Wake-on-connect supervisor

A long-lived avocado vm supervise process is spawned alongside QEMU. It owns the user-facing SSH port and docker socket (QEMU's host-forward moves to a loopback-only internal port) and:

  • Proxies inbound SSH/docker connections to QEMU, sending QMP cont to resume a paused guest on accept.
  • Tracks active connections and an idle timer; after idle.hibernate_after_secs with no activity, sends QMP stop. Host CPU on QEMU drops to ~0% while RAM stays resident, and any subsequent SSH or docker call wakes it.

The default idle timeout is 60 seconds. Override it with avocado vm config set idle.hibernate_after_secs N or AVOCADO_VM_IDLE_HIBERNATE_SECS.

PSCI idle-states for lower idle CPU

QEMU's -machine virt doesn't emit cpu-idle-states device-tree bindings, so under HVF idle vCPUs fell back to bare WFI and bounced through vmexit/vmenter — roughly 80% host CPU per vCPU at guest idle. On arm64 launches the CLI now splices PSCI idle-states into the machine DTB (pure-Rust FDT parsing, no dtc dependency) and caches the patched copy under ~/.avocado/vm/dtb/. Measured at smp=8 idle: 670% → 275–344% host CPU. The DTB cache key also moved to the QEMU binary mtime, saving ~300–500 ms per VM start.

Status and telemetry

  • avocado vm status reports hibernation — surfaces (hibernated — wakes on next ssh/docker call) when the guest is paused. The machine-readable status gains a tri-state paused field (true / false / unknown).
  • Infrastructure lane for telemetry — new --infra-port and --docker-socket-stream lanes are proxied like the user-facing ports but do not count toward the idle timer or wake a paused VM. This lets background telemetry (e.g. the desktop app's agent tunnel) observe the VM without pinning it awake or respawn-waking it, so hibernation actually sticks.
  • USB re-attach on wake — resuming a paused VM fires a vm.notify.woke hint so the desktop can re-attach USB devices that were dropped while the guest was frozen.

Reproducible builds

Channel snapshot pinning

Each target is now pinned to an immutable, point-in-time snapshot of its feed channel, recorded in the lock file. A clean and rebuild reproduces exactly, even after the live channel head advances or evicts the package versions the lock referenced.

  • Lock file v7 adds a per-target repo-snapshot. It's additive — v6 lock files read as "track head" and keep working.
  • On first fetch a target auto-pins to the channel's latest snapshot; pins freeze all sysroots together via AVOCADO_RELEASEVER with no per-call-site plumbing.
  • avocado update advances the snapshot pin to the newest and clears package/kernel pins so the next install re-resolves and re-locks (Cargo-style move-forward).
  • If a pinned snapshot has been garbage-collected, you get an actionable "run avocado update" error instead of an opaque dnf failure.

Auto-pinning now also works for projects that rely on the baked-in default feed (no explicit distro.repo.url), and the default repo URL is single-sourced (Config::effective_repo_url()).


Top-level permissions: block

Users and groups are now declared in a top-level permissions: map and referenced by name from rootfs.<name>.permissions / initramfs.<name>.permissions (or inlined), instead of being buried inside a single extension:

permissions:
  dev:
    users: [...]
    groups: [...]

rootfs:
  default:
    permissions: dev

This puts identity provisioning at the image layer, where one coherent passwd/shadow/group makes sense, and lets the same block be reused across rootfs and initramfs. When no permissions: is set on an image, no script section is emitted and the base packages' files are left untouched.

Deprecation: extensions that declare users: / groups: still work but now emit a deprecation warning. That path will be removed in a future release — move identity definitions to a top-level permissions: block.

avocado init now scaffolds new projects with this layout (a dev permissions profile referenced by explicit rootfs: / initramfs: blocks) and stamps the generated cli_requirement to >=0.41.0.


Private package feeds: custom CA / TLS

distro.repo.ca and distro.repo.tls_verify (overridable via AVOCADO_REPO_CA / AVOCADO_REPO_INSECURE) let the SDK trust a self-signed or private-CA package endpoint. The setting is applied across every dnf phase — SDK bootstrap, SDK packages, extensions, runtime, rootfs, initramfs, and the per-module dnf subcommands — for both host and target repo configs.


Build caching: per-step input hashes

Build stamps previously shared one input hash across ext install/build/image and another across runtime install/build, so editing a build-only field (e.g. an extension's image: args or a runtime's post_build:) over-invalidated the install stamp and cascaded into unnecessary rebuilds. Each step now hashes exactly the inputs it consumes, the kernel: block is hashed narrowly so cosmetic edits don't invalidate unrelated stamps, and post_build / post_install hooks now hash script contents (not just the path). STAMP_VERSION bumped 1 → 2; existing stamps invalidate once on first run after upgrade, then the narrower hashing applies.


Connect

  • avocado deploy on macOS — deploys now work on macOS. The TUF repo HTTP server runs inside the slirp-NAT'd avocado-vm and was unreachable by the target device; the CLI now opens a per-deploy QMP host-forward and advertises the host's LAN IP so the device can fetch the repo. Validated end-to-end to a LAN Raspberry Pi 4.
  • Connect-signed deploy and runtime listing — adds Connect runtime listing and support for connect-signed deploys, with output tuned for desktop integration.
  • Org selection in connect auth login--org <id> scopes a new token non-interactively; multi-org users get an interactive picker, single-org users skip it. Org-scoped commands also fall back to connect.org in avocado.yaml, then the active profile's organization_id, before erroring.
  • avocado connect ext (super-admin) — build-once publish of a packaged extension RPM to the feed, plus status and list of published versions. The publish feed is now derived from your distro config (get_distro_release() / get_distro_channel()) rather than separate --target-release / --target-channel flags, so build and publish share one source of truth.
  • Target-aware extension packagingavocado ext package stamps a Provides: avocado-target(<machine>) per supported_targets entry (wildcard when unset). This lands in the feed metadata so target compatibility can be queried without downloading the RPM, and connect ext publish derives its target list from supported_targets instead of a hardcoded default.
  • Nested extension layout — packaged extensions can nest content under /<ext_name>/ and self-describe via Provides: avocado-ext-layout(nested), installing into a shared includes installroot so one rpmdb tracks every extension with no cross-extension file collisions. Legacy packages keep the per-extension installroot; final content lands at includes/<ext_name>/ either way, so consumers are unchanged.

Other changes

  • avocado init drops the channel suffix from the generated SDK image tag — it now pins only the distro release (docker.io/avocadolinux/sdk:{release}).
  • Cross-platform groundwork — the hibernation supervisor, QMP/QGA modules, and deploy port-forwarding are now cfg(unix)-gated so a Windows build compiles. On Windows the hibernation feature is unavailable (QEMU binds the user-facing port directly and the VM never auto-pauses).

Bug fixes

  • Root login no longer silently breaks when a runtime has no explicit rootfs: / initramfs: reference. runtime build now falls back to the default rootfs/initramfs for permissions resolution, so the root user's shadow entry is rewritten and the image's /etc/shadow carries the configured (empty) password instead of the inherited root:*:….
  • TUI install rendering fix — the unset-environment-variable warning for {{ env.VAR }} interpolation now routes through the output module instead of a raw eprintln!, fixing stranded/stacked spinner lines during installs that fetch remote extensions.