-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathTaskfile.yml
More file actions
2302 lines (2137 loc) · 112 KB
/
Copy pathTaskfile.yml
File metadata and controls
2302 lines (2137 loc) · 112 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
# DevCell Taskfile. See README.md
#
# SCOPE: `task image:*` is for CI / release publishing only.
# For LOCAL DEVELOPMENT, use the CLI: `cell build`.
# - `cell build` — rebuild the local cell image from this checkout
# - `cell build --update` — bump nix flake inputs + rebuild
# - `cell build --thin` — incremental, mounts nix store on a Docker volume
#
# Taxonomy (post CELL-180 + Modules 2.0):
# image:pure:* ← nix2container path (default, what CI ships)
# image:impure:* ← Dockerfile/bake path (DEPRECATED, kept for one release)
# image:{build,push} — aggregates (pure only after Modules 2.0)
# image:{mirror,manifest} — variant-agnostic utilities
#
# Back-compat aliases for muscle memory + external refs are preserved
# on each renamed task. See `task --list` for the full canonical surface.
version: '3'
vars:
GIT_COMMIT_HASH:
sh: git rev-parse --short HEAD 2>/dev/null || echo "unknown"
BUILD_DATE:
sh: date -u +%Y%m%dT%H%M%SZ
# Version resolution order:
# 1. explicit VERSION= override (e.g. release workflow passing the tag)
# 2. `git describe` — nearest annotated tag + commits-ahead + short SHA,
# -dirty suffix when the working tree has uncommitted changes
# 3. "v0.0.0" fallback when the repo is unavailable / shallow with no tags
# Result is stamped into version.Version so `cell --version` reports it
# regardless of the build path (Taskfile / goreleaser / flake all agree).
CELL_VERSION:
sh: |
if [ -n "${VERSION:-}" ]; then
echo "$VERSION"
else
git describe --tags --always --dirty 2>/dev/null || echo "v0.0.0"
fi
CELL_LDFLAGS: -s -w -X github.com/DimmKirr/devcell/internal/version.Version={{.CELL_VERSION}} -X github.com/DimmKirr/devcell/internal/version.GitCommit={{.GIT_COMMIT_HASH}} -X github.com/DimmKirr/devcell/internal/version.BuildDate={{.BUILD_DATE}}
CELL_BUILD_TAGS:
sh: |
tags=""
if pkg-config --exists wimlib 2>/dev/null; then tags="wimlib"; fi
echo "$tags"
# Docker daemon the Docker-dependent test tasks talk to.
#
# Pinned to the Docker Desktop socket while the Colima migration is in
# flight, so a half-provisioned Colima VM cannot silently become the build
# daemon mid-suite (a 2 GiB stock VM makes the thin build's ceilings drop
# out entirely — see clampBuildLimits and `task debug:colima`).
#
# Resolution order:
# 1. explicit DOCKER_SOCK= override
# 2. ~/.docker/run/docker.sock — Docker Desktop, present on the host Mac
# 3. ambient DOCKER_HOST, else /var/run/docker.sock — this is the path
# inside a devcell container, where the host forwards its daemon socket
# and the Docker Desktop user socket does NOT exist. Hardcoding (2)
# would break every container-side run.
#
# Target a different daemon explicitly:
# task test:integration DOCKER_SOCK=unix://$HOME/.colima/default/docker.sock
DOCKER_SOCK:
sh: |
if [ -S "$HOME/.docker/run/docker.sock" ]; then
echo "unix://$HOME/.docker/run/docker.sock"
else
echo "${DOCKER_HOST:-unix:///var/run/docker.sock}"
fi
env:
BUILDKIT_PROGRESS: plain
tasks:
# ── Generation (artefacts derived from source) ─────────────────────────
swagger:generate:
aliases: [swag:generate] # back-compat
desc: Regenerate Swagger docs from Go annotations
dir: "{{.TASKFILE_DIR}}"
silent: true
cmds:
- go run github.com/swaggo/swag/cmd/swag@latest init -g cmd/serve.go -o docs --parseDependency --parseInternal
docs:generate:
aliases: [web:docs] # back-compat
desc: Generate cell CLI markdown reference for the web site (requires Go)
dir: "{{.TASKFILE_DIR}}"
silent: true
cmds:
- go run $(ls cmd/*.go | grep -Ev '(_test|main)\.go') web/src/content/cell
hm:generate:
desc: Regenerate nix/home-manager/options.nix from the Go config schema (internal/cfg.CellConfig)
dir: "{{.TASKFILE_DIR}}"
silent: true
cmds:
- mkdir -p nix/home-manager
- go run cmd/hmoptgen.go -out nix/home-manager/options.nix
# Parse-check the module pair when nix is available (CI Go jobs may not have it).
- |
if command -v nix-instantiate >/dev/null 2>&1; then
for f in nix/home-manager/options.nix nix/home-manager/module.nix; do
nix-instantiate --parse "$f" >/dev/null || exit 1
done
fi
nix:sync:
desc: Sync flake.nix vendorHash with go.mod/go.sum and stage it — the one command to run after changing dependencies
dir: "{{.TASKFILE_DIR}}"
silent: true
cmds:
- |
# vendorHash covers the THIRD-PARTY module set (go.sum), not devcell's
# own source — nix hashes `src` itself. So this only ever needs running
# when go.mod/go.sum change; editing devcell code never invalidates it.
#
# Resolution is the fixed-output dance: build goModules with the current
# hash and read the `got:` value from the mismatch error. Deliberately
# NOT nix-update: its eval imports the flake via `getFlake <path>`,
# which copies the ENTIRE repo directory into the store — including
# gitignored VM images (test/ alone is >100 GB) — and fills the disk.
# `nix build` on a flake ref uses the git fetcher: tracked files only.
FLAKE="{{.TASKFILE_DIR}}/flake.nix"
OLD=$(grep 'vendorHash = "sha256-' "$FLAKE" | sed 's/.*"\(sha256-[^"]*\)".*/\1/')
echo "Verifying vendorHash against go.sum ($(awk '{print $1}' go.sum | sort -u | wc -l | tr -d ' ') modules — downloads on first run)..."
if OUT=$(nix build "{{.TASKFILE_DIR}}#cell.goModules" --no-link 2>&1); then
echo "vendorHash already correct ($OLD)"
exit 0
fi
NEW=$(printf '%s\n' "$OUT" | sed -n 's/.*got: *\(sha256-[^ ]*\).*/\1/p' | head -1)
if [ -z "$NEW" ]; then
echo "ERROR: goModules build failed for a reason other than a hash mismatch:" >&2
printf '%s\n' "$OUT" | tail -15 >&2
exit 1
fi
sed "s|$OLD|$NEW|" "$FLAKE" > "$FLAKE.tmp" && mv "$FLAKE.tmp" "$FLAKE"
if ! nix build "{{.TASKFILE_DIR}}#cell.goModules" --no-link; then
echo "ERROR: resolved hash did not verify — flake.nix restored." >&2
git checkout -- "$FLAKE"
exit 1
fi
git add "$FLAKE"
echo "vendorHash updated and staged: $OLD → $NEW"
nix:check-vendor-hash:
desc: Warn if go.mod/go.sum changed without a vendorHash update (non-blocking, instant)
dir: "{{.TASKFILE_DIR}}"
silent: true
cmds:
- |
# Instant check: if go.mod or go.sum are staged, the vendorHash
# line in flake.nix must be part of the same staged diff. Merely
# touching flake.nix for an unrelated reason (e.g. an unrelated
# nix module edit riding in the same commit) is not enough — it
# previously produced a false "all good" on commit 96bee90. No
# nix build needed for this check.
if git diff --cached --name-only | grep -qE '^(go\.mod|go\.sum)$'; then
if ! git diff --cached -- flake.nix | grep -q '^[+-].*vendorHash'; then
echo "⚠ go.mod/go.sum changed but the vendorHash line in flake.nix was not updated — run: task nix:sync" >&2
fi
fi
exit 0
nix:build:
desc: Build the cell package via nix and verify it runs — the real check that vendorHash/flake.nix are correct
dir: "{{.TASKFILE_DIR}}"
deps: [hm:generate]
cmds:
- |
set -e
OUT=$(nix build .#cell --no-link --print-out-paths)
"$OUT/bin/cell" --version
test:powershell:lint:
desc: Lint the guest PowerShell scripts (.ps1, .psm1) with PSScriptAnalyzer — template lint lives in go-winkit (task test:powershell:lint there)
dir: "{{.TASKFILE_DIR}}"
silent: true
cmds:
- |
set -e
if ! command -v pwsh >/dev/null 2>&1; then
echo "pwsh not found — install: nix profile install nixpkgs#powershell" >&2
exit 1
fi
pwsh -NoProfile -NonInteractive -Command '
if (-not (Get-Module -ListAvailable PSScriptAnalyzer)) {
Write-Output "Installing PSScriptAnalyzer..."
Install-Module PSScriptAnalyzer -Force -Scope CurrentUser -Repository PSGallery
}
$files = @(
Get-ChildItem -Recurse -Path "internal/vm/qemu/guest" -Include "*.ps1","*.psm1"
)
if ($files.Count -eq 0) { Write-Output "No .ps1/.psm1 files found"; exit 0 }
$fail = $false
foreach ($f in $files) {
$results = Invoke-ScriptAnalyzer -Path $f.FullName -Severity Error,Warning
if ($results) {
$fail = $true
foreach ($r in $results) {
Write-Output (" {0}:{1} [{2}] {3}" -f $r.ScriptName, $r.Line, $r.Severity, $r.Message)
}
} else {
Write-Output (" OK {0}" -f $f.FullName)
}
}
if ($fail) { exit 1 } else { Write-Output "All scripts clean." }
'
bake:validate:
desc: Print resolved docker-bake config (HCL parse check, no build)
dir: "{{.TASKFILE_DIR}}"
silent: true
cmds:
- GIT_COMMIT={{.GIT_COMMIT_HASH}} docker buildx bake --file {{.TASKFILE_DIR}}/docker-bake.hcl --print ci
# ── cell CLI binary ────────────────────────────────────────────────────
cell:deps:
desc: Verify build-time library dependencies
dir: "{{.TASKFILE_DIR}}"
silent: true
cmds:
- |
ok=1
if [ "$(uname -s)" = "Darwin" ]; then
if ! command -v pkg-config >/dev/null 2>&1; then
echo "⚠ pkg-config not found (brew install pkg-config)"
ok=0
fi
if ! pkg-config --exists wimlib 2>/dev/null; then
echo "⚠ wimlib not found — QEMU Windows VM builds will be disabled"
echo " Install: brew install wimlib"
fi
fi
if ! command -v go >/dev/null 2>&1; then
echo "✗ go not found"; ok=0
fi
[ "$ok" -eq 1 ] || { echo ""; echo "Fix the above and re-run."; exit 1; }
# macOS Sequoia (15.x) launch-constraint guard: after `go build`, the OS-
# recorded provenance for the freshly written file doesn't match the new
# binary's content → kernel SIGKILLs the process at exec time, before
# main() runs. Symptom: `cell --version` dies with `zsh: killed`, exit 137,
# no output. Strip xattrs + re-sign ad-hoc so the OS re-registers the
# binary's current state. No-op on Linux (uname guard) where neither
# `xattr` nor `codesign` exists.
cell:build:
desc: Build cell CLI binary → ./bin/cell
dir: "{{.TASKFILE_DIR}}"
deps: [swagger:generate, hm:generate, cell:deps]
silent: true
cmds:
- mkdir -p bin
- CGO_ENABLED={{if eq OS "darwin"}}1{{else}}0{{end}} go build {{if .CELL_BUILD_TAGS}}-tags "{{.CELL_BUILD_TAGS}}" {{end}}-ldflags "{{.CELL_LDFLAGS}}" -o ./bin/cell ./cmd/
- '[ "$(uname -s)" != "Darwin" ] || xattr -c ./bin/cell'
- '[ "$(uname -s)" != "Darwin" ] || codesign --force --sign - --entitlements entitlements.plist ./bin/cell'
cell:install:
aliases: [install]
desc: Build + copy ./bin/cell → ~/.local/bin/cell
dir: "{{.TASKFILE_DIR}}"
deps: [cell:build]
silent: true
cmds:
- mkdir -p ~/.local/bin
- cp ./bin/cell ~/.local/bin/cell
# Re-apply xattr/codesign at the destination — OS provenance is path-
# keyed, so the just-copied file needs its own re-registration even
# though the source was already cleaned.
- '[ "$(uname -s)" != "Darwin" ] || xattr -c ~/.local/bin/cell'
- '[ "$(uname -s)" != "Darwin" ] || codesign --force --sign - --entitlements entitlements.plist ~/.local/bin/cell'
- echo "Installed to ~/.local/bin/cell"
# ── image (CI/release aggregates) ──────────────────────────────────────
# For LOCAL development, use `cell build` — these tasks are for CI/release.
# After Modules 2.0, image:build = pure only. Impure remains opt-in for
# one release as image:impure:* and will be removed when nothing depends
# on it.
image:build:
desc: Build pure images (CI/release path). For local dev, use `cell build`.
cmds:
- task: image:pure:build
image:push:
desc: Push pure images to registry (CI). Impure path is opt-in via image:impure:push.
cmds:
- task: image:pure:push
# ── image:impure (DEPRECATED Dockerfile/bake path) ─────────────────────
# Kept for one release for callers still on the legacy build path.
# Will be removed once Modules 2.0 is the only supported workflow.
# Do not use for new local development — use `cell build` instead.
image:impure:build:
desc: "[DEPRECATED] Build legacy impure images. Use `cell build` for local dev."
cmds:
- task: image:impure:build:base
- task: image:impure:build:ultimate
image:impure:build:base:
aliases: [bake:local, image:build:base] # back-compat
desc: "[DEPRECATED] Build legacy impure base image. Use `cell build` for local dev."
silent: true
cmds:
- >-
USER_UID=$(id -u) USER_GID=$(id -g)
GIT_COMMIT={{.GIT_COMMIT_HASH}}
NIX_CACHE_IMAGE=public.ecr.aws/docker/library/debian:trixie-slim
PLATFORMS=""
docker buildx bake --file {{.TASKFILE_DIR}}/docker-bake.hcl --load local-core {{.CLI_ARGS}}
image:impure:build:ultimate:
aliases: [image:build:ultimate] # back-compat
desc: "[DEPRECATED] Build legacy impure ultimate image. Use `cell build` for local dev."
silent: true
cmds:
- >-
USER_UID=$(id -u) USER_GID=$(id -g)
GIT_COMMIT={{.GIT_COMMIT_HASH}}
NIX_CACHE_IMAGE=public.ecr.aws/docker/library/debian:trixie-slim
PLATFORMS=""
docker buildx bake --file {{.TASKFILE_DIR}}/docker-bake.hcl --load local-ultimate {{.CLI_ARGS}}
image:impure:push:
aliases: [image:push:debian] # back-compat (pre-rename name)
desc: "[DEPRECATED] Push legacy impure ci group. CI should publish pure images via image:pure:push."
silent: true
cmds:
# oci-mediatypes=true: publish with OCI manifest so zstd-compressed
# layers are legal. Without this, downstream skopeo trips:
# "unsupported docker v2s2 media type:
# application/vnd.docker.image.rootfs.diff.tar.zstd"
# because zstd is not a legal media type under Docker v2s2 (gzip only).
- >-
GIT_COMMIT={{.GIT_COMMIT_HASH}}
docker buildx bake --file {{.TASKFILE_DIR}}/docker-bake.hcl --push
--set '*.output=type=image,push=true,oci-mediatypes=true,compression=zstd,compression-level=3,force-compression=true'
ci {{.CLI_ARGS}}
# ── image: variant-agnostic utilities ──────────────────────────────────
image:mirror:
desc: "Copy a tag between registries (e.g. GHCR → ECR Public). SRC=<full-ref> DST=<full-ref>"
silent: true
cmds:
- docker buildx imagetools create -t "{{.DST}}" "{{.SRC}}"
image:manifest:
desc: "Stitch per-arch tags into multi-arch manifest. REPO= VERSION= SUFFIX=<-core|-ultimate|-base-pure|-ultimate-pure|''> ARCHES=amd64,arm64"
silent: true
vars:
ARCHES: '{{.ARCHES | default "amd64,arm64"}}'
cmds:
- |
srcs=""
for arch in $(echo "{{.ARCHES}}" | tr ',' ' '); do
srcs="$srcs {{.REPO}}:{{.VERSION}}-${arch}{{.SUFFIX}}"
done
docker buildx imagetools create \
-t "{{.REPO}}:{{.VERSION}}{{.SUFFIX}}" \
-t "{{.REPO}}:latest{{.SUFFIX}}" \
-t "{{.REPO}}:dev{{.SUFFIX}}" \
$srcs
# ── Tests ──────────────────────────────────────────────────────────────
test:unit:
desc: Run Go unit tests (cmd/, internal/) — no Docker, no nix
silent: true
dir: "{{.TASKFILE_DIR}}"
cmds:
- go test -timeout 120s ./cmd/... ./internal/... {{.CLI_ARGS}}
test:integration:
aliases: [test] # back-compat (cited in README)
desc: Run the full container integration test suite (./test/...)
silent: true
dir: "{{.TASKFILE_DIR}}"
cmds:
- 'echo "docker daemon: {{.DOCKER_SOCK}}"'
- go test -v -timeout 120s ./test/... {{.CLI_ARGS}}
# see DOCKER_SOCK in top-level vars
env:
DOCKER_HOST: '{{.DOCKER_SOCK}}'
test:vagrant:
desc: Run E2E install test in a clean Debian VM (QEMU). Requires vagrant + vagrant-qemu plugin.
dir: "{{.TASKFILE_DIR}}/test/vagrant"
cmds:
- vagrant up --provider=qemu
- defer: vagrant destroy -f
env:
CELL_VERSION: '{{.CELL_VERSION | default "0.0.0"}}'
# ── Web site ───────────────────────────────────────────────────────────
web:start:
desc: Start Astro dev server for the DevCell landing page
silent: true
dir: "{{.TASKFILE_DIR}}/web"
cmds:
- npm install
- npm run dev
# ── Cache round-trip test (top-level user control) ─────────────────────
# Runs the local proxy for the GHCR nix-cache pipeline in
# build.dev.yml. All orchestration — registry lifecycle, fixture
# build, push, pull, verify, teardown — lives in
# test/cache_roundtrip_test.go (Go test idiom). This Taskfile entry
# is just the ergonomic top-level wrapper, matching test:unit /
# test:integration / test:vagrant.
test:cache:
desc: Round-trip nix-cache pipeline locally (~1 min, requires docker + crane)
cmds:
- 'echo "docker daemon: {{.DOCKER_SOCK}}"'
- go test -v -count=1 -run TestCacheRoundtrip ./test/...
# see DOCKER_SOCK in top-level vars
env:
DOCKER_HOST: '{{.DOCKER_SOCK}}'
# ── Full Windows install via QEMU (CELL-429) ──────────────────────────
# Usage:
# task test:windows:build # runs tcg subtest (default)
# task test:windows:build ACCEL=hvf # runs hvf subtest
# task test:windows:build ACCEL="" # runs all subtests
test:windows:build:
desc: "Run the full unattended Windows install through `cell build --engine=qemu`"
platforms: [darwin]
silent: true
vars:
ACCEL: tcg
DEBUG_DIR: '{{.TASKFILE_DIR}}/.scratch/debug'
cmds:
- mkdir -p {{.DEBUG_DIR}}
- |
set -euo pipefail
env_log="{{.DEBUG_DIR}}/environment.log"
# ── Resolve every path the VM boot uses ──
qemu_bin=$(which qemu-system-aarch64 2>/dev/null || true)
qemu_real=""
qemu_prefix=""
fw=""
if [ -n "$qemu_bin" ]; then
qemu_real=$(realpath "$qemu_bin" 2>/dev/null || readlink -f "$qemu_bin" 2>/dev/null || echo "$qemu_bin")
qemu_prefix=$(dirname "$qemu_real")/..
fw="${qemu_prefix}/share/qemu/edk2-aarch64-code.fd"
fi
cache_dir="${DEVCELL_QEMU_CACHE_DIR:-$HOME/.devcell/cache/qemu}"
win_iso="${DEVCELL_TEST_WINDOWS_ISO:-${cache_dir}/windows-arm64-en-us.iso}"
virtio_iso="${DEVCELL_TEST_VIRTIO_ISO:-${cache_dir}/virtio-win.iso}"
check_file() {
local label="$1" path="$2"
if [ -f "$path" ]; then
printf " %-14s %-6s %s\n" "$label" "$(du -h "$path" 2>/dev/null | cut -f1)" "$path"
else
printf " %-14s %-6s %s\n" "$label" "MISS" "$path"
fi
}
{
echo "=== test:windows:build $(date -u +%Y%m%dT%H%M%SZ) ==="
echo "--- qemu binary ---"
echo " PATH lookup: ${qemu_bin:-NOT FOUND}"
echo " realpath: ${qemu_real:-N/A}"
[ -n "$qemu_bin" ] && qemu-system-aarch64 --version 2>&1 | head -1 | sed 's/^/ version: /'
echo ""
echo "--- VM boot file preflight ---"
check_file "firmware" "$fw"
check_file "windows-iso" "$win_iso"
check_file "virtio-iso" "$virtio_iso"
echo ""
echo "--- cache directory ---"
echo " resolved: $cache_dir"
if [ -d "$cache_dir" ]; then
ls -lh "$cache_dir" 2>/dev/null | sed 's/^/ /'
else
echo " DOES NOT EXIST"
fi
echo ""
} 2>&1 | tee "$env_log"
# ── Run the full Windows install test ──
accel="{{.ACCEL}}"
run_filter="TestCellBuildWindows_QEMU"
if [ -n "$accel" ]; then
run_filter="TestCellBuildWindows_QEMU/${accel}"
fi
log="{{.DEBUG_DIR}}/TestCellBuildWindows_QEMU.log"
echo "▸ ${run_filter} → ${log}"
build_env="DEVCELL_TEST_INSTALL=1 DEVCELL_TEST_REBUILD=1"
if env $build_env go test -run "${run_filter}" -timeout 8h -v -count=1 ./internal/vm/qemu/ 2>&1 | tee "${log}"; then
echo " ✓ PASS"
else
echo " ✗ FAIL"
exit 1
fi
# ── Nix-store GHCR cache pipeline ──────────────────────────────────────
# Stream the populated /nix volume to GHCR as a multi-layer cache image.
# `cell nix-store push` splits the tar into ~1 GB chunks (CELL-297), each
# uploaded as a separate OCI layer via stream.NewLayer — per-layer size
# stays small enough to avoid GHCR throttling / stall-abort cycles.
#
# Used by CI (build.dev.yml + build.release.yml) AND reproducible locally
# against a populated devcell-nix-store-<arch> volume.
nix-cache:publish:
desc: Publish the /nix Docker volume to GHCR as a nix-cache image
dir: "{{.TASKFILE_DIR}}"
requires:
vars: [ARCH, REGISTRY, IMAGE_NAME_LC, HASH]
vars:
STAGE: '{{.STAGE | default "publish"}}'
VOL: 'devcell-nix-store-{{.ARCH}}'
EXACT: '{{.REGISTRY}}/{{.IMAGE_NAME_LC}}:nix-cache-{{.ARCH}}-{{.HASH}}'
LATEST: '{{.REGISTRY}}/{{.IMAGE_NAME_LC}}:nix-cache-{{.ARCH}}-latest'
# gcr.io distroless static — ~2 MB, multi-arch manifest resolves
# automatically, generous anonymous pulls (no ECR-style rate limit).
BASE: 'gcr.io/distroless/static-debian12:latest'
cmds:
- |
./bin/cell nix-store push \
--volume "{{.VOL}}" \
--base "{{.BASE}}" \
--image "{{.LATEST}}" \
--tag-alias "{{.EXACT}}" \
--retries 3 \
--min-size 1GB
echo "[{{.STAGE}}] pushed nix-cache images:"
echo " {{.EXACT}}"
echo " {{.LATEST}}"
debug:
cmds:
- task: debug:macos
# ── Forced QEMU Windows autobuild with full log capture (CELL-428/429) ─────
debug:autobuild:
desc: "Forced QEMU Windows template build with full log capture → .scratch/debug/autobuild.log"
platforms: [darwin]
deps: [install]
silent: true
env:
# CELL-429 iteration 2 instrument: ships the WinPE agent on the answer
# volume plus a pre-baked one-shot diagnostic (drvload vioscsi + diskpart
# volume list → devcell-out.txt).
# H1 ($WinPEDriver$ sweep never ran): devcell-setupact.log snapshot
# shows whether wpeinit processed the driver dir.
# H2 (padForFAT broke the driver files): drivers now ship byte-exact,
# and drvload's real output/exit code lands in devcell-out.txt.
# H3 (any non-reg-add RunSynchronous aborts 0x8007000D): this run IS
# the test — the agent launcher is the vetted %l pattern; if the
# abort returns, H3 is confirmed and the agent design is dead.
DEVCELL_QEMU_WINPE_AGENT: "1"
vars:
LOG: '{{.TASKFILE_DIR}}/.scratch/debug/autobuild.log'
CACHE: '{{.HOME}}/.devcell/cache/qemu'
cmds:
- mkdir -p {{.TASKFILE_DIR}}/.scratch/debug
- |
{
echo "=== debug:autobuild $(date -u +%Y%m%dT%H%M%SZ) ==="
echo "--- cell version ---"
cell --version 2>&1 || true
echo "--- qemu ---"
command -v qemu-system-aarch64 2>&1 || true
qemu-system-aarch64 --version 2>&1 | head -1 || true
echo "--- media cache ---"
ls -la "{{.CACHE}}/" 2>&1 || true
echo "--- cached ISO volume descriptors (expect: catalog@15, BEA01@16, CD001 BRVD@17, NSR02@18, TEA01@19) ---"
for s in 15 16 17 18 19; do
printf "sector %s: " "$s"
dd if="{{.CACHE}}/windows-arm64-en-us.iso" bs=2048 skip=$s count=1 2>/dev/null | xxd -l 8 | head -1 || echo "unreadable"
done
echo "--- bootloader sidecar ---"
ls -la "{{.CACHE}}/windows-arm64-en-us.iso.bootaa64.efi" 2>&1 || echo "no sidecar (will re-master or fall back to ISO read)"
echo "--- template dir before ---"
ls -la "{{.HOME}}/.devcell/windows/ultimate/" 2>&1 || true
echo ""
} > {{.LOG}} 2>&1
- |
set -o pipefail
cell claude --engine=qemu --debug --force 2>&1 | tee -a {{.LOG}}
rc=$?
{
echo ""
echo "=== exit code: $rc ==="
echo "=== serial.log (tail 120) ==="
tail -120 "{{.TASKFILE_DIR}}/.scratch/debug/serial.log" 2>/dev/null || true
echo "=== guest-progress.log (tail 60) ==="
tail -60 "{{.TASKFILE_DIR}}/.scratch/debug/guest-progress.log" 2>/dev/null || true
echo "=== newest screenshots ==="
ls -t "{{.TASKFILE_DIR}}/.scratch/debug/screenshots/" 2>/dev/null | head -12 || true
echo "=== template dir after ==="
ls -la "{{.HOME}}/.devcell/windows/ultimate/" 2>/dev/null || true
} >> {{.LOG}} 2>&1
echo ""
echo "full log: {{.LOG}}"
exit $rc
# ── Windows ISO debugging (UUP dump download + wimlib assembly) ─────
debug:windows:
desc: "Quick Windows QEMU smoke test: build → exec echo hello world"
platforms: [darwin]
deps: [install]
silent: true
vars:
MARKER: '{{.HOME}}/.devcell/windows/ultimate/.provisioned'
LOG: '{{.TASKFILE_DIR}}/.scratch/debug/windows.log'
cmds:
- |
set -euo pipefail
mkdir -p "$(dirname "{{.LOG}}")"
_run() {
echo "=== [0/3] Pre-flight diagnostics ==="
echo " date: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo " cell: $(cell --version 2>&1 || echo 'not found')"
echo " qemu: $(qemu-system-aarch64 --version 2>/dev/null | head -1 || echo 'not found')"
echo " bunk env: DEVCELL_BUNK=${DEVCELL_BUNK:-<unset>} SESSION_PORT_PREFIX=${SESSION_PORT_PREFIX:-<unset>}"
echo " marker: {{.MARKER}} — $(test -f '{{.MARKER}}' && echo 'EXISTS' || echo 'MISSING')"
echo ""
echo " template disk:"
ls -lh {{.HOME}}/.devcell/windows/ultimate/disk-ultimate.qcow2 2>/dev/null || echo " not found"
echo ""
echo " instance disk:"
ls -lh {{.HOME}}/.devcell/DIMM/windows/disk.qcow2 2>/dev/null || echo " not found"
echo ""
echo " ports.json:"
cat {{.HOME}}/.devcell/DIMM/windows/ports.json 2>/dev/null || echo " not found"
echo ""
echo " qemu processes:"
pgrep -lf qemu-system 2>/dev/null || echo " none"
echo ""
echo " port 2222 (legacy):"
lsof -i :2222 -sTCP:LISTEN 2>/dev/null || echo " not in use"
echo ""
echo "=== [1/3] Build VM ==="
if [ -f "{{.MARKER}}" ]; then
echo "Template already provisioned — skipping build."
else
echo "Template not provisioned — building (--force in case stale disk exists)..."
cell build --engine=qemu --force --debug 2>&1
fi
echo ""
echo "=== [2/3] Post-build diagnostics ==="
echo " marker: $(test -f '{{.MARKER}}' && echo 'EXISTS' || echo 'MISSING')"
echo " template disk:"
ls -lh {{.HOME}}/.devcell/windows/ultimate/disk-ultimate.qcow2 2>/dev/null || echo " not found"
echo " qemu processes:"
pgrep -lf qemu-system 2>/dev/null || echo " none"
echo ""
echo "=== [3/3] Shell exec: echo hello world ==="
cell shell --engine=qemu --debug -- cmd /c echo hello world
}
_run 2>&1 | tee "{{.LOG}}"
# ── macOS VM debugging ────────────────────────────────────────────────
debug:macos:
desc: Mount a stopped macOS VM disk and dump diagnostics to .devcell/debug/<datetime>-macos.log
platforms: [darwin]
silent: true
vars:
CELL_NAME: '{{.CELL_NAME | default "DIMM"}}'
VM_USER: '{{.VM_USER | default .USER}}'
DISK: '{{.HOME}}/.devcell/{{.CELL_NAME}}/darwin/disk.img'
DEBUG_DIR: '{{.TASKFILE_DIR}}/.devcell/debug'
preconditions:
- sh: test -f "{{.DISK}}"
msg: "No disk image at {{.DISK}} — set CELL_NAME if not DIMM"
cmds:
- |
set -euo pipefail
mkdir -p "{{.DEBUG_DIR}}"
LOG="{{.DEBUG_DIR}}/$(date -u +%Y%m%dT%H%M%SZ)-macos.log"
DISK="{{.DISK}}"
MNT="$(mktemp -d)/devcell-inspect"
mkdir -p "$MNT"
_dump() {
echo "devcell macOS VM — offline disk diagnostic"
echo "==========================================="
echo " date: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo " cell: {{.CELL_NAME}}"
echo " user: {{.VM_USER}}"
echo " disk: $DISK"
echo ""
echo "--- attaching disk image ---"
DEVNODE=$(hdiutil attach "$DISK" -nomount -noverify 2>&1 | head -1 | awk '{print $1}')
echo " device: $DEVNODE"
diskutil list "$DEVNODE"
echo ""
DATA_PART=$(diskutil list "$DEVNODE" | grep -i Data | awk '{print $NF}')
if [ -z "$DATA_PART" ]; then
echo "ERROR: could not find Data partition on $DEVNODE"
hdiutil detach "$DEVNODE" -force 2>/dev/null
exit 1
fi
diskutil mount -mountPoint "$MNT" "/dev/$DATA_PART" >/dev/null
echo " mounted /dev/$DATA_PART at $MNT"
echo ""
# ---------- provisioning files ----------
echo "[1/12] SSH host keys (/etc/ssh/ssh_host_*)"
echo " expect: ed25519 + rsa key pairs, owned 0:0, mode 0600"
ls -ln "$MNT/private/etc/ssh/ssh_host_"* 2>/dev/null || echo " MISSING — sshd cannot start without host keys"
echo ""
echo "[2/12] sshd_config (relevant directives)"
grep -Ei '^(Port|PermitRootLogin|PubkeyAuth|PasswordAuth|UsePAM|AuthorizedKeysFile)' \
"$MNT/private/etc/ssh/sshd_config" 2>/dev/null | sed 's/^/ /' || echo " no sshd_config or no matching directives"
echo ""
echo "[3/12] LaunchDaemons (com.devcell.*)"
echo " expect: enable-sshd, serial-console, setup-skip — all owned 0:0"
ls -ln "$MNT/Library/LaunchDaemons/com.devcell."* 2>/dev/null | sed 's/^/ /' || echo " none present (cleaned up after first boot)"
echo ""
echo "[4/12] launchd disabled.plist"
echo " expect: com.openssh.sshd => false"
plutil -p "$MNT/private/var/db/com.apple.xpc.launchd/disabled.plist" 2>/dev/null | sed 's/^/ /' || echo " no disabled.plist"
echo ""
echo "[5/12] dslocal user record ({{.VM_USER}})"
echo " expect: uid=501, gid=20, shell=/bin/zsh, ShadowHashData present"
plutil -p "$MNT/private/var/db/dslocal/nodes/Default/users/{{.VM_USER}}.plist" 2>/dev/null | sed 's/^/ /' || echo " MISSING — user not provisioned"
echo ""
echo "[6/12] authorized_keys ({{.VM_USER}})"
echo " expect: one ssh-ed25519 public key, owned 501:20, mode 0600"
cat "$MNT/Users/{{.VM_USER}}/.ssh/authorized_keys" 2>/dev/null | sed 's/^/ /' || echo " no authorized_keys"
ls -ln "$MNT/Users/{{.VM_USER}}/.ssh/" 2>/dev/null | sed 's/^/ /' || echo " no .ssh dir"
echo ""
echo "[7/12] sudoers.d/{{.VM_USER}}"
echo " expect: {{.VM_USER}} ALL=(ALL) NOPASSWD: ALL, owned 0:0, mode 0440"
cat "$MNT/private/etc/sudoers.d/{{.VM_USER}}" 2>/dev/null | sed 's/^/ /' || echo " MISSING"
ls -ln "$MNT/private/etc/sudoers.d/{{.VM_USER}}" 2>/dev/null | sed 's/^/ /' || true
echo ""
echo "[8/12] .AppleSetupDone + kcpassword"
echo " expect: both present, owned 0:0"
ls -ln "$MNT/private/var/db/.AppleSetupDone" 2>/dev/null | sed 's/^/ /' || echo " .AppleSetupDone MISSING — Setup Assistant will launch"
ls -ln "$MNT/private/etc/kcpassword" 2>/dev/null | sed 's/^/ /' || echo " kcpassword MISSING — auto-login won't work"
echo ""
# ---------- runtime logs (only present after first boot) ----------
echo "[9/12] enable-sshd execution log (/var/log/devcell-sshd-enable.log)"
echo " expect: step 1-6 output after first boot"
cat "$MNT/private/var/log/devcell-sshd-enable.log" 2>/dev/null | sed 's/^/ /' || echo " no log yet (VM has not booted, or script did not run)"
echo ""
echo "[10/12] system.log — sshd / launchd / devcell entries"
grep -iE '(sshd|com\.devcell|ssh-keygen|remotelogin)' "$MNT/private/var/log/system.log" 2>/dev/null | tail -30 | sed 's/^/ /' || echo " no system.log or no matches"
echo ""
echo "[11/12] install.log — ssh entries"
grep -iE '(sshd|ssh)' "$MNT/private/var/log/install.log" 2>/dev/null | tail -10 | sed 's/^/ /' || echo " no install.log or no matches"
echo ""
echo "[12/12] /var/log/devcell-*.log (all devcell runtime logs)"
for f in "$MNT/private/var/log/devcell-"*.log; do
[ -f "$f" ] || continue
echo " --- $(basename "$f") ---"
cat "$f" | sed 's/^/ /'
done
ls "$MNT/private/var/log/devcell-"*.log >/dev/null 2>&1 || echo " none"
echo ""
# ---------- cleanup ----------
hdiutil detach "$DEVNODE" -force 2>/dev/null
rm -rf "$(dirname "$MNT")"
echo "=== done ==="
}
_dump 2>&1 | tee "$LOG"
echo ""
echo "saved to: $LOG"
# ── CD bus × accelerator matrix (CELL-429) ──────────────────────────
debug:macos:disk:
desc: "Run WinPE CD visibility tests (scsi-cd × hvf × {el2,no-el2}) on macOS"
platforms: [darwin]
silent: true
vars:
DEBUG_DIR: '{{.TASKFILE_DIR}}/.scratch/debug'
cmds:
- mkdir -p {{.DEBUG_DIR}}
- |
set -euo pipefail
env_log="{{.DEBUG_DIR}}/environment.log"
# ── Resolve every path the VM boot uses ──
# These mirror the Go helpers: requireQEMUBin, FirmwarePath,
# requireWindowsISO, requireVirtioISO (boot_test.go / download.go).
qemu_bin=$(which qemu-system-aarch64 2>/dev/null || true)
qemu_real=""
qemu_prefix=""
fw=""
if [ -n "$qemu_bin" ]; then
qemu_real=$(realpath "$qemu_bin" 2>/dev/null || readlink -f "$qemu_bin" 2>/dev/null || echo "$qemu_bin")
qemu_prefix=$(dirname "$qemu_real")/..
fw="${qemu_prefix}/share/qemu/edk2-aarch64-code.fd"
fi
cache_dir="${DEVCELL_QEMU_CACHE_DIR:-$HOME/.devcell/cache/qemu}"
win_iso="${DEVCELL_TEST_WINDOWS_ISO:-${cache_dir}/windows-arm64-en-us.iso}"
virtio_iso="${DEVCELL_TEST_VIRTIO_ISO:-${cache_dir}/virtio-win.iso}"
fw_override="${QEMU_FIRMWARE_OVERRIDE:-}"
# Helper: print file info or MISSING
check_file() {
local label="$1" path="$2"
if [ -f "$path" ]; then
printf " %-14s %-6s %s\n" "$label" "$(du -h "$path" 2>/dev/null | cut -f1)" "$path"
else
printf " %-14s %-6s %s\n" "$label" "MISS" "$path"
fi
}
{
echo "=== debug:macos:disk $(date -u +%Y%m%dT%H%M%SZ) ==="
echo "--- qemu binary ---"
echo " PATH lookup: ${qemu_bin:-NOT FOUND}"
echo " realpath: ${qemu_real:-N/A}"
echo " prefix: ${qemu_prefix:-N/A}"
[ -n "$qemu_bin" ] && qemu-system-aarch64 --version 2>&1 | head -1 | sed 's/^/ version: /'
echo ""
echo "--- VM boot file preflight ---"
echo " (mirrors Go: FirmwarePath, requireWindowsISO, requireVirtioISO)"
check_file "firmware" "$fw"
check_file "windows-iso" "$win_iso"
check_file "virtio-iso" "$virtio_iso"
[ -n "$fw_override" ] && check_file "fw-override" "$fw_override"
echo ""
echo "--- firmware fingerprint ---"
if [ -f "$fw" ]; then
echo " sha256: $(shasum -a 256 "$fw" | cut -d' ' -f1)"
strings "$fw" | grep -i 'edk2-stable\|build.*20[0-9][0-9]' | head -3 | sed 's/^/ build-tag: /' || true
fi
echo ""
echo "--- cache directory ---"
echo " DEVCELL_QEMU_CACHE_DIR=${DEVCELL_QEMU_CACHE_DIR:-<unset>}"
echo " resolved: $cache_dir"
if [ -d "$cache_dir" ]; then
ls -lh "$cache_dir" 2>/dev/null | sed 's/^/ /'
else
echo " DOES NOT EXIST"
fi
echo ""
echo "--- env overrides ---"
echo " DEVCELL_TEST_WINDOWS_ISO=${DEVCELL_TEST_WINDOWS_ISO:-<unset>}"
echo " DEVCELL_TEST_VIRTIO_ISO=${DEVCELL_TEST_VIRTIO_ISO:-<unset>}"
echo " QEMU_FIRMWARE_OVERRIDE=${QEMU_FIRMWARE_OVERRIDE:-<unset>}"
echo " DEVCELL_QEMU_EFI_KERNEL=${DEVCELL_QEMU_EFI_KERNEL:-<unset>}"
echo ""
echo "--- qemu share directory ---"
if [ -n "$qemu_prefix" ] && [ -d "${qemu_prefix}/share/qemu" ]; then
echo " ${qemu_prefix}/share/qemu:"
ls -1 "${qemu_prefix}/share/qemu/" | grep -iE 'edk2|efi|uefi|aarch64|arm' | sed 's/^/ /'
else
echo " NOT FOUND"
fi
echo ""
} 2>&1 | tee "$env_log"
# ── Test matrix: scsi-cd × {tcg,hvf} × {el2,no-el2} ──
# usb-storage disabled: EDK2 can't enumerate the answer FAT
# volume when it falls to a USB 2.0 port (XHCI port exhaustion).
# All variants run in one go test binary so subtests share a single
# timestamped results directory.
log="{{.DEBUG_DIR}}/TestWinPECDVisibility.log"
echo "▸ TestWinPECDVisibility (all variants) → ${log}"
env_args=""
[ -n "$fw_override" ] && env_args="QEMU_FIRMWARE_OVERRIDE=$fw_override"
if env $env_args go test -run "TestWinPECDVisibility/scsi-cd/hvf" -timeout 60m -v ./internal/vm/qemu/ 2>&1 | tee "${log}"; then
echo " ✓ PASS"
else
echo " ✗ FAIL"
exit 1
fi
debug:macos:winpe:
desc: "Run WinPE Hyper-V/WSL2 injection test (tcg + hvf) on macOS"
platforms: [darwin]
silent: true
vars:
DEBUG_DIR: '{{.TASKFILE_DIR}}/.scratch/debug'
cmds:
- mkdir -p {{.DEBUG_DIR}}
- |
set -euo pipefail
fw_override="${QEMU_FIRMWARE_OVERRIDE:-}"
failed=0
for sub in "tcg" "hvf"; do
slug="TestWinPEHyperVInjection-${sub}"
log="{{.DEBUG_DIR}}/${slug}.log"
echo "▸ TestWinPEHyperVInjection/${sub} → ${log}"
env_args=""
[ -n "$fw_override" ] && env_args="QEMU_FIRMWARE_OVERRIDE=$fw_override"
if env $env_args go test -run "TestWinPEHyperVInjection/${sub}" -timeout 15m -v ./internal/vm/qemu/ 2>&1 | tee "${log}"; then
echo " ✓ PASS"
else
echo " ✗ FAIL"
failed=$((failed + 1))
fi
echo ""
done
echo "=== done — ${failed} failure(s) ==="
exit $failed
# ── QEMU Windows debug VM (boot an existing debug disk) ─────────────
debug:windows:start:
desc: "Boot the Windows.utm disk with qemu-system-aarch64 — same hardware as UTM (see .scratch/UTMCommand.txt), macOS-native hvf, vmnet-shared network. Vars: DISK, EFI_VARS, SMP, MEM, ACCEL (hvf|tcg)"
silent: true
vars:
# The UTM bundle's own disk, booted in place: Windows state stays
# continuous whether the VM last ran under UTM or under this task.
DISK: '{{.DISK | default (printf "%s/test/testdata/Windows.utm/Data/D76FB0BC-D7CC-4481-A6B6-492BEB4D834B.qcow2" .TASKFILE_DIR)}}'
# UEFI variable store from the same bundle — boot entries and
# firmware state the machine was installed with.
EFI_VARS: '{{.EFI_VARS | default (printf "%s/test/testdata/Windows.utm/Data/efi_vars.fd" .TASKFILE_DIR)}}'
# UTM profile: cpus=4,sockets=1,cores=4,threads=1 and 8 GiB.
SMP: '{{.SMP | default "4"}}'
MEM: '{{.MEM | default "8192"}}'
# hvf = macOS-native virtualization, near-native speed; no guest EL2
# on stock QEMU <= 11.0, so Hyper-V/WSL2 cannot start (QEMU 11.1+ on
# M3+/macOS 15+ lifts this). tcg = full emulation: slow, but the EL3
# secure machine where the whole WSL2 chain works.
ACCEL: '{{.ACCEL | default (eq OS "darwin" | ternary "hvf" "tcg")}}'
# NESTED=0 disables guest EL2 on hvf even when QEMU supports it —
# the proven plain-virt boot (with TPM), at the cost of no WSL2.
NESTED: '{{.NESTED | default "1"}}'
# SECURE=0 boots qemu's plain EDK2 instead of UTM's Secure-Boot-
# enforcing build. Windows runs fine without Secure Boot; use this
# when the firmware refuses to launch it (signature/SB errors).
SECURE: '{{.SECURE | default "1"}}'
# tcg only: kernel-loaded EL3 firmware (hvf boots EDK2 pflash).
FIRMWARE: '{{.FIRMWARE | default (printf "%s/.devcell/cache/qemu/QEMU_EFI.kernel.fd" .HOME)}}'
# VNC console on 127.0.0.1:5907 (open with Screen Sharing) — the only
# way to see pre-OS screens like BitLocker prompts. VNC=0 disables.
VNC: '{{.VNC | default "1"}}'
# Directory shared into the guest over SMB (drive Z:). No virtiofsd
# exists on macOS and Windows speaks no 9p, so the share is served
# by a disposable samba container on the host (needs docker).
SHARE_DIR: '{{.SHARE_DIR | default .USER_WORKING_DIR}}'
preconditions:
- sh: command -v qemu-system-aarch64 >/dev/null
msg: qemu-system-aarch64 not found on PATH
- sh: test -f "{{.DISK}}"
msg: "no disk at {{.DISK}} — copy the Windows.utm bundle into test/testdata or set DISK="
cmds:
- |
set -euo pipefail
# All runtime state (pid, logs, sockets) lives in the repo-local
# .tmp/ (gitignored).
D="{{.TASKFILE_DIR}}/.tmp"; mkdir -p "$D"
PIDFILE="$D/qemu-windows.pid"
# vmnet-shared needs the com.apple.vm.networking entitlement, which
# a plain qemu binary lacks — root is the workaround UTM does not
# need (its app bundle is entitled).
SUDO=""
[ "$(uname)" = "Darwin" ] && [ "$(id -u)" != "0" ] && SUDO="sudo"
# Simple pid lock. NOTE: 'kill'/'ps' must go through env/command in
# these tasks — task's embedded shell (mvdan/sh) has a job-control
# kill builtin that silently no-ops on external pids.
if [ -f "$PIDFILE" ] && ps -p "$(cat "$PIDFILE")" >/dev/null 2>&1; then
echo "already running (pid $(cat "$PIDFILE")) — task debug:windows:stop first"
exit 1
fi
rm -f "$PIDFILE"
# The disk's own write lock catches everything else (UTM itself,
# a hand-launched qemu): fail with a pointer instead of reaping.
if ! qemu-img info "{{.DISK}}" >/dev/null 2>&1; then
echo "ERROR: {{.DISK}} is write-locked — is the VM running in UTM or another qemu?"
echo " close it there (or: ps aux | grep qemu-system) and retry"
exit 1
fi
# Hardware mirrors .scratch/UTMCommand.txt: -machine virt -cpu host,
# nvme with the bundle's serial, virtio-net with UTM's MAC on