Skip to main content
May 2026

0.40.2

avocado vm UX

Streaming downloads with progress

avocado vm update previously buffered each artifact's entire response body in memory before writing to disk, with a 30‑second client-wide timeout that the 458 MB var.btrfs could not meet. Replaced with connect_timeout only plus a streaming download loop that writes each chunk directly to disk:

  • Human mode renders an indicatif MultiProgress queue matching avocado connect upload's style — one cyan/blue bar per artifact, finishing with (done).
  • JSON mode emits throttled download_started, download_progress, and download_completed NDJSON events at roughly 10 Hz.

Smarter avocado vm start defaults

After avocado vm update populates ~/.avocado/vm/install/, vm start now finds it without needing --vm-source or AVOCADO_VM_DIR. Resolution order is now:

  1. --vm-source flag
  2. AVOCADO_VM_DIR env var
  3. ~/.avocado/vm/install/ (managed install)
  4. ~/.avocado/vm/artifact-dir (dev workflow)
  5. Helpful error pointing at avocado vm update

End-users on the managed path no longer have to think about environment variables.

Persistent CPU and memory overrides

avocado vm start --memory-mib and --cpus are now optional. They resolve to runtime.cpus and runtime.memory_mib in ~/.avocado/vm/config.yaml (also written by Avocado.app's settings UI). Passing a flag writes it back so the next flag-less vm start and the desktop app converge. vm reset / vm update and route-on-demand callers now pass None instead of hardcoded 4096 MiB / 4 CPUs, picking up persisted settings.

avocado vm config subcommand

~/.avocado/vm/config.yaml is the shared host/guest configuration file Avocado.app reads and writes. The CLI gains the matching surface:

avocado vm config get <key>
avocado vm config set <key> <value>
avocado vm config unset <key>
avocado vm config list

The first consumer is network.dns (and an optional one-shot --dns on vm start). The configured resolver is pushed into the guest via resolvectl once SSH is up, resolving the macOS-VPN case where scoped resolvers on the host are invisible to SLIRP's DNS proxy.


avocado vm lifecycle

CLI owns QEMU lifecycle on macOS

vm start and vm stop previously delegated to Avocado.app over a synchronous JSON-line IPC client, which made the CLI unusable when the app was not installed and stalled vm stop whenever the app's main actor was busy. The CLI now spawns and signals QEMU directly on every platform; Avocado.app, when present, adopts the PID via its existing pidfile reconciler.

Remaining IPC (vm.notify.{starting,running,stopping,stopped}) is now best-effort dashboard hinting only: 100 ms timeouts, silent no-op when unreachable, self-heals via the reconciler within roughly two seconds.

A second virtio-serial port (avocado.controlcontrol.sock) lets Avocado.app's USB host bridge and control plane talk to avocado-vm-agent without spawning QEMU itself.


Desktop integration (JSON output)

avocado runtime deploy --output json

Skips TUI rendering and emits NDJSON task_registered, step, and step_error events per phase (stamps, hash-collection, metadata-sign, deploy). Phase names mirror the human labels so the desktop app can render them directly without translation.

avocado config show --detail

Adds a detail block alongside the existing narrow projection:

  • Per-runtime extension references with defined and enabled flags plus node_paths for cross-reference navigation.
  • Per-extension types, packages, services, and used_by_runtimes.
  • An SDK image and packages summary.

Default output is byte-stable so existing consumers (the desktop app's project-list scan) keep working unchanged.


Cross-platform groundwork

Windows MSVC compile check

A new windows-check PR job runs cargo check --target x86_64-pc-windows-msvc. The qga (AF_UNIX QEMU guest agent) and qmp (QEMU monitor over AF_UNIX) modules are now #[cfg(unix)] at the module declaration; call sites in lifecycle::stop and boot_sync::wait_for_guest_ready get matching gates with a non-Unix fallback that waits for SSH only. libc::SIG{TERM,KILL} is replaced with local POSIX constants at the two referencing sites. No behavior change on macOS or Linux.


Runtime extension map syntax everywhere

runtimes.<name>.extensions accepts the map form:

runtimes:
  dev:
    extensions:
      - foo: { enabled: false }
      - bar

utils::runtime_extension::RuntimeExtensionSpec::parse_entry is the single source of truth — every list-walker (Config::extension_deps, Config::find_active_extensions, runtime/build, runtime/deps, build, install) now funnels through it. Two more call sites that were silently skipping map-form entries — collect_extension_dependencies in build.rs and the runtime input-hash compute in stamps.rs — are fixed in this release. runtime build propagates enabled: false into the manifest's AVOCADO_EXT_DISABLED so avocadoctl skips activation at refresh time.


Extension build

AVOCADO_ON_MERGE=systemd-tmpfiles --create for tmpfiles.d/

Mirrors the existing sysusers.d and ld.so.conf.d detection. Extensions shipping configuration under usr/(local/)lib/tmpfiles.d/ or etc/tmpfiles.d/ get the merge hook automatically.


Reliability

Streaming hashing for multi-GB images

compute_file_hash previously read the entire file into memory before feeding it to SHA-256 / BLAKE3. Image artifacts can be tens of gigabytes; on a 16 GB Mac that pushed the process into swap thrash. Replaced with a fixed 1 MiB chunk streaming loop. The resulting digest is identical.


Bug fixes

  • avocado clean --unlock actually unlocks now. The previous code called lock_file.save() after clearing entries, which merges with on-disk state and re-adds them. The desktop app's Unlock button hit this and silently no-op'd. Switched to save_replacing; regression test included.
  • avocado clean --unlock now clears pinned kernel versions. --unlock cleared the SDK, rootfs, initramfs, target-sysroot, extension, and runtime locks, but left the pinned KERNEL_VERSION and per-kernel sysroot package tables intact. After unlocking and reinstalling, dnf re-resolved sysroot content from the latest feed while the CLI still demanded the old pinned kernel by exact name, failing with Unable to find a match: kernel-image-… once the rolling feed dropped that version. Unlock now also drops the kernel pins so the next install re-picks the latest.
  • Update banner no longer corrupts --output json. When a newer release is available, avocado writes a yellow [UPDATE] avocado X is available … banner to stderr. Callers that merge stdout and stderr before decoding (the desktop app's CLI runner) saw the banner break every JSON payload the moment a new version shipped on the CDN. The banner is now suppressed whenever --output json is present in the arguments; native CLI runs still get the human-readable nudge.
  • avocado vm update no longer fails the post-update boot check on preserved var images. verify_all ran a SHA-256 check against every artifact in the new release manifest, including SeedOnly entries like var. Once you mutate your preserved var image (the entire point of SeedOnly), its on-disk hash legitimately diverges from the release's seed hash, and every post-update boot failed with a sha256 mismatch for role 'var' error. verify_all now skips SeedOnly artifacts; replace artifacts (kernel, initramfs, rootfs) are still verified.
  • avocado connect init produces valid YAML on compact-style scaffolds. The connect-extension inserter hard-coded a 6-space indent when adding the three connect extensions to a runtime's extensions: sequence. Reference scaffolds that emit the sequence in YAML compact style (items at the same indent as the extensions: key) ended up with mixed-indent output that no parser would accept — avocado install -f failed immediately with did not find expected key on any freshly scaffolded, connect-init'd project. All four runtime-list scans now recognize - items at the parent indent as still inside the block.