Repository navigation
Expand file tree
/
Copy pathMakefile
More file actions
1697 lines (1545 loc) · 91 KB
/
Copy pathMakefile
File metadata and controls
1697 lines (1545 loc) · 91 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
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
SHELL := /bin/bash
# Guard: never run make as root on the host. Running build plumbing
# as root leaves root-owned files in target/ that break subsequent user builds
# with BrokenPipe errors from nextest finding stale binaries.
# Skip this guard inside containers (where root is normal).
ifeq ($(shell id -u),0)
ifeq ($(wildcard /.dockerenv /run/.containerenv),)
$(error Do not run make as root. Use 'make test-root' as your normal user — it uses sudo only for the test runner)
endif
endif
# Find Rust toolchain bin directory and set PATH
# Prefer stable (has musl target), fall back to any toolchain
RUST_BIN := $(shell command -v cargo >/dev/null 2>&1 && dirname $$(command -v cargo) || \
(test -x $(HOME)/.cargo/bin/cargo && echo $(HOME)/.cargo/bin) || \
(ls -d $(HOME)/.rustup/toolchains/stable-*/bin 2>/dev/null | head -1) || \
(ls -d $(HOME)/.rustup/toolchains/*/bin 2>/dev/null | head -1))
export PATH := $(RUST_BIN):$(PATH)
CARGO_BIN ?= cargo
# Every Cargo command issued by this Makefile holds a shared lease on this
# worktree's target directory. runner-disk-preflight.sh takes the same lease
# exclusively before pruning idle artifacts, so age-check -> delete cannot race
# a build. `override` prevents `make CARGO=cargo ...` from silently bypassing
# the safety protocol; CARGO_BIN remains available for toolchain selection.
override CARGO = "$(MAKEFILE_DIR)scripts/cargo-target-run.sh" $(CARGO_BIN) $(CARGO_LOCAL_CONFIG)
# Custom dependencies bin directory
CUSTOM_DEPS_BIN := /mnt/fcvm-btrfs/deps/bin
ifneq ($(wildcard $(CUSTOM_DEPS_BIN)),)
export PATH := $(CUSTOM_DEPS_BIN):$(PATH)
endif
# Brief notes (see .claude/CLAUDE.md for details):
# FILTER=x STREAM=1 - filter tests, stream output
# Assets are content-addressed (kernel by URL SHA, rootfs by script SHA, initrd by binary SHA)
# Logs: /tmp/fcvm-test-logs/
.PHONY: show-notes
show-notes:
@echo "━━━ fcvm ━━━ FILTER=$(FILTER) STREAM=$(STREAM) Assets=SHA-cached (see .claude/CLAUDE.md)"
.PHONY: test-runner-acceptance
test-runner-acceptance:
python3 scripts/test-runner-acceptance.py
# Offline CI trust and AWS argument regressions. No credentials or builds.
.PHONY: test-ci-scope
test-ci-scope:
python3 scripts/test-ci-stack-scope.py
.PHONY: test-ci-security test-ci-workflow-security test-ami-security
test-ci-security: test-ci-workflow-security test-ami-security
test-ci-workflow-security:
python3 scripts/test-ci-workflows.py
test-ami-security:
python3 scripts/test-ami-security.py
# Paths (can be overridden via environment)
FUSE_BACKEND_RS ?= /home/ubuntu/fuse-backend-rs
FUSE_BACKEND_RS_OVERRIDE ?=
FUSER ?= /home/ubuntu/fuser
# Container settings
CONTAINER_TAG := fcvm-test:latest
CONTAINER_ARCH ?= $(shell uname -m)
# Per-mode data directories (prevents UID conflicts between test modes)
ROOT_DATA_DIR := /mnt/fcvm-btrfs/root
CONTAINER_DATA_DIR := /mnt/fcvm-btrfs/container
# Test options: FILTER=pattern STREAM=1 LIST=1 IGNORED=1
FILTER ?=
ifeq ($(IGNORED),1)
NEXTEST_IGNORED := --run-ignored=all
else
NEXTEST_IGNORED :=
endif
# On IPv6-only hosts, auto-exclude bridged tests (they require IPv4 iptables)
# User can still explicitly run bridged tests with FILTER=bridged
ifeq ($(IPV6_ONLY),1)
ifndef FILTER
# No filter set: exclude bridged tests
NEXTEST_PARTITION := --partition hash:1/2
IPV6_FILTER := -E 'not test(/bridged/)'
else
# User set a filter: respect it (they know what they're doing)
IPV6_FILTER :=
endif
else
IPV6_FILTER :=
endif
# CI runs a representative SUBSET of the active nested (NV2) tests, not all of them.
# Nested tests are the arm64 long pole and do NOT parallelize — they degrade ~3.7x under
# mutual contention (system-wide dsb(sy) on every nested guest-exit + NV2 overhead; see the
# [test-groups.nested-tests] comment in .config/nextest.toml), so concurrency can't shrink
# them. Instead CI runs three that cover the distinct nested risks and skips the rest
# (which still run locally with a normal `make test-root`):
# - test_nested_run_fcvm_inside_vm : nesting works at all + /dev/kvm usable in the guest
# - test_nested_l2_with_large_files : FUSE-over-FUSE cache coherency (the #630 DSB regression guard)
# - test_nested_l2_nfs : the NFS-share data path under nesting
# Skipped in CI (still run locally): test_nested_l2_fuse (FUSE path is covered by large_files),
# test_nested_l2_network_fuse / _network_nfs (iperf throughput — the slowest AND flakiest under
# any contention), and test_utimensat_pjdfstest_nested_kernel. The L3/L4, benchmark, and
# podman-load nested tests are already #[ignore]'d (run manually, tracked in #630-B).
# Gated on CI=true (set by GitHub Actions). Override anytime with an explicit FILTER=...
# Scoped to package(fcvm) so fuse-pipe's fast test_nested_file unit test is NOT excluded.
# Mutually exclusive with IPV6_FILTER in practice — CI runners have an IPv4 route, so
# IPV6_ONLY is never set there (two -E filtersets would be UNIONED, not intersected).
CI ?=
ifndef FILTER
ifeq ($(CI),true)
# Base test-root CI filter: the nested subset (exclude all nested except the 3 kept).
CI_ROOT_BASE := (not (package(fcvm) & (test(/nested/) | test(/podman_load_over_fuse/))) | test(=test_nested_run_fcvm_inside_vm) | test(=test_nested_l2_with_large_files) | test(=test_nested_l2_nfs))
ifeq ($(FCVM_NO_SNAPSHOT),)
# SnapshotEnabled mode (FCVM_NO_SNAPSHOT unset): ALSO drop test_nested_l2_with_large_files.
# It guards FUSE-over-FUSE DATA INTEGRITY (the #630 DSB cache-coherency fix) — a live-traffic
# property of the always-on kernel patch, independent of the snapshot feature — and it already
# runs in the SnapshotDisabled job (~225s). Under SnapshotEnabled it runs TWICE (cold + restore)
# and the warm run is a 4GB *nested* VM UFFD restore whose page faults go through double Stage-2
# translation (~842s), ~18 min total for zero extra integrity coverage. Snapshot/restore
# correctness is covered by test_startup_snapshot_* and the CH/clone roundtrips. This is the
# single biggest lever on the arm64 SnapshotEnabled long pole (the file copy itself is only ~20s).
CI_NESTED_FILTER := -E '$(CI_ROOT_BASE) & not test(=test_nested_l2_with_large_files)'
else
# SnapshotDisabled mode: keep test_nested_l2_with_large_files (the FUSE-integrity guard runs here).
CI_NESTED_FILTER := -E '$(CI_ROOT_BASE)'
endif
endif
endif
# Default log level: fcvm debug, suppress FUSE spam
# Override with: RUST_LOG=debug make test-root
TEST_LOG ?= fcvm=debug,health-monitor=info,fuser=warn,fuse_backend_rs=warn,passthrough=warn
ifeq ($(STREAM),1)
NEXTEST_CAPTURE := --no-capture
endif
ifeq ($(LIST),1)
NEXTEST_CMD := list
else
NEXTEST_CMD := run
endif
# Architecture detection
ARCH := $(shell uname -m)
ifeq ($(ARCH),aarch64)
MUSL_TARGET := aarch64-unknown-linux-musl
else
MUSL_TARGET := x86_64-unknown-linux-musl
endif
# IPv6-only detection: If no IPv4 default route exists, bridged networking won't work
# (bridged uses IPv4 iptables DNAT). Auto-exclude bridged tests on IPv6-only hosts.
HAS_IPV4 := $(shell ip route show default 2>/dev/null | grep -q . && echo 1 || echo 0)
ifeq ($(HAS_IPV4),0)
IPV6_ONLY := 1
$(info Note: IPv6-only host detected - bridged tests will be skipped)
endif
# Build artifacts go in a per-worktree directory on btrfs; scripts/cargo-target-link.sh
# owns the derivation and the symlink, and is the ONLY place that computes the path.
# Overridable so tests/test_cargo_target_link.rs can point it at a temp dir.
BTRFS_ROOT ?= /mnt/fcvm-btrfs
export BTRFS_ROOT
MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
# A Make-only patch leaves the checked-in manifests resolvable by Dependabot.
# Use one absolute path for Cargo, provenance, and the container bind mount.
FUSE_BACKEND_RS_OVERRIDE_PATH := $(if $(strip $(FUSE_BACKEND_RS_OVERRIDE)),$(shell realpath -m -- "$(FUSE_BACKEND_RS_OVERRIDE)"))
CARGO_LOCAL_CONFIG = $(if $(FUSE_BACKEND_RS_OVERRIDE_PATH),--config 'patch."https://github.com/ejc3/fuse-backend-rs.git".fuse-backend-rs.path="$(FUSE_BACKEND_RS_OVERRIDE_PATH)"')
# cargo-deny 0.18.9 cannot consume Cargo's CLI patch configuration.
ifneq ($(FUSE_BACKEND_RS_OVERRIDE_PATH),)
ifneq ($(filter lint,$(MAKECMDGOALS)),)
$(error lint audits the locked Git graph; unset FUSE_BACKEND_RS_OVERRIDE and run make dependency-metadata first, or use make clippy for local code)
endif
endif
# Base test command
# `target` is a symlink into BTRFS_ROOT, created by the cargo-target-link target
# (a prerequisite of every build/test target).
export CARGO_TARGET_DIR := target
# Cargo does not forward global --config to external subcommands.
NEXTEST := $(CARGO) nextest $(NEXTEST_CMD) $(CARGO_LOCAL_CONFIG) --release
TEST_CONFIG_WRAPPER := ./scripts/with-test-config.sh
# sudo resets the environment, so a caller's FCVM_CONFIG_DIR has to be passed
# through explicitly; without it `fcvm setup` under sudo regenerates and reads
# root's config instead of the caller's isolated one. The recipe shell expands
# "$FCVM_CONFIG_DIR" itself, so spaces and double quotes in the directory
# survive. It is exported only when set, because fcvm rejects an empty value.
ifneq ($(FCVM_CONFIG_DIR),)
export FCVM_CONFIG_DIR
endif
SUDO_FCVM := sudo $(if $(FCVM_CONFIG_DIR),env "FCVM_CONFIG_DIR=$$FCVM_CONFIG_DIR",)
# Extra flags forwarded to every criterion bench recipe (see bench-quick).
#
# Criterion's flags belong to the BENCH BINARY, so they have to follow `--`.
# Given to cargo directly they are rejected by its own argument parser before
# any benchmark starts:
#
# $ cargo bench -p fuse-pipe --bench throughput --sample-size 10
# error: unexpected argument '--sample-size' found
# tip: to pass '--sample-size' as a value, use '-- --sample-size'
# Usage: cargo bench --package [<SPEC>] --bench [<NAME>] [-- [ARGS]...]
#
# BENCH_SEPARATED is what the recipes interpolate, and it stays empty when
# BENCH_ARGS is, so plain `make bench` carries no trailing separator.
BENCH_ARGS ?=
BENCH_SEPARATED = $(if $(strip $(BENCH_ARGS)),-- $(BENCH_ARGS))
# Where criterion keeps baselines and reports. Pinned to an absolute path
# because criterion's default is not the directory it looks like.
#
# Its second choice after CRITERION_HOME is `$CARGO_TARGET_DIR/criterion`, and
# CARGO_TARGET_DIR here is the relative string `target`, which criterion
# resolves inside the bench binary, whose working directory cargo sets to the
# package root. Every suite was therefore writing to `fuse-pipe/target/criterion`
# while the ownership repair scanned `target/` at the repo root and found
# nothing to do. Overridable so tests can point it at a scratch directory.
CRITERION_HOME ?= $(CURDIR)/target/criterion
# cargo/nextest target runner for the privileged suites. cargo-nextest stays unprivileged
# and only the test binary is elevated, so the `sudo` hop sits in the middle of the process
# tree — and a privilege boundary breaks parent->child teardown in BOTH directions: nextest
# (uid 1000) cannot signal the uid-0 test binary at all, and `sudo` cannot forward the
# SIGKILL that killed it. The elevated test binary is then orphaned-but-alive and its whole
# microVM subtree with it. root-test-runner.sh restores the link with a kernel-enforced
# PR_SET_PDEATHSIG; see that script's header for the measurements.
ROOT_TEST_RUNNER := sudo -E env PATH=$(PATH) $(CURDIR)/scripts/root-test-runner.sh
# Optional cargo cache directory (for CI caching)
CARGO_CACHE_DIR ?=
ifneq ($(CARGO_CACHE_DIR),)
# CI mode: use cache directory for both registry and target
CARGO_CACHE_MOUNT := -v $(CARGO_CACHE_DIR)/registry:/usr/local/cargo/registry
TARGET_MOUNT := -v $(CARGO_CACHE_DIR)/target:/workspace/fcvm/target
else
# Local mode: use temp directory for target (avoids permission conflicts)
CARGO_CACHE_MOUNT :=
TARGET_MOUNT := -v /tmp/fcvm-container-target:/workspace/fcvm/target
endif
# Test log directory (mounted into container)
TEST_LOG_DIR := /tmp/fcvm-test-logs
# Container run command (base)
# Note: Use -v instead of --device for /dev/kvm to preserve group permissions in rootless mode
# See: https://github.com/containers/podman/issues/16701
# The cache mount presents a different inode inside the container. Pass the
# host generation's lease through FD 3 so it also survives the Podman CLI.
CONTAINER_RUN_BASE := "$(MAKEFILE_DIR)scripts/cargo-target-run.sh" \
/bin/bash -ec 'exec 3<target; flock -s 3; exec "$$@"' -- \
podman run --preserve-fds=1 --rm --privileged \
--security-opt label=disable --group-add keep-groups \
-v .:/workspace/fcvm \
$(TARGET_MOUNT) \
-v "$(if $(FUSE_BACKEND_RS_OVERRIDE_PATH),$(FUSE_BACKEND_RS_OVERRIDE_PATH),$(FUSE_BACKEND_RS)):/workspace/fuse-backend-rs" -v $(FUSER):/workspace/fuser \
$(if $(FUSE_BACKEND_RS_OVERRIDE_PATH),-e FUSE_BACKEND_RS_OVERRIDE=/workspace/fuse-backend-rs) \
--device /dev/fuse -v /dev/kvm:/dev/kvm -v /dev/userfaultfd:/dev/userfaultfd \
--ulimit nofile=65536:65536 --ulimit nproc=-1:-1 \
-v /mnt/fcvm-btrfs:/mnt/fcvm-btrfs \
-v $(TEST_LOG_DIR):$(TEST_LOG_DIR) $(CARGO_CACHE_MOUNT) \
-e FCVM_DATA_DIR=$(CONTAINER_DATA_DIR)
# Container run with high process limits (for parallel test suites)
CONTAINER_RUN := $(CONTAINER_RUN_BASE) --ulimit nproc=65536:65536 --pids-limit=65536
.PHONY: all help build clean clean-test-data check-disk check-host-kernel-build \
test test-unit test-agent-unit test-fast test-all test-root test-packaging test-ci-infrastructure test-clone-floor-overlap fuzz race-gym _race-gym race-gym-cases \
_test-unit _test-agent-unit _test-fast _test-all _test-root _setup-fcvm _bench \
container-build container-test container-test-unit container-test-fast container-test-all container-test-fc-mock \
container-setup-fcvm container-shell container-clean container-bench \
cargo-target-link build-host-tools setup-btrfs setup-default release-default-kernel setup-fcvm setup-gpu setup-phone phone-emulator phone-system-image phone-test phone-app-test gpu-residue-test gpu-routed-test setup-cloud-hypervisor setup-passt setup-pjdfstest setup-hugepages bench bench-vm bench-hugepages bench-hugepages-test \
bench-container-import bench-computesdk bench-gpu-restore test-gpu-restore test-computesdk bench-chromium analyze-chromium-request analyze-chromium-campaign bench-clone-latency test-chromium-request \
bench-chromium-request-build bench-webkit-request-build bench-webkit-request-golden bench-webkit-request-verify bench-webkit-request-run test-chromium bench-chromium-request-golden bench-chromium-request-verify \
bench-chromium-corpus bench-chromium-corpus-extra bench-stop \
bench-chromium-request-run bench-chromium-request-all bench-chromium-hostcdp bench-chromium-fault \
bench-chromium-request-diag bench-webkit-request-diag \
bench-chromium-scale check-chromium-scale analyze-chromium-scale report-chromium-scale test-chromium-scale \
test-chromium-fault \
bench-quick bench-throughput bench-operations bench-protocol \
lint fmt update-dependency ssh test-serve-sdk
all: build
help:
@echo "fcvm Makefile"
@echo ""
@echo "Build:"
@echo " build Build fcvm + fc-agent"
@echo " build-fc-mock Build fc-mock (Firecracker mock for container mode)"
@echo " update-dependency Update one Cargo.lock package (PACKAGE=..., optional VERSION=...)"
@echo " clean Remove target directory"
@echo ""
@echo "Test (host):"
@echo " test-unit Unit tests only (no VMs, no sudo)"
@echo " test-agent-unit fc-agent unit tests only (no VMs, no sudo)"
@echo " test-fast + quick VM tests (rootless, no sudo)"
@echo " test-all + slow VM tests (rootless, no sudo)"
@echo " test-root, test + privileged tests (bridged, pjdfstest, sudo)"
@echo " test-fc-mock Run tests with fc-mock (no KVM required)"
@echo " test-clone-floor-overlap Reproduce the clone/CH/hugepage lifecycle overlap"
@echo " test-ci-infrastructure Deterministic runner-failure classifier fixtures"
@echo " fuzz Seeded lifecycle chaos fuzz (SEEDS=N|list OPS=M, defaults 1/10)"
@echo " race-gym Race gym: enumerate interleavings of concurrent actors (SCENARIO= PHASE=pairs|walk SCHEDULES= PARALLEL=; REPLAY=<file> [EMIT=<name>])"
@echo " race-gym-cases Run the race gym replays emitted into test_lifecycle_interleave.rs"
@echo ""
@echo "Test (container):"
@echo " container-test-unit Unit tests in container"
@echo " container-test-fast + quick VM tests in container"
@echo " container-test-all, container-test + slow VM tests in container"
@echo " container-test-fc-mock fc-mock tests in container (no KVM)"
@echo ""
@echo "Container:"
@echo " container-build Build test container"
@echo " container-shell Interactive shell in container"
@echo " container-bench Run benchmarks in container"
@echo ""
@echo "Setup:"
@echo " setup-btrfs Create btrfs loopback at /mnt/fcvm-btrfs"
@echo " setup-fcvm Download kernel and create rootfs"
@echo " setup-gpu Build the gpu profile kernel and NVIDIA driver image"
@echo " setup-phone Build the phone profile (gpu + KVM) kernel, Firecracker and driver image"
@echo " phone-emulator Build the Android Emulator from source for the phone (about 25 min once)"
@echo " phone-system-image Build the phone's Android system image and tools from AOSP source (x86_64 host)"
@echo " phone-test A phone VM renders on the GPU and moves to GPU2 by warm snapshot (GPU=, GPU2=)"
@echo " phone-app-test Robot Park's scene unit tests and shader check (fleet/phone-app)"
@echo " gpu-residue-test GPU memory residue between VMs (GPU=<pci address>)"
@echo " gpu-routed-test A routed GPU VM and its restore hold their IPv6 address (GPU=<pci address>)"
@echo " setup-passt Build and install the pinned upstream pasta/passt"
@echo " setup-pjdfstest Build pjdfstest"
@echo " setup-lint-tools Install cargo-audit and cargo-deny"
@echo " install-host-kernel Build and install host kernel with patches (requires reboot)"
@echo " check-host-kernel-build Check packages and free space install-host-kernel needs"
@echo ""
@echo "Kernel patches:"
@echo " kernel-patch-create PROFILE=nested NAME=0004-fix FILE=fs/fuse/dir.c"
@echo " kernel-patch-edit PROFILE=nested PATCH=0002"
@echo " kernel-patch-validate PROFILE=nested"
@echo ""
@echo "SDK:"
@echo " test-serve-sdk Run ComputeSDK E2E test (requires computesdk sibling repo)"
@echo ""
@echo "Benchmarks:"
@echo " bench Run fuse-pipe benchmarks"
@echo " bench-vm Run VM benchmarks (exec, clone)"
@echo " bench-hugepages Run hugepages benchmark (32GB VM, 16GB dirty)"
@echo " bench-hugepages-test Run hugepages benchmark (2GB VM, 256MB dirty)"
@echo " bench-container-import Compare podman load vs direct image mount"
@echo " bench-computesdk ComputeSDK Burst TTI against fcvm serve (CONCURRENCY=, ITERATIONS=, BURSTS=)"
@echo " bench-gpu-restore Cache-hit GPU VM restore latency, N runs (needs vfio-pci bound GPUs)"
@echo " bench-chromium Chromium shared-nothing clone bench (egress x memory matrix)"
@echo " bench-chromium-request-build Build the request-bench container image"
@echo " bench-webkit-request-build Build the WebKit request-bench container image"
@echo " bench-chromium-request-golden Create golden snapshot (TAG=, HUGEPAGES=1, NETMODE=, GUEST_ENV=)"
@echo " bench-chromium-request-verify Prove CDP hops on a restored clone (TAG=)"
@echo " bench-chromium-request-run Measured run (TAG=, BACKEND=, UFFD_MODE=, UFFD_PREFETCH=, REPS=, WARMUP=, ARMS=, RESULTS=)"
@echo " bench-chromium-request-all Full chain: image, golden, verify, run"
@echo " bench-chromium-request-diag In-guest load diagnostics, one traced render per clone, serve always --uffd-prefetch off (TAG=, BACKEND=, UFFD_MODE=, DIAG_URLS=, DIAG_REPS=, DIAG_EXPECT_IPS=, DIAG_MAX_LOAD_MS=, RESULTS=)"
@echo " bench-webkit-request-diag WebKit twin of bench-chromium-request-diag (TAG=, BACKEND=, DIAG_URLS=, DIAG_REPS=, DIAG_EXPECT_IPS=, DIAG_MAX_LOAD_MS=, RESULTS=)"
@echo " bench-chromium-corpus Corpus campaign, orchestrator frozen per run (TAG=, CPU=, PHASE=)"
@echo " bench-chromium-corpus-extra Host-control and matched-memory corpus campaign (PHASES=, RESULTS=)"
@echo " bench-stop Stop all bench processes, reap stray VMs, restore dnsmasq"
@echo " bench-chromium-hostcdp Host direct-CDP baseline (COMPARISON_LABEL=standalone CPU_BUDGET=unlimited; CPUS= needs CPU_BUDGET=vm-matched)"
@echo " bench-chromium-fault Page-fault bench (FAULT_OUT= required; needs bench.sh goldens)"
@echo " analyze-chromium-request Re-run publication gates for RESULTS=/path/to/run"
@echo " analyze-chromium-campaign Index retained CAMPAIGN_RUNS='run1 run2' into CAMPAIGN_OUT=path"
@echo " test-chromium Run ALL bench unit tests (what CI runs)"
@echo " test-gpu-restore Run bench/gpu-restore/summarize.py's unit tests"
@echo " test-computesdk Run bench/computesdk/run.sh's output-directory checks"
@echo " test-chromium-request Run the request benchmark's deterministic unit tests"
@echo " bench-chromium-scale Open-loop FILE/UFFD Chromium request scalability run"
@echo " check-chromium-scale Validate the SCALE_* variables and their schedule, no VM"
@echo " analyze-chromium-scale Validate a scale run and write deterministic JSON"
@echo " report-chromium-scale Validate a scale run and write plain Markdown"
@echo " test-chromium-scale Run the scale harness's deterministic unit tests"
@echo " test-chromium-fault Run the fault harness's deterministic unit tests"
@echo " bench-clone-latency Clone spawn->exec-ready latency (LABEL=, N=)"
@echo ""
@echo "CI merge train (pooled CI for a batch of PRs, see docs/ci-train.md):"
@echo " train-create PRS=\"689 690\" Build the ci-train branch from a batch of PRs"
@echo " train-dispatch Push the train and dispatch ONE full CI matrix for the batch"
@echo " train-status Show the train run's state"
@echo " train-land After a green run, merge every batch PR in order"
@echo " train-bisect Split a red train into two half-trains (recurse with TRAIN=ci-train-a)"
@echo ""
@echo "Other:"
@echo " lint Run linting (auto-installs tools if needed)"
@echo " fmt Format code"
@echo " clean-test-data Remove VM disks, snapshots, state (keeps cached assets)"
@echo " check-disk Check disk space requirements"
@echo ""
@echo "Options: FILTER=pattern STREAM=1 LIST=1"
# Point this worktree's target/ at its own directory on btrfs.
#
# Every entry point that invokes cargo must depend on this. `make build` had no
# such prerequisite, so a fresh worktree created a REAL target/ on the root
# filesystem — which is nearly full — silently defeating the whole arrangement.
.PHONY: cargo-target-link
cargo-target-link:
@BTRFS_ROOT="$(BTRFS_ROOT)" "$(MAKEFILE_DIR)scripts/cargo-target-link.sh"
# One lease per RECIPE, for the recipes that reach through target/ outside
# Cargo.
#
# cargo-target-run.sh leases the published generation for the length of one
# Cargo process. A concurrent make waiting to republish target/ is granted the
# exclusive lease the moment that process exits, so a recipe that writes a file
# through the link and then reads it back can read a generation its own build
# never wrote. make runs a recipe line as `$(SHELL) $(.SHELLFLAGS) "<line>"`,
# so naming the wrapper as a target-specific SHELL puts the whole line inside
# one shared lease; each such line names the targets below, and
# makefile_leases_every_raw_target_access pins that no raw target/ access is
# left outside one.
#
# `private` keeps the wrapper off the prerequisites, whose recipes run before
# target/ exists (cargo-target-link is what creates it). It is deliberately NOT
# the Makefile-wide SHELL: that also governs parse-time $(shell ...) and every
# recipe of every target, including in the scratch checkouts that
# tests/test_dep_provenance.rs stages with a partial scripts/ directory, where
# every recipe line then dies with `Error 127`.
TARGET_LEASE_SHELL := $(MAKEFILE_DIR)scripts/cargo-target-run.sh
# Disk space check - fails if either root or btrfs is too full
# Requires 10GB free on root (for cargo target) and 15GB on btrfs (for VMs)
check-disk: cargo-target-link
@# Ensure test log directory exists for container mounts
@mkdir -p $(TEST_LOG_DIR)
@# Fix advisory-db ownership (sudo/non-sudo mixing corrupts it)
@sudo chown -R $$(id -u):$$(id -g) "$$HOME/.cargo/advisory-db" 2>/dev/null || true
@sudo chown -R $$(id -u):$$(id -g) "$$HOME/.cargo/advisory-dbs" 2>/dev/null || true
@# Symlink ~/.cargo and target/ to btrfs so cargo builds don't fill root filesystem
@if [ -d /mnt/fcvm-btrfs ] && ! [ -L "$$HOME/.cargo" ]; then \
if [ -d "$$HOME/.cargo" ]; then \
echo "==> Moving existing ~/.cargo to /mnt/fcvm-btrfs/cargo..."; \
sudo rm -rf /mnt/fcvm-btrfs/cargo; \
mv "$$HOME/.cargo" /mnt/fcvm-btrfs/cargo; \
elif [ ! -e "$$HOME/.cargo" ]; then \
mkdir -p /mnt/fcvm-btrfs/cargo; \
fi; \
ln -sf /mnt/fcvm-btrfs/cargo "$$HOME/.cargo"; \
echo "==> Symlinked ~/.cargo → /mnt/fcvm-btrfs/cargo"; \
fi
@# Self-heal a wiped cargo home. The btrfs above is ephemeral and can be reset
@# out from under us, leaving ~/.cargo a dangling symlink. cargo can't restore
@# itself, so recreate the symlink's TARGET dir (mkdir through a dangling symlink
@# fails, so operate on the readlink'd path) and relink the rustup toolchain
@# shims; the registry re-downloads on the next build.
@if [ -L "$$HOME/.cargo" ] && ! [ -e "$$HOME/.cargo/bin/cargo" ]; then \
echo "==> cargo home wiped; restoring toolchain..."; \
TGT=$$(readlink "$$HOME/.cargo"); \
mkdir -p "$$TGT/bin"; \
TCBIN=$$(ls -d "$$HOME"/.rustup/toolchains/*/bin 2>/dev/null | head -1); \
if [ -n "$$TCBIN" ]; then \
for b in cargo rustc cargo-clippy clippy-driver rustfmt cargo-fmt rustdoc; do \
[ -e "$$TCBIN/$$b" ] && ln -sf "$$TCBIN/$$b" "$$TGT/bin/$$b"; \
done; \
echo "==> Restored cargo shims from $$TCBIN into $$TGT/bin"; \
else echo "==> WARNING: no rustup toolchain found to restore cargo from"; fi; \
fi
@# Ensure cargo-nextest (the test runner; a separate install from rustup).
@if ! PATH="$$HOME/.cargo/bin:$$PATH" $(CARGO) nextest --version >/dev/null 2>&1; then \
echo "==> Installing cargo-nextest..."; \
case "$$(uname -m)" in aarch64|arm64) NURL=https://get.nexte.st/latest/linux-arm ;; *) NURL=https://get.nexte.st/latest/linux ;; esac; \
mkdir -p "$$HOME/.cargo/bin"; \
curl -LsSf "$$NURL" | tar zxf - -C "$$HOME/.cargo/bin" \
|| PATH="$$HOME/.cargo/bin:$$PATH" $(CARGO) install cargo-nextest --locked; \
fi
@BTRFS_FREE=$$(df -BG /mnt/fcvm-btrfs 2>/dev/null | awk 'NR==2 {gsub("G",""); print $$4}'); \
if [ -n "$$BTRFS_FREE" ] && [ "$$BTRFS_FREE" -lt 15 ]; then \
echo "ERROR: Need 15GB free on /mnt/fcvm-btrfs (have $${BTRFS_FREE}GB)"; \
echo "Try: make clean-test-data"; \
exit 1; \
fi; \
echo "Disk check passed: /mnt/fcvm-btrfs has $${BTRFS_FREE}GB free"
# Clean leftover test data (VM disks, snapshots, state files)
# Preserves cached assets (kernels, rootfs, initrd, image-cache)
# CRITICAL: Uses fcvm's proper cleanup commands to handle btrfs CoW correctly
clean-test-data: private SHELL := $(TARGET_LEASE_SHELL)
clean-test-data: build
@echo "==> Killing stale VM processes from previous runs..."
@sudo pkill -9 firecracker 2>/dev/null; sudo pkill -9 pasta 2>/dev/null; sudo kill -9 $$(pgrep -x sleep -P 1) 2>/dev/null; sleep 1; true
@echo "==> Cleaning stale network namespaces..."
@for ns in $$(sudo ip netns list 2>/dev/null | grep -E '^(fcvm-|test-lf-|test-pf-|test-proxy-)' | awk '{print $$1}'); do sudo ip netns del "$$ns" 2>/dev/null && echo " deleted $$ns"; done; true
@echo "==> Cleaning stale iptables rules from fcvm VMs..."
@sudo iptables-save -t nat 2>/dev/null | grep -E 'MASQUERADE.*(172\.30\.|10\.0\.)' | sed 's/^-A//' | while read rule; do sudo iptables -t nat -D $$rule 2>/dev/null; done; true
@sudo ip6tables-save -t nat 2>/dev/null | grep 'MASQUERADE' | grep -v NETAVARK | sed 's/^-A//' | while read rule; do sudo ip6tables -t nat -D $$rule 2>/dev/null; done; true
@echo "==> Force unmounting stale FUSE mounts..."
@# Find and force unmount any FUSE mounts from previous test runs
@mount | grep fuse | grep -E '/tmp|/var/tmp' | cut -d' ' -f3 | xargs -r -I{} fusermount3 -u -z {} 2>/dev/null || true
@echo "==> Cleaning snapshots via fcvm (handles btrfs CoW properly)..."
@# Use fcvm's snapshot prune for proper cleanup - handles reflinks correctly
sudo ./target/release/fcvm snapshots prune --all --force 2>/dev/null || true
@# Also clean per-mode directories
sudo FCVM_DATA_DIR=$(ROOT_DATA_DIR) ./target/release/fcvm snapshots prune --all --force 2>/dev/null || true
sudo FCVM_DATA_DIR=$(CONTAINER_DATA_DIR) ./target/release/fcvm snapshots prune --all --force 2>/dev/null || true
@# Fallback: remove any snapshots that prune couldn't parse (stale/incompatible configs)
sudo rm -rf /mnt/fcvm-btrfs/snapshots/*
sudo rm -rf $(ROOT_DATA_DIR)/snapshots/* $(CONTAINER_DATA_DIR)/snapshots/*
@echo "==> Cleaning leftover VM disks..."
sudo rm -rf /mnt/fcvm-btrfs/vm-disks/*
sudo rm -rf $(ROOT_DATA_DIR)/vm-disks/* $(CONTAINER_DATA_DIR)/vm-disks/*
@echo "==> Cleaning state files..."
sudo rm -rf /mnt/fcvm-btrfs/state/*.json /mnt/fcvm-btrfs/state/*.lock
sudo rm -rf $(ROOT_DATA_DIR)/state/*.json $(ROOT_DATA_DIR)/state/*.lock
sudo rm -rf $(CONTAINER_DATA_DIR)/state/*.json $(CONTAINER_DATA_DIR)/state/*.lock
@echo "==> Removing root owner records that root does not own..."
@# A reclaim/0 owned by another uid was written by a rootless container's root (host uid
@# 1000), and every root fcvm on this host refuses it. Root's own records stay for reclaim.
sudo find /mnt/fcvm-btrfs/reclaim -mindepth 1 -maxdepth 1 -name 0 ! -uid 0 -exec rm -rf {} + 2>/dev/null || true
@echo "==> Cleaning UFFD sockets..."
sudo rm -f /mnt/fcvm-btrfs/uffd-*.sock
sudo rm -f $(ROOT_DATA_DIR)/uffd-*.sock $(CONTAINER_DATA_DIR)/uffd-*.sock
@echo "==> Cleaned test data (kept cached assets and test logs)"
# Record Cargo.lock's resolved FUSE Git sources, not unused sibling checkouts.
# An explicit fuse-backend-rs override reports the selected checkout instead:
# git describe plus a tracked-diff digest distinguishes local dirty states.
# Issue #807: two local fuse-backend-rs checkouts drifted 19 commits apart
# and no build log recorded which one a given binary used. Pinned by
# tests/test_dep_provenance.rs.
.PHONY: dep-provenance
dep-provenance:
@if [ -n "$(FUSE_BACKEND_RS_OVERRIDE_PATH)" ]; then \
dir="$(FUSE_BACKEND_RS_OVERRIDE_PATH)"; \
if desc=$$(git -C "$$dir" describe --always --dirty 2>/dev/null); then \
case "$$desc" in \
*-dirty) desc="$$desc+$$(git -C "$$dir" diff --no-ext-diff HEAD | sha256sum | cut -c1-12)" ;; \
esac; \
else desc=MISSING; fi; \
else \
desc=$$(awk -F'"' '/^name = "fuse-backend-rs"$$/ {f=1; next} f && /^\[\[package\]\]/ {exit} f && /^source = / {print $$2; exit}' "$(MAKEFILE_DIR)Cargo.lock" 2>/dev/null); \
fi; \
echo "fuse-backend-rs: $${desc:-MISSING}"
@src=$$(awk -F'"' '/^name = "fuser"$$/ {f=1; next} f && /^\[\[package\]\]/ {exit} f && /^source = / {print $$2; exit}' "$(MAKEFILE_DIR)Cargo.lock" 2>/dev/null); \
echo "fuser: $${src:-MISSING}"
# The provenance the dep-provenance prerequisite prints only describes the
# sources cargo read if nothing changed the sibling tree mid-build. Both
# build targets therefore snapshot the provenance before their cargo
# commands, re-derive it after, and fail on any difference instead of
# logging a line about a tree cargo never saw.
#
# The cargo commands, the copy that reads their output back through target/,
# and the config sync that runs the binary they produced are one recipe line,
# so one lease covers all of it. The sync is best-effort (the config is
# embedded at compile time and only needs pushing to the user config dir), so
# `test -x` is what states that the binary must exist; `|| true` on the sync
# would otherwise let `make build` report success with no target/release/fcvm.
build: private SHELL := $(TARGET_LEASE_SHELL)
build: cargo-target-link dep-provenance
@echo "==> Building..."
@set -e; \
before="$$($(MAKE) --no-print-directory dep-provenance)"; \
CARGO_TARGET_DIR=target $(CARGO) build --release -p fcvm; \
CARGO_TARGET_DIR=target $(CARGO) build --release -p fc-agent --target $(MUSL_TARGET); \
mkdir -p target/release; \
cp target/$(MUSL_TARGET)/release/fc-agent target/release/fc-agent; \
after="$$($(MAKE) --no-print-directory dep-provenance)"; \
if [ "$$before" != "$$after" ]; then \
printf 'ERROR: dependency provenance changed during the build\nbefore:\n%s\nafter:\n%s\n' "$$before" "$$after" >&2; \
exit 1; \
fi; \
test -x target/release/fcvm; \
./target/release/fcvm setup --generate-config --force 2>/dev/null || true
# Host-native tools used by the kernel-builder AMI/workflow. Unlike `build`,
# this does not require a musl Rust target for the guest agent.
build-host-tools: cargo-target-link dep-provenance
@echo "==> Building host-native fcvm and fc-agent..."
@set -e; \
before="$$($(MAKE) --no-print-directory dep-provenance)"; \
CARGO_TARGET_DIR=target $(CARGO) build --release -p fcvm -p fc-agent; \
after="$$($(MAKE) --no-print-directory dep-provenance)"; \
if [ "$$before" != "$$after" ]; then \
printf 'ERROR: dependency provenance changed during the build\nbefore:\n%s\nafter:\n%s\n' "$$before" "$$after" >&2; \
exit 1; \
fi
# The coordinator needs the host binary, not a guest agent or VM assets. Keep
# these entry points under the same worktree target lease as the other builds.
.PHONY: build-fleet-coordinator test-fleet-coordinator
build-fleet-coordinator: private SHELL := $(TARGET_LEASE_SHELL)
build-fleet-coordinator: cargo-target-link dep-provenance
@set -e; \
before="$$($(MAKE) --no-print-directory dep-provenance)"; \
CARGO_TARGET_DIR=target $(CARGO) build --locked --release -p fcvm; \
after="$$($(MAKE) --no-print-directory dep-provenance)"; \
test "$$before" = "$$after"; \
test -x target/release/fcvm
test-fleet-coordinator: private SHELL := $(TARGET_LEASE_SHELL)
test-fleet-coordinator: cargo-target-link dep-provenance
CARGO_TARGET_DIR=target $(CARGO) test --locked --release --no-default-features -p fcvm --test test_fleet_coord
# The directed camera's program with no votes, as JSON (fleet/core/examples/program.rs):
# make fleet-program SEED=1 SECS=600 OUT=program.json
.PHONY: fleet-program
fleet-program: private SHELL := $(TARGET_LEASE_SHELL)
fleet-program: cargo-target-link
@test -n "$(OUT)" || { echo "fleet-program needs OUT=FILE" >&2; exit 2; }
CARGO_TARGET_DIR=target $(CARGO) run --locked --release -q -p fleet-core --example program -- $(or $(SEED),1) $(or $(SECS),600) > $(OUT)
build-fc-mock: private SHELL := $(TARGET_LEASE_SHELL)
build-fc-mock: cargo-target-link
@echo "==> Building fc-mock..."
CARGO_TARGET_DIR=target $(CARGO) build --release -p fc-mock
@# Install to /usr/local/bin so it's accessible from within user namespaces
@# (rootless networking uses nsenter which can't traverse /home/ubuntu/ with 750 perms)
sudo install -m 755 target/release/fc-mock /usr/local/bin/fc-mock
# Test that the release binary works without source tree (simulates cargo install)
test-packaging: private SHELL := $(TARGET_LEASE_SHELL)
test-packaging: build
@echo "==> Testing packaging (simulates cargo install)..."
./scripts/test-packaging.sh target/release/fcvm
clean:
@# Publish an empty namespace; the disk guard safely reclaims the retired
@# generation without unlinking dentries that may be foreign mountpoints.
@BTRFS_ROOT="$(BTRFS_ROOT)" "$(MAKEFILE_DIR)scripts/cargo-target-link.sh" --rotate
# Run-only targets (no setup deps, used by container)
_test-unit: cargo-target-link
$(TEST_CONFIG_WRAPPER) $(NEXTEST) --no-default-features $(FILTER)
_test-agent-unit: cargo-target-link
$(TEST_CONFIG_WRAPPER) $(NEXTEST) -p fc-agent $(FILTER)
_test-fast: cargo-target-link
RUST_LOG="$(TEST_LOG)" \
$(TEST_CONFIG_WRAPPER) ./scripts/no-sudo.sh $(NEXTEST) $(NEXTEST_CAPTURE) --no-default-features --features integration-fast $(FILTER)
_test-all: cargo-target-link
RUST_LOG="$(TEST_LOG)" \
$(TEST_CONFIG_WRAPPER) ./scripts/no-sudo.sh $(NEXTEST) $(NEXTEST_CAPTURE) $(FILTER)
_test-root: private SHELL := $(TARGET_LEASE_SHELL)
_test-root: cargo-target-link
@if find target/ -user root -print -quit 2>/dev/null | grep -q .; then \
echo "==> WARNING: root-owned files in target/ (from sudo cargo?). Fixing ownership..."; \
sudo chown -R $$(id -u):$$(id -g) target/; \
fi
@RUST_LOG="$(TEST_LOG)" \
FCVM_DATA_DIR=$(ROOT_DATA_DIR) \
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUNNER='$(ROOT_TEST_RUNNER)' \
CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUNNER='$(ROOT_TEST_RUNNER)' \
$(TEST_CONFIG_WRAPPER) $(NEXTEST) $(NEXTEST_CAPTURE) $(NEXTEST_IGNORED) --features privileged-tests $(IPV6_FILTER) $(CI_NESTED_FILTER) $(FILTER) || \
{ echo ""; \
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"; \
echo "TEST FAILED - Check debug logs for root cause:"; \
echo " 📋 Debug logs: /tmp/fcvm-test-logs/*.log"; \
echo " 💡 Re-run with STREAM=1 to see tracing output in real-time"; \
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"; \
exit 1; }
# Host targets (with setup, check-disk first to fail fast if disk is full)
test-unit: show-notes check-disk build _test-unit
test-agent-unit: show-notes check-disk cargo-target-link _test-agent-unit
test-fast: show-notes check-disk setup-fcvm _test-fast
test-all: show-notes check-disk setup-fcvm _test-all
test-root: show-notes check-disk setup-fcvm setup-pjdfstest setup-hugepages setup-cloud-hypervisor _test-root
test: test-root
# Seeded lifecycle chaos fuzz (tests/test_fuzz_chaos.rs): one rootless VM per
# seed, randomized-but-seeded op schedule, end-state oracles. Runs through the
# same _test-root machinery as the rest of the suite.
# Usage: make fuzz SEEDS=25 OPS=30 (defaults: SEEDS=1 OPS=10)
# SEEDS is a count N (runs seeds 1..=N) or a comma list of literal seeds
# ("3,17,42"; a trailing comma like "7," replays exactly seed 7).
SEEDS ?= 1
OPS ?= 10
fuzz: show-notes check-disk setup-fcvm
FCVM_FUZZ_SEEDS=$(SEEDS) FCVM_FUZZ_OPS=$(OPS) $(MAKE) _test-root FILTER=fuzz_lifecycle_chaos
# Race gym (docs/race-gym.md): concurrent fcvm actors parked at named failpoints and
# released in enumerated orders. Rootless, so it runs as the invoking user.
# make race-gym SCENARIO=cold-run PHASE=pairs SCHEDULES=N PARALLEL=P
# make race-gym PHASE=walk SCHEDULES=200 SEED=1
# make race-gym REPLAY=<file> rerun one recorded schedule
# make race-gym REPLAY=<file> EMIT=<name> [PIN=outcome] add it as a case to test_lifecycle_interleave.rs
# failing while a PIN oracle fails (default: any)
# make race-gym-cases run the emitted cases
# Host inputs come from the environment: RACE_GYM_GPUS (one --gpu per actor),
# RACE_GYM_ARGS (extra podman run arguments), RACE_GYM_IMAGE, RACE_GYM_OUT.
SCENARIO ?= cold-run
PHASE ?= pairs
SCHEDULES ?=
PARALLEL ?= 1
SEED ?= 1
FIRST ?= 0
REPLAY ?=
EMIT ?=
RERECORD ?=
PIN ?=
race-gym: show-notes check-disk
ifneq ($(EMIT),)
./scripts/race-gym-emit.sh "$(REPLAY)" "$(EMIT)" "$(PIN)"
else
$(MAKE) setup-fcvm _race-gym
endif
_race-gym: cargo-target-link
RACE_GYM_SCENARIO="$(SCENARIO)" RACE_GYM_PHASE="$(PHASE)" RACE_GYM_SCHEDULES="$(SCHEDULES)" \
RACE_GYM_PARALLEL="$(PARALLEL)" RACE_GYM_SEED="$(SEED)" RACE_GYM_FIRST="$(FIRST)" RACE_GYM_REPLAY="$(REPLAY)" RACE_GYM_RERECORD="$(RERECORD)" \
$(TEST_CONFIG_WRAPPER) $(NEXTEST) $(NEXTEST_CAPTURE) --no-default-features --features race-gym -E 'binary(test_race_gym)'
race-gym-cases: show-notes check-disk setup-fcvm cargo-target-link
$(TEST_CONFIG_WRAPPER) $(NEXTEST) $(NEXTEST_CAPTURE) --no-default-features --features integration-fast,race-gym \
-E 'binary(test_lifecycle_interleave) & test(/lifecycle_interleave_gym_/)'
# fc-mock: container-mode tests (no KVM required)
# Uses fc-mock binary instead of Firecracker.
# Only runs tests known to work with fc-mock (no KVM, no real Firecracker).
FC_MOCK_FILTER := package(fcvm) & (test(=test_sanity_bridged) | test(=test_sanity_rootless) | test(/fc_mock/) | test(/state_manager/) | test(/health_monitor/) | test(/no_sudo/))
_test-fc-mock: cargo-target-link
@FCVM_FIRECRACKER_BIN=/usr/local/bin/fc-mock \
RUST_LOG="$(TEST_LOG)" \
FCVM_DATA_DIR=$${FCVM_DATA_DIR:-$(ROOT_DATA_DIR)} \
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUNNER='$(ROOT_TEST_RUNNER)' \
CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUNNER='$(ROOT_TEST_RUNNER)' \
$(TEST_CONFIG_WRAPPER) $(NEXTEST) $(NEXTEST_CAPTURE) --profile fc-mock --features privileged-tests -E '$(FC_MOCK_FILTER)' $(FILTER) || \
{ echo ""; \
echo "TEST FAILED (fc-mock mode)"; \
exit 1; }
test-fc-mock: show-notes check-disk build build-fc-mock setup-fcvm _test-fc-mock
# Container targets (setup on host where needed, run-only in container)
# Container uses shadowed target/ mount to avoid permission conflicts
# check-disk runs on host before container tests start
# fc-mock in container (subset — networking tests excluded)
# Rootless podman containers can't create nested user namespaces or TAP devices,
# so only fc-mock unit tests run here. Full fc-mock tests run on bare metal (test-fc-mock).
FC_MOCK_CONTAINER_FILTER := package(fcvm) & (test(/fc_mock/) | test(/state_manager/) | test(/health_monitor/) | test(/no_sudo/)) & not test(=test_fc_mock_sanity) & not test(=test_fc_mock_container_launch)
container-test-fc-mock: check-disk container-build setup-btrfs
@echo "==> Running fc-mock tests in container (unit tests only)..."
$(CONTAINER_RUN) $(CONTAINER_TAG) bash -c 'make build build-fc-mock && exec make _test-container-fc-mock "$$@"' -- \
FILTER='$(FILTER)' STREAM='$(STREAM)' LIST='$(LIST)' TEST_LOG='$(TEST_LOG)' \
FC_MOCK_CONTAINER_FILTER='$(FC_MOCK_CONTAINER_FILTER)'
# Construct Cargo arguments after the dependency override is remapped inside.
.PHONY: _test-container-fc-mock
_test-container-fc-mock: cargo-target-link
FCVM_FIRECRACKER_BIN=/usr/local/bin/fc-mock \
RUST_LOG="$(TEST_LOG)" \
$(NEXTEST) $(NEXTEST_CAPTURE) --profile fc-mock --features privileged-tests -E "$(FC_MOCK_CONTAINER_FILTER)" $(FILTER) || \
{ echo "TEST FAILED (fc-mock container mode)"; exit 1; }
container-test-unit: check-disk container-build
@echo "==> Running unit tests in container..."
$(CONTAINER_RUN) $(CONTAINER_TAG) make build _test-unit
container-test-fast: check-disk container-setup-fcvm
@echo "==> Running fast tests in container..."
$(CONTAINER_RUN) $(CONTAINER_TAG) make _test-fast
container-test-all: check-disk container-setup-fcvm
@echo "==> Running all tests in container..."
$(CONTAINER_RUN) $(CONTAINER_TAG) make _test-all
container-test: container-test-all
CONTAINER_CACHE_REPO ?= ghcr.io/ejc3/fcvm-cache
# Only push cache if authenticated to ghcr.io (CI has login, local dev doesn't)
CACHE_TO_FLAG := $(shell podman login --get-login ghcr.io >/dev/null 2>&1 && echo "--cache-to $(CONTAINER_CACHE_REPO)" || echo "")
container-build:
@sudo mkdir -p /mnt/fcvm-btrfs 2>/dev/null || true
@mkdir -p /tmp/fcvm-container-target
podman build -t $(CONTAINER_TAG) -f Containerfile --build-arg ARCH=$(CONTAINER_ARCH) \
--layers --cache-from $(CONTAINER_CACHE_REPO) $(CACHE_TO_FLAG) . \
|| podman build -t $(CONTAINER_TAG) -f Containerfile --build-arg ARCH=$(CONTAINER_ARCH) \
--layers --cache-from $(CONTAINER_CACHE_REPO) .
container-shell: cargo-target-link container-build
$(CONTAINER_RUN) -it $(CONTAINER_TAG) bash
container-clean:
podman rmi $(CONTAINER_TAG) 2>/dev/null || true
# Setup targets
setup-cloud-hypervisor: private SHELL := $(TARGET_LEASE_SHELL)
setup-cloud-hypervisor: build setup-btrfs
./target/release/fcvm setup --cloud-hypervisor
setup-passt:
./scripts/build-passt.sh
setup-pjdfstest:
@if [ ! -x /tmp/pjdfstest-check/pjdfstest ]; then \
echo '==> Building pjdfstest...'; \
rm -rf /tmp/pjdfstest-check && \
git clone --depth 1 https://github.com/pjd/pjdfstest /tmp/pjdfstest-check && \
cd /tmp/pjdfstest-check && autoreconf -ifs && ./configure && make; \
fi
# Hugepage pool for privileged tests (512 pages = 1GB, enough for 512MB test VMs)
HUGEPAGE_POOL_TESTS := 512
# The pool lock (/mnt/fcvm-btrfs/hugepage-pool.lock) is shared with
# bench-chromium-fault and bench/chromium/reqbench.sh, and taken through
# scripts/hugepage-pool-lock.sh, which opens it read-only (never O_CREAT, so
# fs.protected_regular cannot refuse it whoever owns it) and creates it
# atomically when absent. Do not touch, chmod, or chown the lock here: a
# root-created file with a `flock <path>` open behind it is how every
# unprivileged run after the first died with "Permission denied". (Keep this
# comment HERE: a `#` line inside the continued recipe below is shell text
# and eats the trailing backslash -- that is how #868's first revision
# produced `syntax error: unexpected end of file`.)
setup-hugepages:
@current=$$(cat /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages 2>/dev/null || echo 0); \
if [ "$$current" -ge "$(HUGEPAGE_POOL_TESTS)" ]; then \
echo "==> Hugepages already allocated: $$current (need $(HUGEPAGE_POOL_TESTS))"; \
else \
echo "==> Allocating hugepage pool ($(HUGEPAGE_POOL_TESTS) pages = $$(( $(HUGEPAGE_POOL_TESTS) * 2 ))MB)..."; \
scripts/hugepage-pool-lock.sh \
sudo sh -c 'echo $(HUGEPAGE_POOL_TESTS) > /proc/sys/vm/nr_hugepages'; \
fi
setup-btrfs:
@if [ -d /mnt/fcvm-btrfs ] && stat -f -c '%T' /mnt/fcvm-btrfs 2>/dev/null | grep -q btrfs; then \
echo '==> /mnt/fcvm-btrfs already on btrfs'; \
sudo chown $$(id -un):$$(id -gn) /mnt/fcvm-btrfs; \
elif stat -f -c '%T' /mnt 2>/dev/null | grep -q btrfs; then \
echo '==> /mnt is btrfs, creating /mnt/fcvm-btrfs as directory (no loopback needed)'; \
sudo mkdir -p /mnt/fcvm-btrfs && \
sudo chown $$(id -un):$$(id -gn) /mnt/fcvm-btrfs && \
mkdir -p /mnt/fcvm-btrfs/{kernels,rootfs,initrd,cache,image-cache}; \
elif ! mountpoint -q /mnt/fcvm-btrfs 2>/dev/null; then \
echo '==> Creating btrfs loopback (host is not btrfs)...'; \
if [ ! -f /var/fcvm-btrfs.img ]; then \
sudo truncate -s 60G /var/fcvm-btrfs.img && sudo mkfs.btrfs /var/fcvm-btrfs.img; \
fi && \
sudo mkdir -p /mnt/fcvm-btrfs && \
sudo mount -o loop /var/fcvm-btrfs.img /mnt/fcvm-btrfs && \
sudo mkdir -p /mnt/fcvm-btrfs/{kernels,rootfs,initrd,cache,image-cache} && \
sudo chown -R $$(id -un):$$(id -gn) /mnt/fcvm-btrfs && \
echo '==> btrfs ready at /mnt/fcvm-btrfs'; \
fi
@# Ensure these dirs exist with correct permissions (may be missing after reboot/corruption)
@sudo mkdir -p /mnt/fcvm-btrfs/image-cache /mnt/fcvm-btrfs/containers
@sudo chown $$(id -un):$$(id -gn) /mnt/fcvm-btrfs/image-cache /mnt/fcvm-btrfs/containers
@# Heal root-owned store entries (see the script header for why)
@./scripts/normalize-store-ownership.sh /mnt/fcvm-btrfs
@# Enable IP forwarding (required for bridged networking)
@sudo sysctl -q -w net.ipv4.ip_forward=1
@# Create per-mode data directories (state, snapshots, vm-disks)
@# Default: owned by current user (test-fast runs as ubuntu)
@mkdir -p /mnt/fcvm-btrfs/{state,snapshots,vm-disks,tmp}
@# ROOT_DATA_DIR: owned by root (test-root runs with sudo)
@sudo mkdir -p $(ROOT_DATA_DIR)/{state,snapshots,vm-disks}
@# CONTAINER_DATA_DIR: owned by current user (podman rootless maps to subordinate UIDs)
@sudo mkdir -p $(CONTAINER_DATA_DIR)/{state,snapshots,vm-disks}
@sudo chown -R $$(id -un):$$(id -gn) $(CONTAINER_DATA_DIR)
setup-default: private SHELL := $(TARGET_LEASE_SHELL)
setup-default: build setup-btrfs
@FREE_GB=$$(df -BG /mnt/fcvm-btrfs 2>/dev/null | awk 'NR==2 {gsub("G",""); print $$4}'); \
if [ -n "$$FREE_GB" ] && [ "$$FREE_GB" -lt 15 ]; then \
echo "ERROR: Need 15GB on /mnt/fcvm-btrfs (have $${FREE_GB}GB)"; \
exit 1; \
fi
@echo "==> Running fcvm setup (default kernel; builds only if release is absent)..."
@# Tests run fcvm via sudo, which reads /root/.config/fcvm — sync BOTH the
@# user and root configs or a rootfs-config.toml change silently boots the
@# previous rootfs under sudo (root keeps the stale SHA).
$(SUDO_FCVM) ./target/release/fcvm setup --generate-config --force
./target/release/fcvm setup --kernel-profile default --build-kernels
# The default kernel as a publishable release artifact.
#
# The release workflow uses this instead of calling `fcvm setup` directly, so that
# storage setup, config synchronisation and any future build routing stay in one
# place rather than being duplicated in YAML.
#
# FORCE=1 makes a forced rebuild actually rebuild, via --force-build-kernels.
# Deleting the cached file is NOT enough: the release being replaced is still
# published at build time, so `--build-kernels` (which only builds after a FAILED
# download) downloads it again and the job republishes the exact artifact the
# operator asked to replace. A post-run existence check cannot catch that, since
# a download leaves the same file. KERNEL_FILE names the artifact to assert on.
release-default-kernel: private SHELL := $(TARGET_LEASE_SHELL)
release-default-kernel: build setup-btrfs
@if [ "$(FORCE)" = "1" ] && [ -z "$(KERNEL_FILE)" ]; then echo "ERROR: FORCE=1 needs KERNEL_FILE=<vmlinux-...bin> to assert the rebuild produced it"; exit 1; fi
$(SUDO_FCVM) ./target/release/fcvm setup --generate-config --force
@if [ "$(FORCE)" = "1" ]; then echo "==> FORCE: rebuilding from source, bypassing the published release"; $(SUDO_FCVM) ./target/release/fcvm setup --kernel-profile default --force-build-kernels --config "$(CURDIR)/rootfs-config.toml"; else $(SUDO_FCVM) ./target/release/fcvm setup --kernel-profile default --build-kernels --config "$(CURDIR)/rootfs-config.toml"; fi
@if [ -n "$(KERNEL_FILE)" ] && [ ! -f "/mnt/fcvm-btrfs/kernels/$(KERNEL_FILE)" ]; then echo "ERROR: setup finished without producing /mnt/fcvm-btrfs/kernels/$(KERNEL_FILE)"; ls -la /mnt/fcvm-btrfs/kernels/; exit 1; fi
setup-fcvm: private SHELL := $(TARGET_LEASE_SHELL)
setup-fcvm: setup-default
@echo "==> Running fcvm setup --kernel-profile nested..."
./target/release/fcvm setup --kernel-profile nested --build-kernels
@echo "==> Running fcvm setup --kernel-profile btrfs..."
./target/release/fcvm setup --kernel-profile btrfs --build-kernels
# GPU passthrough guest: the gpu profile's kernel, its Firecracker build, and
# the NVIDIA driver image built against that kernel (src/setup/nvidia.rs). Not
# part of setup-fcvm: it downloads the NVIDIA driver (~300-400 MB). Reads this
# checkout's rootfs-config.toml rather than syncing it over ~/.config/fcvm.
setup-gpu: private SHELL := $(TARGET_LEASE_SHELL)
setup-gpu: build setup-btrfs
./target/release/fcvm setup --kernel-profile gpu --build-kernels --config "$(CURDIR)/rootfs-config.toml"
# Phone guest (x86_64): the gpu profile's kernel plus KVM, the Firecracker fork that keeps a
# guest's nested virtualization state in snapshots, and the NVIDIA driver image for that
# kernel (docs/fleet/ARCHITECTURE.md, "Phones mode").
setup-phone: private SHELL := $(TARGET_LEASE_SHELL)
setup-phone: build setup-btrfs
./target/release/fcvm setup --kernel-profile phone --build-kernels --config "$(CURDIR)/rootfs-config.toml"
# The Android Emulator built from source with the console's gpu release and gpu retake
# (scripts/phone/emulator.sh): about 25 minutes the first time, then reused.
phone-emulator:
./scripts/phone/emulator.sh
# The phone's Android 15 system image and the tools the phone test uses (adb, and what
# builds the probe app), built from AOSP source (scripts/phone/aosp-build.sh) into
# /mnt/fcvm-btrfs/aosp-build/<id>/dist. Runs only on an x86_64 host and needs about 400 GB
# free. scripts/phone/aosp.sh fetches the published build by its pinned checksums.
phone-system-image:
./scripts/phone/aosp-build.sh
# A phone VM renders on its GPU and survives a warm snapshot onto a second GPU
# (scripts/phone/run.sh): KVM works in the restored VM, an Android Emulator built from source
# boots the AOSP-built system image with -gpu host, SurfaceFlinger and an app render on the
# GPU, the running phone releases the GPU and takes it up again, and a clone of its warm
# snapshot runs the app on GPU2. Fetches the pinned system image and tools first
# (scripts/phone/aosp.sh), builds the emulator, and builds Robot Park (scripts/phone/app.sh)
# against the SDK's API 35 platform (scripts/phone/sdk.sh, which needs
# ACCEPT_ANDROID_SDK_LICENSE=1), which then draws seamless tiles, survives surface loss and
# takes its actions (scripts/phone/scene-test.sh).
phone-test: private SHELL := $(TARGET_LEASE_SHELL)
phone-test: $(if $(strip $(GPU)),setup-phone phone-emulator)
@test -n "$(GPU)" && test -n "$(GPU2)" || { echo "usage: make phone-test GPU=<pci address> GPU2=<another pci address> ACCEPT_ANDROID_SDK_LICENSE=1" >&2; exit 2; }
./scripts/phone/test-aosp-lock.sh
./scripts/phone/test-gpu-hook.sh
./scripts/phone/test-phone-sh.sh
./scripts/phone/aosp.sh
ACCEPT_ANDROID_SDK_LICENSE="$(ACCEPT_ANDROID_SDK_LICENSE)" ./scripts/phone/sdk.sh
./scripts/phone/app.sh > /dev/null
FCVM=./target/release/fcvm ./scripts/phone/run.sh "$(GPU)" "$(GPU2)"
# Robot Park's scene tests and shader check (fleet/phone-app), in its build container; no SDK.
phone-app-test:
$(MAKE) -C fleet/phone-app test
# A routed GPU VM, and one restored from its snapshot at another address, hold the
# --ipv6-address they were given on eth0 and reach the internet over IPv6
# (scripts/gpu-routed/run.sh). Needs root, a GPU on vfio-pci and a routable prefix.
gpu-routed-test: private SHELL := $(TARGET_LEASE_SHELL)
gpu-routed-test: $(if $(strip $(GPU)),setup-gpu)
@test -n "$(GPU)" || { echo "usage: make gpu-routed-test GPU=<pci address>" >&2; exit 2; }
sudo -E FCVM=./target/release/fcvm ./scripts/gpu-routed/run.sh "$(GPU)"
# Cross-tenant GPU memory residue (scripts/gpu-residue/run.sh): one VM fills a
# GPU's memory with a marker and ends, the next VM on that GPU counts the marker
# in fresh allocations. Needs a host with the GPU on vfio-pci.
gpu-residue-test: private SHELL := $(TARGET_LEASE_SHELL)
gpu-residue-test: setup-gpu
@test -n "$(GPU)" || { echo "usage: make gpu-residue-test GPU=<pci address> [RESIDUE_OUT=<dir>]" >&2; exit 2; }
FCVM=./target/release/fcvm ./scripts/gpu-residue/run.sh "$(GPU)" $(RESIDUE_OUT)
# Build and install host kernel with all patches from kernel/patches/
# Requires reboot to activate the new kernel
install-host-kernel: private SHELL := $(TARGET_LEASE_SHELL)
install-host-kernel: check-host-kernel-build build setup-btrfs
dir=$$($(MAKEFILE_DIR)scripts/check-host-kernel-build.sh) || exit 2; \
$(SUDO_FCVM) env FCVM_HOST_KERNEL_BUILD_DIR="$$dir" ./target/release/fcvm setup --kernel-profile nested --build-kernels --install-host-kernel
# Under make -j a prerequisite's position orders nothing, so when the goal is
# install-host-kernel the check is an order-only prerequisite of the build steps.
ifneq ($(filter install-host-kernel,$(MAKECMDGOALS)),)
build setup-btrfs: | check-host-kernel-build
endif
# Fails before anything is compiled when a package `bindeb-pkg` needs is missing
# or the build directory lacks room (scripts/check-host-kernel-build.sh), and
# prints the directory the build will use: on the scratch disk when /tmp is on
# the root disk.
check-host-kernel-build:
@dir=$$($(MAKEFILE_DIR)scripts/check-host-kernel-build.sh) && echo "==> host kernel build directory: $$dir"
# Run setup inside container (for CI - container has Firecracker)
container-setup-fcvm: cargo-target-link container-build setup-btrfs
@echo "==> Running fcvm setup in container..."
@# Fix ownership for rootless podman: container UID 0 maps to host user,
@# so /mnt/fcvm-btrfs/firecracker must be writable by current user (not root)
@sudo chown -R $$(id -un):$$(id -gn) /mnt/fcvm-btrfs/firecracker 2>/dev/null || true
$(CONTAINER_RUN) $(CONTAINER_TAG) make build _setup-fcvm
_setup-fcvm: private SHELL := $(TARGET_LEASE_SHELL)
_setup-fcvm:
@FREE_GB=$$(df -BG /mnt/fcvm-btrfs 2>/dev/null | awk 'NR==2 {gsub("G",""); print $$4}'); \
if [ -n "$$FREE_GB" ] && [ "$$FREE_GB" -lt 15 ]; then \
echo "ERROR: Need 15GB on /mnt/fcvm-btrfs (have $${FREE_GB}GB)"; \
exit 1; \
fi
$(SUDO_FCVM) ./target/release/fcvm setup --generate-config --force
$(SUDO_FCVM) ./target/release/fcvm setup --kernel-profile default --build-kernels