Skip to content

release: 0.40.0 — vm download UX, lifecycle-on-CLI, config tree, deploy JSON#145

Merged
mobileoverlord merged 17 commits into
mainfrom
rel/0.40.0
May 21, 2026
Merged

release: 0.40.0 — vm download UX, lifecycle-on-CLI, config tree, deploy JSON#145
mobileoverlord merged 17 commits into
mainfrom
rel/0.40.0

Conversation

@mobileoverlord
Copy link
Copy Markdown
Contributor

@mobileoverlord mobileoverlord commented May 21, 2026

Summary

Bumps avocado-cli to 0.40.0. 17 commits since 0.39.0.

Three themes:

  1. avocado vm polish — streaming downloads with progress, smarter defaults, persistent cpu/memory/DNS config, and a vm config subcommand shared with Avocado.app. macOS lifecycle moved back into the CLI itself (Avocado.app adopts the pid via its existing reconciler) so vm start / vm stop no longer depend on the desktop being installed or responsive.
  2. More JSON-output surface for the desktop app — runtime deploy --output json and config show --detail give the desktop the structured project + deploy events it needs to render its UI without parsing prose.
  3. Cross-platform groundworkcargo check --target x86_64-pc-windows-msvc now runs in CI, with #[cfg(unix)] gating on the AF_UNIX-bound modules.

Plus a clean --unlock correctness fix (regression test included), runtime-extension map syntax everywhere it should have been, an AVOCADO_ON_MERGE=systemd-tmpfiles --create extension hook, and streaming hashing for multi-GB image signing.


