Repository navigation
Expand file tree
/
Copy pathjustfile
More file actions
869 lines (782 loc) · 41.9 KB
/
Copy pathjustfile
File metadata and controls
869 lines (782 loc) · 41.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
# justfile — repo-wide task runner; the lo
8000
cal twin of the frozen CI workflows
# (docs/ci-strategy.md §8: CI YAML schedules, the logic lives here + scripts/).
# OS-specific recipes carry [linux]/[macos] attributes — `just --list` shows
# only this host's. `just --list` renders ONLY the last comment line above a
# recipe, so that line must be a standalone summary (warnings included);
# rationale, CI pointers and caveats go above it, separated by a bare `#`.
scratch := justfile_directory() / ".scratch"
arch := arch()
musl-target := arch + "-unknown-linux-musl"
# Flat libkrun link/runtime prefix for the DYNAMIC upstream package (Linux;
# macOS links the Homebrew install). No longer on the dev stack's path — kept
# for `just libkrun`, which still fetches it.
krun-prefix := env('HOME') / ".krun"
# Prefix for the STATIC libkrun the dev stack links (Linux). Built from the
# vendored pin, not fetched; `just clean` removes it.
krun-static := scratch / "libkrun-static" / musl-target
kernel := scratch / "vmlinuz"
rootfs := scratch / "rootfs.img"
initramfs := scratch / "initramfs.cpio"
gvproxy := scratch / "gvproxy"
# On Linux minvmd builds for the musl target (static libkrun — the linkage
# users receive), so it lands under target/<triple>/ rather than target/debug.
# macOS keeps the native debug dir.
minvmd-dir := if os() == "linux" { justfile_directory() / "target" / musl-target / "debug" } else { justfile_directory() / "target/debug" }
minvmd-bin := minvmd-dir / "minvmd"
minimald-bin := justfile_directory() / "target/debug/minimald"
# The CLI crate is `minimal`; its [[bin]] target is `min`.
min-bin := justfile_directory() / "target/debug/min"
# State dir for the host-native minimald that `just up` runs on Linux.
native-dir := scratch / "native-state"
# The workspace doesn't compile natively on macOS (minimald/lcache/mctx are
# Linux-only): scope to the darwin-capable crates there; `just test-cross`
# covers the rest. The Linux lanes run nextest's ci profile; macOS has none.
scope := if os() == "macos" { "-p minvmd -p boxes -p bep" } else { "--workspace" }
# The strict gate's scope on macOS: the same crates `clippy` and `test` already
# use, because the Linux-only crates do not build there. The script reports any
# changed crate this leaves out, so the scope is never silently narrower.
strict-scope := if os() == "macos" { "-p minvmd -p boxes -p bep" } else { "" }
# Crates carrying a `fuzz/` workspace. `rcache` is Linux-only: it pulls in
# `lcache`, which uses the Linux-only `common::renameat2`.
fuzz-crates := if os() == "macos" { "args common diagnostics graph mfile paths" } else { "args common diagnostics graph mfile paths rcache" }
ci-profile := if os() == "macos" { "" } else { "--profile ci" }
e2e-env := "E2E_VM=1 E2E_PROJECT_DIR=/tmp" + if os() == "linux" { " E2E_MINIMAL_ARGS='--provider local-minvmd'" } else { "" }
# Shared dev-stack env: target/debug on PATH so autospawn finds sibling
# binaries; MINVMD_* are inert outside the VM recipes. 150s timeouts: the
# generic guest kernel can spend 40-70s probing hardware before pid-1 starts,
# overrunning the 60s READY / 75s autospawn / 30s lifecycle defaults.
#
# minvmd-dir leads: on Linux it holds the musl build, and `min` autospawns
# `minvmd` by bare name. No LD_LIBRARY_PATH — minvmd links libkrun statically
# and dlopens nothing, so there is nothing for the loader to resolve.
export PATH := minvmd-dir + ":" + justfile_directory() / "target/debug:" + env('PATH')
export MINVMD_KERNEL_PATH := kernel
export MINVMD_ROOTFS_PATH := rootfs
export MINVMD_INITRAMFS := initramfs
export MINVMD_READY_TIMEOUT_SECS := env('MINVMD_READY_TIMEOUT_SECS', '150')
export MINIMAL_SPAWN_TIMEOUT_SECS := env('MINIMAL_SPAWN_TIMEOUT_SECS', '150')
export MINVMD_LIFECYCLE_BOOT_TIMEOUT_SECS := env('MINVMD_LIFECYCLE_BOOT_TIMEOUT_SECS', '150')
[private]
default:
@just --list
# Installs nothing; each failed check prints the command that fixes it. The
# list mirrors AGENTS.md §System dependencies.
#
# Check this host for the tools the recipes need, and print a fix for each gap.
preflight:
@scripts/preflight.sh
# `cross` runs the host's x86_64 Linux rustup toolchain inside its x86_64
# image, and rustup refuses that toolchain on an arm64 Mac without
# --force-non-host. Checking first turns rustup's install error into the fix.
[macos]
_preflight-cross:
@scripts/preflight.sh cross
# Linux needs no gate here: `just preflight` warns when cross is missing.
[linux]
_preflight-cross:
# ── build & fetch ────────────────────────────────────────────────────────────
#
# Prereqs — macOS: `brew install slp/krun/libkrun zstd jq`.
# Linux: a KVM host with kvm-group membership, Rust + protoc + jq + cpio + zstd.
# `just preflight` checks them. See crates/minvmd/README.md for the manual bring-up.
# Prebuilt from the public cache via the per-commit package index — nothing is
# built or materialized locally (scripts/fetch-prebuilt.sh; pkgs commit pinned
# in .minimal/minimal.toml).
#
# Fetch the prebuilt guest kernel + generic rootfs (`just clean` forces a refresh).
artifacts:
@mkdir -p {{scratch}}
@[ -f {{kernel}} ] || scripts/fetch-prebuilt.sh kernel {{kernel}} {{arch}}
@[ -f {{rootfs}} ] || scripts/fetch-prebuilt.sh rootfs {{rootfs}} {{arch}}
# Not on the dev stack's path any more — see `libkrun-static`. Kept because
# nightly-tests.yml still builds against the upstream package, and this is how
# you reproduce that locally.
#
# Fetch the prebuilt DYNAMIC libkrun + libkrunfw into the link/runtime prefix.
[linux]
libkrun:
scripts/fetch-prebuilt.sh krun {{krun-prefix}} {{arch}}
# The linkage Linux users actually receive, and what `minvmd-build` and
# `test-vm` link against. Built from the vendored pin rather than fetched.
# Skips when already built; `just clean` forces a rebuild.
#
# Build the STATIC libkrun (libkrun.a) from the vendored pin.
[linux]
libkrun-static:
@[ -f {{krun-static}}/libkrun.a ] || scripts/build-libkrun-linux.sh {{krun-static}} {{musl-target}}
# Fetch the pinned gvproxy switch (guest egress + own-IP; missing = switchless boot).
gvproxy:
@mkdir -p {{scratch}}
@scripts/fetch-gvproxy.sh {{gvproxy}}
# Run one Go test on the patched gvproxy source (`just gvproxy-test TestUnexposeClosesOnlyItsOwnAcceptedConnections`).
gvproxy-test name:
scripts/build-gvproxy.sh --test {{quote(name)}}
# Cross-compile minimald → initramfs /init.
initramfs:
scripts/build-initramfs.sh {{initramfs}} {{musl-target}}
# Compiles the guest minimald here, then hands the binary to
# scripts/build-initramfs.sh via MINIMALD_BIN (its prebuilt mode), so the script
# packs the cpio without compiling anything itself. That sidesteps the script's
# `cross` fallback, which is what `just initramfs` lands on when it can't prove a
# usable host toolchain — on macOS it can never prove one, because it compares a
# raw `uname -m` ("arm64") against the target triple's arch ("aarch64").
#
# REQUIREMENTS — all must already be on PATH; use `just initramfs` (Docker +
# `cross`) instead when they are not:
# - a Rust toolchain with the `{{musl-target}}` target installed
# - cargo-zigbuild and zig, which supply the musl linker for the cross link
#
# Build minimald → initramfs /init using the cross toolchain on PATH (no container).
initramfs-nodocker:
cargo zigbuild -p minimald --profile initramfs --target {{musl-target}}
MINIMALD_BIN="{{justfile_directory()}}/target/{{musl-target}}/initramfs/minimald" \
scripts/build-initramfs.sh {{initramfs}} {{musl-target}}
# The ad-hoc (-s -) hypervisor-entitlement codesign must be the LAST touch:
# any later cargo call that relinks minvmd drops it (EINVAL from krun_start_enter).
#
# bep, the Box Egress Proxy, is built in its own cargo call beside minvmd:
# minvmd starts it from its own directory (MINVMD_BEP_BIN overrides). It is
# signed because the macOS Keychain admits its keys only to the signed
# program that made them. An ad-hoc signature changes with each rebuild, so
# set BEP_CODESIGN_IDENTITY to a stable identity to keep the keys usable.
#
# Build minvmd and bep (debug); codesign is the last touch.
[macos]
minvmd-build:
cargo build -p bep --bin bep --locked
codesign --force -s "${BEP_CODESIGN_IDENTITY:--}" {{minvmd-dir}}/bep
cargo build -p minvmd --bin minvmd --locked
codesign --entitlements crates/minvmd/minvmd.entitlements --force -s - {{minvmd-bin}}
# The same target and linkage the release ships, so the dev stack exercises
# what users run. MINVMD_REQUIRE_LIBKRUN makes a missing libkrun.a an error
# rather than a silent runtime-bailing stub.
#
# Build minvmd (debug, static musl against the vendored libkrun.a).
[linux]
minvmd-build: libkrun-static
MINVMD_REQUIRE_LIBKRUN=static LIBKRUN_PREFIX="{{krun-static}}" \
cargo build -p minvmd --bin minvmd --locked --target {{musl-target}}
# Its own cargo invocation, never beside `-p minvmd`: cargo unifies features
# across the packages one build selects, and minvmd's default `libkrun`
# feature would stamp libkrun's load command into this root-run program.
#
# Build minzoned, the box-name service program, without libkrun.
zoned-build:
cargo build -p minzoned --locked
# The root-installed answerer copy must resolve no library from a
# user-writable dir (a dylib reached through an rpath there is a root
# escalation): otool -L on macOS, ldd on Linux, system paths only.
#
# Check that minzoned links only system libraries.
zoned-link-check: zoned-build
scripts/check-zoned-links.sh target/debug/minzoned
# The .icns is committed (packaging/macos/AppIcon.icns); this re-renders it
# from packaging/macos/app-icon.svg after a change to the source. Needs
# rsvg-convert (`brew install librsvg`) and iconutil.
#
# Render the macOS app icon from its SVG source.
[macos]
app-icon:
scripts/build-app-icon.sh
# The bundle is unsigned: only the release workflow signs it. It lands under
# target/app-bundle, not beside `min`, so a debug `min` keeps installing the
# bare program until the bundle is copied beside it on purpose
# (`cp -R target/app-bundle/Minimal.app target/debug/`).
#
# Assemble the unsigned macOS app bundle minzoned ships in.
[macos]
app-bundle: zoned-build
scripts/build-app-bundle.sh assemble target/debug/minzoned target/app-bundle/Minimal.app
# Build the `min` CLI.
minimal-cli:
cargo build -p minimal --locked
# Build a host-native (glibc) minimald (for `just up`).
[linux]
minimald-build:
cargo build -p minimald --locked
# THE single build entrypoint for a shippable build: the four release binaries
# for a target triple, built exactly the way release.yml's build jobs build
# them (they call this same script), plus completions from the built `min`.
# Per-package invocations (fat-LTO links serialize), FEATURES for the guest
# minimald, and the fail-loud static-libkrun requirement for minvmd live in
# scripts/dist-build.sh — read its header for the LIBKRUN_PREFIX /
# COMPLETIONS_DIR / FEATURES env knobs.
#
# Linux-only: the shippable Linux build is musl-static, and running it from a
# macOS host would skip minvmd's mandatory codesign and build minimald against
# glibc — the [linux] attribute keeps the mistake unoffered (`just test-cross`
# covers the Linux-only crates from macOS).
[linux]
dist-build target:
scripts/dist-build.sh {{quote(target)}}
# Restage an already-shipped release under its semver so `install.sh --version`
# can find it (the one-time fix for releases staged before the semver-row
# convention, e.g. `just backfill-version-row abc12345 0.5.3`).
[linux]
backfill-version-row sha version:
scripts/backfill-version-row.sh --sha {{quote(sha)}} --version {{quote(version)}}
# ── run & inspect ────────────────────────────────────────────────────────────
# Run the dev-built `min` CLI, args forwarded (e.g. `just min loadout list`).
min *args:
@cargo build {{ if os() == "macos" { "-p minimal" } else { "-p minimal -p minimald" } }} --locked
@"{{min-bin}}" {{args}}
# Print `export` lines for running the stack by hand: eval "$(just env)".
env:
@printf 'export %s="%s"\n' \
PATH "$PATH" \
MINVMD_KERNEL_PATH "$MINVMD_KERNEL_PATH" MINVMD_ROOTFS_PATH "$MINVMD_ROOTFS_PATH" \
MINVMD_INITRAMFS "$MINVMD_INITRAMFS" \
MINVMD_GVPROXY_BIN "{{gvproxy}}" \
MINVMD_READY_TIMEOUT_SECS "$MINVMD_READY_TIMEOUT_SECS" \
MINIMAL_SPAWN_TIMEOUT_SECS "$MINIMAL_SPAWN_TIMEOUT_SECS"
# Subshell with the dev env loaded (exit to leave).
shell:
@echo "minimal dev shell: target/debug on PATH, MINVMD_* set (exit to leave)"
@MINVMD_GVPROXY_BIN="{{gvproxy}}" "${SHELL:-sh}"
# Report the supervised minvmd lifecycle state.
status:
@"{{minvmd-bin}}" status
# Stop the supervised minvmd (SIGTERM → SIGKILL).
stop:
@"{{minvmd-bin}}" stop
# The built libkrun.a goes too: it is keyed to the vendored pin, so a stale one
# would silently outlive a pin bump.
#
# Remove the bring-up artifacts this justfile manages (never all of .scratch).
clean:
rm -f {{kernel}} {{rootfs}} {{initramfs}} {{gvproxy}}
rm -rf {{scratch}}/libkrun-static
# Kill THIS checkout's stranded VM processes (they wedge the next VM's vsock bridge).
reap:
scripts/reap-vms.sh
# Run before landing a guest-kernel bump (the `virtio-linux` version in pkgs, or
# the `locked_commit` that pulls it in): stable-tree commits touching vsock,
# virtio and the guest console — the surface the guest control plane rides on.
# `just kernel-review --pkgs ~/code/pkgs` reads both versions from a checkout.
# Review a guest-kernel bump (`just kernel-review 6.12.43 6.12.94`). Needs `gh`.
kernel-review *args:
scripts/kernel-bump-review.sh {{args}}
# Not a CI gate: the report queries GitHub, kernel.org and Alpine. Lag never
# fails; the checks do: duplicated pins that disagree, a pin that no longer
# resolves to exactly one value, and a vendor lock file missing from the
# inventory (`--check`, or `--check --offline` for the local-only subset,
# which `just test-shell` runs against this tree). The pin inventory is the
# PINS table in scripts/deps-freshness.sh. Needs `gh` and `jq`.
# Report non-Rust dependency pins against upstream (`just deps-freshness [--check]`).
deps-freshness *args:
scripts/deps-freshness.sh {{args}}
# ── CI-parity gates ──────────────────────────────────────────────────────────
# Fail fast with an install hint when a required tool is missing.
_need tool hint:
@command -v {{tool}} >/dev/null 2>&1 || { echo "'{{tool}}' not found — install with: {{hint}}" >&2; exit 1; }
_nextest: (_need "cargo-nextest" "cargo install cargo-nextest --locked")
# Apply rustfmt across the workspace (the fixer for a red `just fmt-check`).
fmt:
cargo fmt --all
# Clippy's `--fix` can shuffle whitespace during its rewrites, so the trailing
# `cargo fmt` normalizes whatever landed. `--allow-dirty` skips the
# clean-worktree check — the usual case here is running mid-edit with
# staged/unstaged work.
#
# The strict clippy gate is not part of this loop. It ran here once, and
# three agent rounds on the NET walk then rewrote lines their task never
# named to clear it, two in conflict rounds whose only directive was the
# merge (#1737, #1730, #1727): everything else this recipe prints is
# something to fix, so a strict hit printed beside it reads the same way,
# whatever a doc says. There is no autofix for that set either — `clippy
# --fix` cannot be scoped to changed lines and would sweep legacy sites — so
# the gate lives in `just clippy-strict` and `just ci`, which is the round
# that owns the judgement.
#
# Autofix pass: fmt, clippy --fix, fmt again (runnable mid-edit).
fix:
cargo fmt --all
cargo clippy {{scope}} --all-targets --fix --allow-dirty -- -D warnings
cargo fmt --all
# CI: ci.yml `fmt`.
#
# Check rustfmt across the workspace without writing (`just fmt` is the fixer).
fmt-check:
cargo fmt --all -- --check
# CI: ci.yml `clippy` / the ci-macos.yml `unit` scope.
#
# Clippy over all targets at this host's scope, warnings denied.
clippy:
cargo clippy {{scope}} --all-targets --locked -- -D warnings
# CI: none — this is a local gate (scripts/clippy-strict.sh).
#
# The lints below are the ones the tree is not yet clean for: CI's clippy job
# runs `-D warnings`, so they live here rather than in Cargo.toml and only the
# lines you changed are reported. Run it once your change is ready, fix what it
# reports on the lines your change introduced, and promote each lint into
# [workspace.lints.clippy] as its count reaches zero.
#
# This recipe and `just ci` are the only callers; `just fix` does not run it. A
# hit on a line the round's directive did not name is a note, not a licence to
# edit it.
#
# Strict Clippy on the lines this branch changed, scoped to the crates you touched.
clippy-strict BASE="":
scripts/clippy-s
8BD0
trict.sh "{{BASE}}" {{strict-scope}}
# A local advisories failure may just mean newer RUSTSEC data than CI's last run.
# CI: ci.yml `cargo-deny` (advisories/bans/licenses/sources).
#
# cargo-deny's all-features check: advisories, bans, licenses, sources.
deny: (_need "cargo-deny" "cargo install cargo-deny --locked")
cargo deny --all-features check
# The declared MSRV floor (`package.rust-version` in the root Cargo.toml, inherited
# by every crate) still type-checks. cargo-hack drives the check against the
# declared version; the toolchain the check runs under is fetched by rustup.
# CI: nightly-tests.yml `msrv` (blocking).
#
# The declared MSRV floor still type-checks (cargo-hack).
msrv: (_need "cargo-hack" "cargo install cargo-hack --locked")
cargo hack check --rust-version --workspace --all-targets --locked
# miri on `switch` — the vsock/subnet/MAC primitives (docs/ci-strategy.md §6
# "vsock framing"). Zero deps + pure integer/IP tests, so it compiles and
# interprets under miri in seconds. Needs the nightly toolchain + miri component:
# rustup toolchain install nightly && rustup +nightly component add miri
# CI: nightly-tests.yml `miri` (non-blocking). Widen the set as more crates prove clean.
#
# miri over `switch`'s vsock/subnet/MAC primitives (needs nightly + the miri component).
miri:
cargo +nightly miri test -p switch
# Kani bounded-verification harnesses over the pure security cores
# (rcache index_file parse, boxes PathDecision lattice). See
# scripts/kani.sh for install + the 0.68.0 pin rationale. #1109.
kani: (_need "cargo-kani" "cargo install --locked kani-verifier --version 0.68.0 && cargo kani setup")
./scripts/kani.sh
# Each `fuzz/` dir is its OWN workspace (so the nightly/sanitizer build can't
# perturb the main one), which means no workspace-wide build ever compiles
# them — they bitrot silently as the crates they target evolve. Plain `cargo
# check` on stable is enough to catch it: no nightly, no sanitizer.
#
# Type-check every cargo-fuzz target (bitrot guard). See docs/fuzzing.md.
fuzz-check:
#!/usr/bin/env sh
set -eu
for c in {{fuzz-crates}}; do
echo "== crates/$c/fuzz =="
# No --locked: fuzz Cargo.lock files are gitignored (see fuzz/.gitignore).
cargo check --manifest-path "crates/$c/fuzz/Cargo.toml" --all-targets
done
# Needs nightly (libFuzzer + sanitizers). The RSS cap is load-bearing: these
# decoders can allocate from an untrusted length field, and the cap turns an
# unbounded-allocation bug into a catchable crash rather than an ambient OOM.
# Seed the corpus first for the structured targets (docs/fuzzing.md).
#
# Run one fuzz target: `just fuzz graph graph_from_bytes -max_total_time=60`.
fuzz crate target *args: (_need "cargo-fuzz" "cargo install cargo-fuzz --locked")
#!/usr/bin/env sh
set -eu
cd crates/{{crate}}
# libFuzzer never loads a dictionary on its own — it has to be passed. Any
# target with a `fuzz/<target>.dict` gets it automatically; a dict only
# biases mutation, so a target without one is not an error.
dict=""
[ -f "fuzz/{{target}}.dict" ] && dict="-dict=fuzz/{{target}}.dict"
cargo +nightly fuzz run {{target}} -- -rss_limit_mb=2048 $dict {{args}}
# Extra arguments go to nextest positionally, so a filterset like
# `-E 'not binary(/_integration$/)'` stays one word (the macOS lane's unit step).
#
# Unit + in-process integration tests. CI: every lane's core-tests suite.
[positional-arguments]
test *args: _nextest
cargo nextest run {{scope}} {{ci-profile}} --locked --no-tests=fail "$@"
# The tight loop while writing one test. Output is shown, so a test that prints
# a measurement is readable without remembering `--no-capture`, and
# `--no-tests=fail` makes a filter that matches nothing say so rather than pass
# silently. The filter is nextest's own substring match and every argument is
# passed positionally, so no character in it needs escaping and a quoted
# argument stays one word.
#
# Run the tests whose name contains a substring (`just test-one own_ip [-p minimald]`).
[positional-arguments]
test-one filter *args: _nextest
cargo nextest run {{ if args == "" { scope } else { "" } }} \
--locked --no-tests=fail --no-capture "$@"
# Doctests — nextest can't run them, so they are their own surface.
doctest:
cargo test {{scope}} --doc --locked
# CI: the macOS unit job stages this archive for the mini's hypervisor tier, so
# the archived package set is whatever `scope` names, never a second list.
#
# Archive this host's test binaries for replay elsewhere (`just test-archive out.tar.zst`).
[positional-arguments]
test-archive file: _nextest
cargo nextest archive {{scope}} --locked --archive-file "$1"
# Regenerate the `dash` insta snapshots after an intentional render change;
# `just test` fails until these are updated. Review the diff, then `just test` to verify.
test-snap-update:
INSTA_UPDATE=always cargo test -p minimal --test dash_snapshots --locked
# The old `cargo test -- --include-ignored` pre-PR surface; the env-gated
# VM/netns harnesses self-skip here (`just test-vm` runs those for real).
#
# The #[ignore] tests GitHub runners can't run — NO CI lane covers these.
[linux]
test-ignored: _nextest
cargo nextest run --workspace --locked --profile ci --run-ignored ignored-only --no-tests=fail
# COST: the first run compiles the workspace under emulation and can take an
# hour+; later runs are incremental. HOME=/tmp: the container has no HOME.
#
# clippy + tests for the Linux-only crates from a Mac (musl in Docker; not minvmd).
[macos]
test-cross: _preflight-cross
DOCKER_DEFAULT_PLATFORM="${DOCKER_DEFAULT_PLATFORM:-linux/amd64}" cross clippy --workspace --exclude minvmd --all-targets --target {{musl-target}} --locked -- -D warnings
DOCKER_DEFAULT_PLATFORM="${DOCKER_DEFAULT_PLATFORM:-linux/amd64}" CROSS_CONTAINER_OPTS="--env HOME=/tmp" cross test --workspace --exclude minvmd --target {{musl-target}} --locked
# commitlint is not replicated by `just ci`; `just hooks` installs its local
# twin as the commit-msg hook (scripts/git-hooks/commit-msg) for this clone,
# every worktree included, so an over-long line is refused before the commit
# exists instead of after the push.
#
# Point this clone's git hooks at scripts/git-hooks (commit-msg = commitlint's local twin).
hooks:
#!/usr/bin/env sh
set -eu
# Repointing core.hooksPath silently disables every hook in the old
old=$(git config --get core.hooksPath || git rev-parse --git-path hooks)
if [ "$old" != scripts/git-hooks ] && [ -d "$old" ]; then
live=$(find "$old" -maxdepth 1 -type f ! -name '*.sample' | sort)
if [ -n "$live" ]; then
echo "hooks: $old holds hooks that core.hooksPath = scripts/git-hooks would stop running:" >&2
echo "$live" | sed 's/^/ /' >&2
echo "hooks: remove them, or chain scripts/git-hooks/commit-msg from $old/commit-msg instead" >&2
exit 1
fi
fi
git config core.hooksPath scripts/git-hooks
echo "hooks: core.hooksPath = scripts/git-hooks (commit-msg checks the message before each commit)"
# Not replicated: commitlint, the dogfood jobs, the installer lane (`just test-installer`).
#
# The local PR gate set, cheapest first.
[linux]
ci: fmt-check check-version clippy clippy-strict deny test doctest test-ignored zoned-link-check
@echo "ci: local PR gates green"
# The local PR gate set, cheapest first (`just test-cross` covers the Linux-only crates).
[macos]
ci: fmt-check check-version clippy clippy-strict deny test doctest zoned-link-check
@echo "ci: local PR gates green"
# Run the curl|sh installer's tests under every POSIX sh. CI: ci-shell-installer.yml.
# `case` runs one named scenario group (see the dispatch at the bottom of
# scripts/install_test.sh); empty runs them all.
test-installer case="":
#!/usr/bin/env bash
set -euo pipefail
if command -v shellcheck >/dev/null 2>&1; then
echo "== shellcheck --shell=sh =="
shellcheck --shell=sh scripts/install.sh scripts/install_test.sh
else
echo "== shellcheck not found, skipping static check =="
fi
for sh in sh dash; do
command -v "$sh" >/dev/null 2>&1 || { echo "== $sh not found, skipping =="; continue; }
echo "== running install_test.sh under $sh${case:+ (case: $case)} =="
SH="$sh" "$sh" scripts/install_test.sh {{case}}
done
# The reviewed harness the frozen ci-shell-installer.yml can't widen to; CI runs
# the same check through crates/common/tests/shell_lint.rs (part of `just test`).
#
# Shellcheck EVERY script under scripts/ (not just the installer's two files).
lint-shell:
bash scripts/lint-shell.sh
# Names and descriptions come from each member's Cargo.toml, so the list cannot
# drift from the workspace. Fails if any member lacks a description, which keeps
# AGENTS.md §Workspace crates honest. Stdlib-only python3 is already a
# prerequisite (AGENTS.md §System dependencies), so this adds no tool to any host.
#
# List workspace crates with their descriptions.
crates:
#!/usr/bin/env python3
import json, subprocess, sys
meta = subprocess.run(
["cargo", "metadata", "--no-deps", "--offline", "--format-version", "1"],
capture_output=True, text=True, check=True)
packages = json.loads(meta.stdout)["packages"]
for pkg in sorted(packages, key=lambda p: p["name"]):
print(pkg["name"] + "\t" + (pkg.get("description") or "-"))
missing = sorted(p["name"] for p in packages if not p.get("description"))
if missing:
sys.exit("missing description: " + ", ".join(missing))
# Not a CI gate. Default lints the markdown this branch touches (changed
# against main, staged, unstaged, and untracked) so the common loop stays
# short; pass files for specific ones, or --all for the whole tree. The
# existing tree carries thousands of alerts: drive files you touch to zero,
# leave untouched files' alerts alone. `vale sync` fetches the pinned packages
# into styles/ (gitignored) and runs again whenever `.vale.ini` changes, so a
# bumped pin moves the lint; --no-global keeps a personal ~/.vale.ini from
# leaking its styles into the repo's run.
#
# Vale prose-lint this branch's markdown (`just lint-prose [files|--all]`).
lint-prose *args: (_need "vale" "brew install vale (or a release binary: github.com/errata-ai/vale/releases)")
#!/usr/bin/env sh
set -eu
args={{quote(args)}}
# Re-sync when a package directory is missing, or when `.vale.ini` changed
# since the last sync. Vale keeps no version stamp of its own next to the
# synced styles, so an existence check alone would freeze a warm checkout
# at its first sync and ignore a later pin bump.
stamp="$(cksum .vale.ini)"
if [ ! -d styles/ai-tells ] || [ ! -d styles/ste ] \
|| [ "$(cat styles/.sync-stamp 2>/dev/null || true)" != "$stamp" ]; then
vale sync
printf '%s\n' "$stamp" > styles/.sync-stamp
fi
# `quote()` joins the arguments into a single shell word, so the splits
# below are what separate them again; `set -f` keeps a path's glob
# characters literal. Repo paths carry no whitespace, so an argument
# holding a space is not supported.
set -f
if [ "$args" = "--all" ]; then
files="$(git ls-files '*.md')"
elif [ -n "$args" ]; then
files="$args"
else
base=main
git rev-parse -q --verify "$base" >/dev/null || base=origin/main
git rev-parse -q --verify "$base" >/dev/null || {
echo "lint-prose: no 'main' or 'origin/main' ref to diff against" >&2
exit 1
}
files="$(
{ git diff --name-only "$base...HEAD" -- '*.md'
git diff --name-only --cached -- '*.md'
git diff --name-only -- '*.md'
git ls-files --others --exclude-standard -- '*.md'
} | sort -u
)"
fi
keep=""
for f in $files; do
if [ -f "$f" ]; then keep="$keep $f"; fi
done
[ -n "$keep" ] || { echo "lint-prose: no markdown to lint" >&2; exit 0; }
# `--` stops vale from parsing a leading-dash path as an option.
exec vale --no-global -- $keep
# scripts/record-smoked.sh writes the smoke-provenance marker and
# scripts/verify-smoked.sh reads it back, over a stubbed `gcloud` backed by a
# directory: no network, no auth. Shellcheck runs too, when present.
#
# Run the promotion gate's test harness (stubbed `gcloud`, no network or auth).
test-promote-gate:
#!/usr/bin/env bash
set -euo pipefail
if command -v shellcheck >/dev/null 2>&1; then
echo "== shellcheck =="
shellcheck scripts/record-smoked.sh scripts/verify-smoked.sh scripts/smoked_test.sh
else
echo "== shellcheck not found, skipping static check =="
fi
echo "== running smoked_test.sh =="
bash scripts/smoked_test.sh
# package.version must be the declared NEXT release: strictly above the newest
# v* tag and at least what the Conventional Commits since the last release
# require (feat -> minor, else patch; no major while 0.x). The workspace
# `package_version` test (crates/common/tests/package_version.rs) runs the same
# check but self-skips on the hosted lanes' shallow, tagless checkouts, so this
# recipe (part of `just ci`) is the proof that actually runs. Needs the tags.
#
# Assert Cargo.toml package.version against the commits since the last v* tag (scripts/next-version.sh --check).
check-version:
scripts/next-version.sh --check
# Run every scripts/*_test.sh harness in one go (the release and installer
# logic). No network, no credentials, no gcloud — the harnesses stub or
# fixture everything. CI runs the same set through
# crates/common/tests/shell_harnesses.rs (part of `just test`), which
# discovers them by convention so a new harness is never forgotten.
test-shell:
#!/usr/bin/env bash
set -euo pipefail
if command -v shellcheck >/dev/null 2>&1; then
echo "== shellcheck =="
shellcheck scripts/*_test.sh
else
echo "== shellcheck not found, skipping static check =="
fi
failed=0
for t in scripts/*_test.sh; do
echo "== running $t =="
bash "$t" || failed=1
done
exit "$failed"
# ── VM & e2e surfaces ────────────────────────────────────────────────────────
# VM recipes need writable /dev/kvm on Linux; no-op on macOS (HVF).
[macos]
_kvm:
[linux]
_kvm:
@[ -e /dev/kvm ] && [ -w /dev/kvm ] || { echo "needs writable /dev/kvm (kvm group membership); one-off: sg kvm -c 'just <recipe>'" >&2; exit 1; }
# minvmd's VM harnesses (tests/*_integration.rs). CI: `test-kvm` / macOS `e2e`.
[macos]
test-vm: _nextest artifacts initramfs gvproxy
#!/usr/bin/env sh
set -eu
export MINVMD_E2E=1 MINVMD_BIN="{{minvmd-bin}}" XDG_STATE_HOME="{{scratch}}/test-state" MINVMD_GVPROXY_BIN="{{gvproxy}}"
# CI's archive pattern: build EVERYTHING, codesign minvmd LAST (a later
# cargo call would relink it → entitlement lost), run from the archive.
cargo nextest archive -p minvmd --locked --archive-file "{{scratch}}/nextest-archive.tar.zst"
cargo build -p minvmd --bin minvmd --locked
cargo build -p minimal --bin min --locked
# Resolve the FFI-smoke binary from cargo itself (an mtime-sorted ls can
# pick a stale build config), BEFORE codesign — the last cargo call allowed.
testbin="$(cargo test -p minvmd --test krun_smoke_integration --no-run --locked --message-format=json 2>/dev/null \
| sed -n 's/.*"executable":"\([^"]*krun_smoke_integration[^"]*\)".*/\1/p' | head -1)"
[ -n "$testbin" ] && [ -x "$testbin" ] || { echo "krun_smoke_integration test binary not resolved via cargo" >&2; exit 1; }
codesign --entitlements crates/minvmd/minvmd.entitlements --force -s - "{{minvmd-bin}}"
# The FFI smoke boots libkrun IN-PROCESS from the unsigned test binary:
# run it kernel-less and directly — never via cargo, never from the filterset.
env -u MINVMD_KERNEL_PATH -u MINVMD_ROOTFS_PATH -u MINVMD_INITRAMFS \
"$testbin" --include-ignored --nocapture
cargo-nextest nextest run --archive-file "{{scratch}}/nextest-archive.tar.zst" \
--workspace-remap "{{justfile_directory()}}" --profile vm \
--run-ignored all --no-tests=fail \
-E 'binary(/_integration$/) and not binary(/_root_integration$/) and not binary(krun_smoke_integration)'
# minvmd's VM harnesses (tests/*_integration.rs). CI: ci-linux-kvm.yml `test-kvm`.
[linux]
test-vm: _nextest _kvm artifacts initramfs minvmd-build gvproxy
MINVMD_E2E=1 MINVMD_BIN="{{minvmd-bin}}" XDG_STATE_HOME="{{scratch}}/test-state" \
MINVMD_REQUIRE_LIBKRUN=static LIBKRUN_PREFIX="{{krun-static}}" \
MINVMD_GVPROXY_BIN="{{gvproxy}}" \
cargo nextest run -p minvmd --profile vm --target {{musl-target}} \
--run-ignored all --no-tests=fail \
-E 'binary(/_integration$/) and not binary(/_root_integration$/)'
# CI: ci-linux-native.yml `minimald-root-integration`. The AppArmor policy
# can't unlock this surface — its namespaces come from hashed-path test
# binaries and /usr/bin/unshare, which a per-binary profile can't cover.
#
# minimald's netns/tap proofs (the tests sudo their own netns commands).
[linux]
test-root-integration: _nextest gvproxy
@[ "$(sysctl -n kernel.apparmor_restrict_unprivileged_userns 2>/dev/null || echo 0)" = "0" ] || { echo "this host restricts unprivileged user namespaces, which this surface needs from hashed-path test binaries (the AppArmor policy can't cover those); leave this lane to CI, or see docs/reference/linux-host-setup.md before relaxing the restriction host-wide" >&2; exit 1; }
MINIMALD_NETNS_TEST=1 GVPROXY_BIN="{{gvproxy}}" cargo nextest run -p minimald --profile ci --run-ignored all --no-tests=fail -E 'binary(/_root_integration$/)'
# minvmd-build is LAST so its macOS codesign is the final touch on the binary.
#
# The unified session e2e against the VM-backed daemon. CI: the session e2e steps.
e2e: _kvm artifacts gvproxy initramfs minimal-cli zoned-build minvmd-build
{{e2e-env}} MINVMD_GVPROXY_BIN="{{gvproxy}}" ./scripts/session-e2e.sh
# CI: ci-linux-native.yml `native-daemon-e2e`.
#
# The SAME session e2e against a host-native minimald (no VM).
[linux]
e2e-native:
cargo build -p minimald --bin minimald -p minimal --bin min --locked
@just _userns-check
env -u E2E_VM ./scripts/session-e2e.sh
# Boots switchless like CI's step — MINVMD_GVPROXY_BIN deliberately not set.
#
# Daemon lifecycle proof (run --detach → Running → stop → Stopped). CI: `lifecycle`.
test-lifecycle: (_need "jq" "brew install jq (or apt install jq)") _kvm artifacts initramfs minvmd-build
XDG_STATE_HOME="{{scratch}}/test-state" ./scripts/minvmd-lifecycle.sh
# nightly-tests.yml runs 10 reps, then `just bulk-upload`. The reap between
# iterations kills THIS checkout's live dev stack — not just the soak's own VMs.
#
# Nightly soak parity: N session-e2e reps; reaps between them — WILL kill your dev stack.
soak n="10": _kvm artifacts gvproxy initramfs minimal-cli minvmd-build
{{e2e-env}} MINVMD_GVPROXY_BIN="{{gvproxy}}" ./scripts/soak-session-e2e.sh {{n}} "{{scratch}}/soak-logs"
# Each pass uploads a 49 MiB project (~13 MB on the wire) and destroys its session.
#
# Bulk host→guest upload proof (#869): N `min session activate`s of a large, compressible project.
bulk-upload n="5": _kvm artifacts gvproxy initramfs minimal-cli minvmd-build
{{e2e-env}} MINVMD_GVPROXY_BIN="{{gvproxy}}" ./scripts/bulk-upload-e2e.sh {{n}}
# Mints N sessions CONCURRENTLY against one daemon (vs. the soak's N-serial reps),
# then bulk-tears-down — the supervision-under-load surface of ci-strategy §6.
#
# Concurrency stress proof. CI: a non-fatal step in nightly-tests.yml `session-e2e-soak`.
stress n="5": _kvm artifacts gvproxy initramfs minimal-cli minvmd-build
{{e2e-env}} MINVMD_GVPROXY_BIN="{{gvproxy}}" ./scripts/stress-session-e2e.sh {{n}}
# ── stack bring-up ───────────────────────────────────────────────────────────
#
# `just up` = this host's default run mode; `just down` stops it.
# macOS: Linux VM over Hypervisor.framework (the only macOS mode).
# Linux: host-native minimald, no VM. `just up-kvm`: Linux + VM over KVM.
# Each backend has its own provider dir: the native daemon dials
# providers/local-minimald0/ssh.sock, the minvmd VM providers/local-minvmd0/ssh.sock
# (minvmd binds it directly, #690, so no bridge is needed). Select the VM
# backend with `--provider local-minvmd`.
# The daemon can reset the very first connect after boot/bind; retry briefly.
_smoke *args:
#!/usr/bin/env sh
for _ in 1 2 3 4 5; do
MINVMD_GVPROXY_BIN="{{gvproxy}}" "{{min-bin}}" {{args}} ls && exit 0
sleep 2
done
echo "min ls failed after retries" >&2; exit 1
# Bring the stack up: Linux VM over Hypervisor.framework (`min ls` autospawns minvmd).
[macos]
up: artifacts gvproxy initramfs minvmd-build minimal-cli && (_smoke)
# `just up` with the initramfs built from the cross toolchain on PATH rather than
# through `cross` — see `initramfs-nodocker` for what that requires. The iteration
# loop for a guest-minimald change: this rebuilds and repacks /init every run.
# minvmd-build stays LAST so its codesign is the final touch on the binary.
#
# Bring the stack up, building the initramfs without a container.
[macos]
up-nodocker: artifacts gvproxy initramfs-nodocker minvmd-build minimal-cli && (_smoke)
# Bring the stack up: host-native minimald, no VM (`just up-kvm` for the VM stack).
[linux]
up: minimald-build minimal-cli gvproxy && (_smoke "--minimal-dir" native-dir)
#!/usr/bin/env sh
set -eu
sock="{{native-dir}}/providers/local-minimald0/ssh.sock"
pidf="{{scratch}}/minimald.pid"
mkdir -p "{{native-dir}}"
# PID files survive reboots and PIDs get reused: only trust the pidfile if
# the PID is still THIS checkout's minimald (prefix match tolerates the
# "(deleted)" suffix a rebuilt binary leaves on /proc/<pid>/exe).
owns() { case "$(readlink "/proc/$1/exe" 2>/dev/null)" in "{{minimald-bin}}"*) return 0 ;; *) return 1 ;; esac; }
if [ -S "$sock" ] && [ -f "$pidf" ] && owns "$(cat "$pidf")"; then
echo "native minimald already up: $sock"
else
rm -f "$sock" "$pidf"
setsid "{{minimald-bin}}" \
--minimal-state-dir "{{native-dir}}" --minimal-cache-dir "{{native-dir}}/cache" \
run --instance-num 0 --gvproxy-bin "{{gvproxy}}" > "{{scratch}}/minimald.log" 2>&1 &
echo $! > "$pidf"
for _ in $(seq 1 50); do [ -S "$sock" ] && break; sleep 0.1; done
fi
[ -S "$sock" ] || { echo "minimald failed to bind $sock; see {{scratch}}/minimald.log" >&2; exit 1; }
# Warn-only: the daemon runs without the AppArmor profile, but every
# `activate` sandbox dies at uid_map EPERM until it's installed.
just _userns-check || true
echo "up: host-native minimald at $sock (pid $(cat "$pidf"))"
echo " own-IP demo: min --minimal-dir {{native-dir}} session activate -n net1 --network own-ip . && min --minimal-dir {{native-dir}} session attach net1"
# Bring the stack up: native Linux + one Linux VM over KVM (stop with `just stop`).
[linux]
up-kvm: _kvm artifacts gvproxy initramfs minvmd-build minimal-cli && (_smoke "--provider" "local-minvmd")
MINVMD_GVPROXY_BIN="{{gvproxy}}" "{{minvmd-bin}}" run --detach --timeout "$MINVMD_READY_TIMEOUT_SECS"
@echo "up-kvm: VM booted; minimald reachable at providers/local-minvmd0/ssh.sock"
# Stop the stack `just up` started.
[macos]
down: stop
# Stop the stack `just up` started (the host-native minimald).
[linux]
down:
#!/usr/bin/env sh
set -eu
pidf="{{scratch}}/minimald.pid"
[ -f "$pidf" ] || { echo "no native minimald running"; exit 0; }
pid="$(cat "$pidf")"
# Never signal a reused PID: the pidfile is only trusted if the process is
# still this checkout's minimald (see the matching check in `up`).
case "$(readlink "/proc/$pid/exe" 2>/dev/null)" in
"{{minimald-bin}}"*) ;;
*) rm -f "$pidf"; echo "stale pidfile (pid $pid is not this checkout's minimald); removed"; exit 0 ;;
esac
kill "$pid" 2>/dev/null || true
for _ in $(seq 1 30); do kill -0 "$pid" 2>/dev/null || break; sleep 0.1; done
kill -9 "$pid" 2>/dev/null || true; rm -f "$pidf"
echo "native minimald stopped (pid $pid)"
# Ubuntu 24.04+ gates unprivileged userns behind AppArmor. The fix is the
# installer's per-binary profile (grants userns to minimald ALONE — flipping
# the sysctl hands it to everything). Details: docs/reference/linux-host-setup.md.
[linux]
_userns-check:
#!/usr/bin/env sh
set -eu
if [ "$(sysctl -n kernel.apparmor_restrict_unprivileged_userns 2>/dev/null || echo 0)" = "0" ]; then exit 0; fi
if [ -e /etc/apparmor.d/minimald ] && grep -rqs "{{minimald-bin}}" /etc/apparmor.d/tunables 2>/dev/null; then exit 0; fi
echo "note: this host restricts unprivileged user namespaces (Ubuntu 24.04+);" >&2
echo " minimald's session sandbox cannot start until its AppArmor profile is" >&2
echo " installed — a one-time step that needs root:" >&2
echo " sudo scripts/install-apparmor-profile.sh --path {{minimald-bin}}" >&2
exit 1