avocado vm UX

  • Streaming downloads + progress (feb9b1e, 8cfc381) — vm update was timing out mid-download on the 458 MB var.btrfs artifact: ClientBuilder::timeout(30s) applied to the body download, and Response::bytes() buffered the entire body in RAM before disk writes started. Replaced with connect_timeout only + bytes_stream() writing each chunk straight to disk. Human mode now renders an indicatif MultiProgress queue matching avocado connect upload's style (one cyan/blue bar per artifact, finishes with (done)); JSON mode emits throttled download_{started,progress,completed} NDJSON events at ~10 Hz.
  • Smarter vm start defaults (170324c) — after avocado vm update populates ~/.avocado/vm/install/, vm start now finds it without needing --vm-source or AVOCADO_VM_DIR. Resolution order: flag → env var → install/ (managed) → artifact-dir (dev workflow) → helpful error pointing at vm update. End-users on the managed path no longer have to think about env vars.
  • Persistent cpu/memory overrides (d06cf42) — vm start --memory-mib / --cpus are now optional and resolve to runtime.{cpus,memory_mib} in ~/.avocado/vm/config.yaml (also written by Avocado.app's settings UI), with persistence: passing a flag writes it back so the next flag-less vm start and the desktop app converge. vm reset / vm update / route-on-demand callers pass None to pick up persisted settings instead of hardcoded 4096 MiB / 4 CPUs.
  • vm config get/set/unset/list + persistent network config (7d41154) — ~/.avocado/vm/config.yaml is the shared host/guest config file Avocado.app reads/writes. First consumer: network.dns (+ optional one-shot --dns on vm start) pushed into the guest via resolvectl once SSH is up. Resolves 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; IPC is best-effort notify (7afc946) — vm start / vm stop previously delegated to Avocado.app over a synchronous JSON-line client, which made the CLI unusable when the app wasn't installed and stalled vm stop whenever the app's main actor was busy. CLI now spawns and signals qemu directly on every platform; Avocado.app (when present) adopts the pid via its pidfile reconciler. Remaining IPC (vm.notify.{starting,running,stopping,stopped}) is pure dashboard hinting — 100 ms timeouts, silent no-op when unreachable, self-heals via the reconciler within ~2s. Also adds a second virtio-serial port (avocado.control → control.sock) so Avocado.app's USBHostBridge / ControlPlane can talk to avocado-vm-agent without spawning qemu itself.

Desktop integration (JSON output)

  • runtime deploy --output json (f96a00d) — skips TUI rendering and emits NDJSON task_registered / step / step_error events per phase (stamps, hash-collection, metadata-sign, deploy). Names mirror the human labels so the desktop can render them directly.
  • config show --detail (a9633a9) — adds a detail block alongside the existing narrow projection: per-runtime extension references (with defined/enabled flags + node_paths for cross-ref navigation), per-extension types/packages/services/used_by_runtimes, and an SDK image+packages summary. Default output is byte-stable so the desktop's project-list scan keeps working unchanged. Six unit tests cover map/list package shapes, broken cross-refs, used_by inversion, and empty configs.

Cross-platform

  • Windows-msvc compile check + unix-only module gating (7e43410) — new windows-check PR job runs cargo check --target x86_64-pc-windows-msvc. qga (AF_UNIX qemu-guest-agent client) and qmp (QEMU monitor over AF_UNIX) become #[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} replaced with local POSIX-value constants at the two call sites that referenced them (the send_signal function itself is already #[cfg(unix)]). No behavior change on macOS/Linux.

Runtime extension map form

  • extensions: - foo: {enabled: false} syntax (3a7c528) — adds utils::runtime_extension::RuntimeExtensionSpec::parse_entry as 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, so any future per-extension knob (merge_index, runs_on, …) lands in one spot. runtime build propagates enabled: false into the manifest's AVOCADO_EXT_DISABLED so avocadoctl skips activation at refresh time.
  • Two follow-up call sites in build.rs + stamps.rs (a97ba18) — collect_extension_dependencies (nested loop) and the runtime input-hash compute in stamps.rs were still treating each entry as a bare string and silently skipping map-form entries. Fixed.

Extension build

  • AVOCADO_ON_MERGE=systemd-tmpfiles --create for tmpfiles.d/ (7519489) — mirrors the existing sysusers.d / ld.so.conf.d detection. Extensions shipping config under usr/(local/)lib/tmpfiles.d/ or etc/tmpfiles.d/ get the merge hook automatically.

Reliability

  • Streaming hashing for multi-GB images (509faca) — compute_file_hash previously read the whole file into memory before feeding it to Sha256/Blake3. Image artifacts can be tens of GB; on a 16 GB Mac that pushed the process into swap thrash. Replaced with a fixed 1 MiB-chunk streaming loop. Digest is identical.
  • clean --unlock actually unlocks (0a141ed) — 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.

Maintenance

  • 2936f8dcargo fmt sweep on update.rs.
  • fca9e79 — docs link update.
  • 48680dd — version bump to 0.40.0.

Test plan

  • cargo fmt --all -- --check clean
  • cargo clippy --all-targets --all-features -- -D warnings clean
  • cargo build clean
  • cargo test — all 9 test binaries green (993 + 1000 + 126 + 49 + 20 + 7 + 6 + 6 + 5 lib/integration tests pass)
  • cargo audit — no advisories on the lockfile
  • CI: windows-check runs on this PR for the first time on a release branch — verify cargo check --target x86_64-pc-windows-msvc clean
  • Manual: avocado vm update end-to-end against the live channel renders the new progress bars, downloads stream to disk, atomic-swap completes
  • Manual: avocado vm start from a clean ~/.avocado/vm/install/ (post-update) finds artifacts without --vm-source / AVOCADO_VM_DIR
  • Manual: avocado vm config set network.dns 1.1.1.1, restart guest, resolvectl status inside the guest shows the configured resolver
  • Manual: avocado vm start --memory-mib 8192 persists; next avocado vm start (no flags) launches with 8192 MiB
  • Manual: avocado runtime deploy --output json -r <runtime> -d <device-ip> emits the four task_registered events then step/success transitions in order
  • Manual: avocado config show --detail --output json returns the detail block with runtime/extension cross-refs populated
  • Manual: avocado clean --unlock, verify on-disk lockfile no longer contains the cleared entry

Comment thread .github/workflows/pr.yml Dismissed
mobileoverlord and others added 13 commits May 21, 2026 14:51
`avocado vm update` was failing mid-download on real-world links:
  Error: error decoding response body
  Caused by: operation timed out

Two underlying causes:
1. `ClientBuilder::timeout(Duration::from_secs(30))` set a global
   request timeout that fires across the body download too — 30s
   is hopeless for a 458 MB artifact on any link slower than
   ~16 MB/s. Fix: switch to `connect_timeout` (still 30s — fail
   fast on a stalled handshake) but leave the overall timeout
   unset so large downloads can run to completion.
2. `Response::bytes()` buffered the entire body in memory before
   writing — 458 MB of heap per artifact, plus the wasted RTT
   before disk writes started. Fix: stream via
   `Response::bytes_stream()` and write each chunk straight to
   `std::fs::File`.

UX:
- Human mode now renders an indicatif progress bar per artifact:
    [1/4] downloading avocado-image-rootfs-qemuarm64.erofs-lz4 (...)
      ===>...    65.0 MB/65.0 MB     10.2 MB/s   ETA  0s
- JSON mode (--output json) emits structured NDJSON events
  throttled to ~10 Hz so a polling consumer (e.g. avocado-desktop's
  VMInstallController) can drive its own progress UI without
  being flooded:
    {"event":"download_started","file":"...","size":N,"index":i,"total":n}
    {"event":"download_progress","file":"...","bytes":N,"total":N}
    {"event":"download_completed","file":"...","bytes":N,"index":i,"total":n}

Verified locally end-to-end: clean cache + state, ran
`avocado vm update --yes` against the live v0.1.0 channel, all
four artifacts downloaded, sha256-verified, atomic-swapped into
~/.avocado/vm/install/. Sha hashing still happens after write
(unchanged from before — keeps the staging-dir verify path the
same).
User flow that hit this:
  $ avocado vm update      # downloads to ~/.avocado/vm/install/
  $ avocado vm start
  Error: --vm-source not given and AVOCADO_VM_DIR is unset…

After `avocado vm update` the artifacts ARE on disk in the canonical
location — `vm start` shouldn't refuse to find them. Resolution order
is now:

  1. --vm-source flag
  2. $AVOCADO_VM_DIR env var
  3. ~/.avocado/vm/install/  (managed install, populated by vm update)
  4. ~/.avocado/vm/artifact-dir  (last `vm start --vm-source <dev>`
                                  or `vm rebuild`, dev workflow)
  5. Helpful error pointing at `avocado vm update`

VmPaths::default_vm_source() centralises the layered fallback. The
existing `vm rebuild` flow still writes the artifact-dir pointer for
backwards compat with the dev workflow, but end-users who only ever
`avocado vm update` + `avocado vm start` no longer need to think
about env vars.

main.rs help text + start.rs error message both updated to reflect
the new behaviour.
Pre-allocate a ProgressBar per artifact via MultiProgress (same as
connect upload at src/commands/connect/upload.rs:563-585), and reuse
the exact template:
  "  {msg} [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec})"
with progress_chars "#>-". Bars finish_with_message("X (done)") so
the queue stays visible at 100% after each download — matches the
connect upload completion behaviour.

The earlier per-file style (green/black bar + ETA, drawn one at a
time after a "[1/4] downloading…" header) is replaced; users now see
all four artifacts queued from the start, filling sequentially. One
visual idiom across the CLI.

JSON mode unchanged — emits the same three download_* events
(started / progress / completed).
Adds `~/.avocado/vm/config.yaml` as a shared host/guest config file with
a typed `VmConfig` reader and a `vm config get/set/unset/list` CLI
surface. avocado-desktop reads/writes the same file so settings stay in
sync across the two clients.

First consumer is DNS: persisted `network.dns` (+ optional one-shot
`--dns` override on `vm start`) is pushed into the guest via
`resolvectl` once SSH comes up. Falls back to SLIRP's 10.0.2.3 when
nothing is configured. Resolves the macOS-VPN case where scoped
resolvers on the host are invisible to the slirp DNS proxy.
New `windows-check` PR job that runs `cargo check --target
x86_64-pc-windows-msvc`. To make that build pass without functional
regressions on unix targets:

- `qga` (AF_UNIX socket client) and `qmp` (QEMU monitor over AF_UNIX)
  are now `#[cfg(unix)]` on the module declarations; their call sites
  in `lifecycle::stop` and `boot_sync::wait_for_guest_ready` get the
  same gating with a non-unix fallback path that waits for SSH only.
- `libc::SIGTERM` / `libc::SIGKILL` are not exposed on Windows — replace
  the call-site uses in `lifecycle` and `forward` with local POSIX-value
  constants (`send_signal` itself is already `#[cfg(unix)]`).

No behavior change on macOS/Linux.
Adds `utils::runtime_extension::RuntimeExtensionSpec::parse_entry` as
the one place that knows how to read a runtime config's `extensions:`
list. The list previously accepted only string entries; now it also
accepts a single-key map whose value carries per-extension flags
(initial flag: `enabled`).

Every iterator that walks the list — `Config::extension_deps`,
`Config::find_active_extensions`, runtime/build, runtime/deps, build,
install — now funnels through the same parser, so any future
per-extension knob (`merge_index`, `runs_on`, …) lands in one spot
without silently dropping entries.

`runtime build` propagates the `enabled: false` flag into the runtime
manifest's `AVOCADO_EXT_DISABLED` env var so avocadoctl skips
activation at refresh time.
…present

Mirror the existing sysusers.d / ld.so.conf.d detection. When an
extension ships config under `usr/(local/)lib/tmpfiles.d/` or
`etc/tmpfiles.d/`, the generated release file gets
`AVOCADO_ON_MERGE="systemd-tmpfiles --create"` so the
extension's tmpfiles take effect after merge without a reboot.
`compute_file_hash` previously read the whole file into memory before
feeding it to the hasher. Image artifacts can be tens of GB
(`var.btrfs` alone is ~458 MB now and growing), so reading them fully
into RAM is a footgun waiting to happen — and on a 16 GB Mac it can
push the process into swap thrash before any sign of progress.

Replace with a fixed-size 1 MiB-chunk streaming loop for both Sha256
and Blake3. No behavior change for callers; the resulting digest is
identical.
Two remaining call sites in build.rs (collect_extension_dependencies'
nested loop and the per-extension dep-resolution loop) and the runtime
input-hash compute in stamps.rs were still treating each entry as a
bare string. After 3a7c528 added support for `extensions: - foo:
{enabled: false}` map syntax, those paths silently skipped any
map-form entry, so disabled extensions could still pull deps via the
nested traversal and the input hash didn't account for them.
Previously the macOS path delegated `vm start` / `vm stop` to
Avocado.app via a synchronous JSON-line client, which made the CLI
unusable on its own and stalled `vm stop` whenever the app's main
actor was busy. Now the CLI spawns and signals qemu directly on every
platform; Avocado.app (when installed) adopts the pid via its
existing pidfile reconciler.

The remaining IPC (`vm.notify.starting` / `.running` / `.stopping` /
`.stopped`) is pure dashboard hinting: 100 ms timeouts, silent no-op
when the desktop isn't reachable, and self-heals via the reconciler
within ~2 s if a notification is dropped. `Client::connect_or_launch`
and `delegate_start_to_app` are gone along with their wait loops.

Also adds a second virtio-serial port (`avocado.control` ->
control.sock) so Avocado.app's USBHostBridge / ControlPlane can talk
to avocado-vm-agent without waiting for the app to spawn qemu itself.
avocado config show --detail emits a `detail` block alongside the
existing narrow projection: per-runtime extension references (with
defined/enabled flags and node_paths for cross-ref navigation),
per-extension types/packages/services/used_by_runtimes, and an SDK
image+packages summary.

Default output is byte-stable so the desktop app's project-list scan
keeps working unchanged; the new payload only appears when --detail
is passed. Six unit tests cover map/list package shapes, broken
runtime->extension refs, used_by inversion, and empty configs.

The desktop app consumes this to render a config tree per project
without having to parse avocado.yaml itself.
The previous code called `lock_file.save()` after clearing entries,
which merges with the on-disk state and re-adds them. The desktop
app's Unlock button hit this and silently no-op'd. Add a regression
test.
`avocado vm start --memory-mib` / `--cpus` now make the flags optional,
resolve to `runtime.{cpus,memory_mib}` in the VM config (also written by
Avocado.app's settings UI), and fall back to DEFAULT_CPUS / DEFAULT_MEMORY_MIB.
When the user passes a flag, the value is written back to the config so the
next flag-less `vm start` (and the desktop app) converge on the same value.

`vm reset` / `vm update` / route-on-demand callers pass `None` so they pick
up the persisted settings instead of hardcoded 4096 MiB / 4 CPUs.
When `--output json` is set, deploy skips TUI rendering and emits
NDJSON `task_registered` / `step` / `step_error` events for each
phase (stamps, hash-collection, metadata-sign, deploy). Names mirror
the human-facing labels so the desktop can render them directly.
@mobileoverlord mobileoverlord merged commit 6d4c265 into main May 21, 2026
6 checks passed
@mobileoverlord mobileoverlord deleted the rel/0.40.0 branch May 21, 2026 19:57
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.

3 participants