Repository navigation
Expand file tree
/
Copy pathMakefile
More file actions
1394 lines (1267 loc) · 74.3 KB
/
Copy pathMakefile
File metadata and controls
1394 lines (1267 loc) · 74.3 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
# **************************************************************************** #
# #
# ::: :::::::: #
# Makefile :+: :+: :+: #
# +:+ +:+ +:+ #
# By: dlesieur <dlesieur@student.42.fr> +#+ +:+ +#+ #
# +#+#+#+#+#+ +#+ #
# Created: Invalid date by ut down the #+# #+# #
# Updated: 2026/08/29 16:54:29 by dlesieur ### ########.fr #
# #
# **************************************************************************** #
# ============================================================================ #
# =========@@ Config @@=========================================================
# Most defaults below come from born2root.toml at the repo root, read right
# after the shell is chosen (see "born2root.toml" further down): VM_NAME,
# BACKEND, SIZE_B2B, VM_RAM_MB, PROFILE, FEATURES, AI_MODE and LOGIN.
#
# BACKEND: which hypervisor executes the VM. The GUEST is identical either way
# -- same preseeded ISO, same LUKS+LVM layout, same b2b-setup.sh, same first
# boot -- so this only decides what runs the machine, never what is inside it.
# auto pick whatever this machine can actually do, and ask only when
# both are available and there is a terminal to ask on
# virtualbox the original path; needs the vboxdrv kernel module (root)
# qemu KVM; needs no module, only access to /dev/kvm, so it works as
# an ordinary user on machines where VirtualBox cannot
# ── Which shell interprets the scripts ──────────────────────────────────────
# make parses the Makefile; the .sh files it calls are interpreted by a shell.
# Run them with the shell you launched make FROM -- hellish when that is your
# shell -- and fall back to bash. (The scripts' own shebang is
# `#!/usr/bin/env hellish`, for when they are started by hand; make never
# consults it, so a host without hellish still builds the VM under bash.)
# The launcher is make's parent process; a
# candidate is used only if it can run the scripts' bash-isms (arrays, local,
# BASH_SOURCE) and says so (BASH_VERSION -- zsh passes the rest of the probe
# and then fails on the first unmatched glob), so `make all` can never pick a
# shell that would choke on them.
# Override with `make ... SCRIPT_SH=/path/to/shell`. Exported, so recursive
# makes and the scripts themselves inherit the same choice without re-probing.
ifeq ($(origin SCRIPT_SH),undefined)
# No shell takes part in finding the launcher. GNU make execs a $(shell ...)
# line that has no shell metacharacters directly, so `cat /proc/self/stat` is
# cat itself, and its 4th field is make's pid; make's own stat gives its
# parent; that parent's comm is the launcher. Each candidate is then asked to
# run tools/launcher_probe.sh itself: it prints B2R_SH=<its path> only when it
# is a shell that can interpret the scripts (bash-isms, BASH_VERSION), so a
# candidate that is no shell at all leaves nothing. The launcher and the
# login shell are only tried when their name is a shell's (a `timeout`,
# an editor or a sub-make in between would otherwise print its usage).
# They are tried one at a time -- $(if) expands only the branch it takes --
# so a hellish launch never starts bash even to ask it.
# Until this line every $(shell) would otherwise have been /bin/sh -c: a run
# launched from hellish now starts no other shell at all, which is what
# tests/born2root_shell_audit.sh in the hellish tree checks from the outside.
_b2r_ppid := $(word 4,$(shell cat /proc/$(word 4,$(shell cat /proc/self/stat))/stat))
_b2r_launcher := $(shell cat /proc/$(_b2r_ppid)/comm)
_b2r_login := $(notdir $(shell printenv SHELL))
_b2r_shells := hellish hellish.real bash zsh dash sh ksh mksh ash busybox
_b2r_try = $(if $(filter $(_b2r_shells),$(notdir $(1))),$(filter B2R_SH=%,$(shell $(1) tools/launcher_probe.sh)))
_b2r_found := $(call _b2r_try,$(_b2r_launcher))
_b2r_found := $(if $(_b2r_found),$(_b2r_found),$(call _b2r_try,$(_b2r_login)))
_b2r_found := $(if $(_b2r_found),$(_b2r_found),$(call _b2r_try,hellish))
_b2r_found := $(if $(_b2r_found),$(_b2r_found),$(call _b2r_try,bash))
SCRIPT_SH := $(patsubst B2R_SH=%,%,$(firstword $(_b2r_found)))
SCRIPT_SH := $(if $(strip $(SCRIPT_SH)),$(strip $(SCRIPT_SH)),bash)
endif
export SCRIPT_SH
# The recipes themselves, too. make would otherwise run every recipe line
# under /bin/sh and only hand the scripts to $(SCRIPT_SH): the loops, the
# printf banners and the `[ ... ] && ...` glue between two scripts would be
# dash in a run that claims to be hellish. One shell for both, from here on:
# every $(shell ...) below this line, VM_PATH's included, runs under it too.
SHELL := $(SCRIPT_SH)
# ── born2root.toml: the defaults people edit ────────────────────────────────
# The knobs below take their default from born2root.toml (read through
# utils/b2b_config.sh, which never fails and never prints at this point), so
# personalising the VM is editing one file instead of remembering flags. The
# command line and the environment still win: a variable given either way has
# an origin other than `undefined`, and the file is not asked. The literal in
# each $(or) is what applies when the file does not answer -- a copy of the
# Makefile alone, as tests/test_make_pull.sh makes, has no file at all.
# B2B_CONFIG=path points every read, here and in the scripts, at another file.
# env, not a bare assignment: make 4.3 does not hand command-line variables to
# $(shell).
B2B_CONFIG_ENV := $(if $(B2B_CONFIG),env B2B_CONFIG=$(B2B_CONFIG) ,)
b2b_conf = $(shell $(B2B_CONFIG_ENV)$(SCRIPT_SH) $(CURDIR)/utils/b2b_config.sh get $(1) 2>/dev/null)
ifeq ($(origin VM_NAME),undefined)
VM_NAME := $(or $(call b2b_conf,B2B_VM_NAME),debian)
endif
ifeq ($(origin BACKEND),undefined)
BACKEND := $(or $(call b2b_conf,B2B_BACKEND),auto)
endif
ifeq ($(origin SIZE_B2B),undefined)
SIZE_B2B := $(or $(call b2b_conf,B2B_SIZE_GB),15)
endif
# VM_SIZE is SIZE_B2B spelled the way people ask for a disk: 50, 50G, 50GB or
# 50Go. It used to be no variable at all, so `make all VM_SIZE=50` built the
# 15 GB default without a word. Both spellings count, `make all VM_SIZE=50`
# and `VM_SIZE=50 make all` (the prefix form this README uses everywhere):
# reading only the command line turned the second into a 15 GB build too.
# `all` hands its sub-make VM_SIZE= so the size the picker grew the disk to is
# not overridden.
ifneq ($(filter command line environment,$(origin VM_SIZE)),)
ifneq ($(strip $(VM_SIZE)),)
VM_SIZE_GB := $(shell printf '%s\n' '$(strip $(VM_SIZE))' | sed -nE 's/^([0-9]+) *([gG]([oObB]|[iI][bB])?)?$$/\1/p')
ifeq ($(VM_SIZE_GB),)
$(error VM_SIZE=$(VM_SIZE) is not a size in GB: write VM_SIZE=50, 50G, 50GB or 50Go)
endif
ifneq ($(filter command line environment,$(origin SIZE_B2B)),)
ifneq ($(SIZE_B2B),$(VM_SIZE_GB))
$(error VM_SIZE=$(VM_SIZE) and SIZE_B2B=$(SIZE_B2B) disagree: give only one)
endif
endif
override SIZE_B2B := $(VM_SIZE_GB)
endif
endif
ifeq ($(origin VM_RAM_MB),undefined)
VM_RAM_MB := $(call b2b_conf,B2B_VM_RAM_MB)
endif
# make accepts any variable, so a misspelt knob is a silent no-op:
# `VM_RAM_DB=15000 make all` built with the toml's 20480 MB and said nothing.
# Whatever the command line or the environment defines is in .VARIABLES, so
# the knob people retype most is checked by prefix (VM_RAM_MB is the only
# VM_RAM* name in the repo; keep it that way or this misfires).
ifneq ($(filter-out VM_RAM_MB,$(filter VM_RAM%,$(.VARIABLES))),)
$(error $(filter-out VM_RAM_MB,$(filter VM_RAM%,$(.VARIABLES))) is not a knob: the guest's memory is VM_RAM_MB=<MB>)
endif
ifeq ($(origin B2B_VM_CPUS),undefined)
B2B_VM_CPUS := $(call b2b_conf,B2B_VM_CPUS)
endif
ifeq ($(origin B2B_VM_IDE_DISK_GB),undefined)
B2B_VM_IDE_DISK_GB := $(or $(call b2b_conf,B2B_VM_IDE_DISK_GB),0)
endif
ifeq ($(origin PROFILE),undefined)
PROFILE := $(or $(call b2b_conf,B2B_PROFILE),auto)
endif
# PROFILE is an install tier, never a file. `PROFILE=profiles/server.toml
# make all` downloaded and extracted the netinst (3 min) before
# feature_profile.sh refused it; a preset goes in B2B_CONFIG, so the refusal
# names that, with the file when `profiles/<PROFILE>.toml` exists.
PROFILE_PRESET := $(firstword $(wildcard $(PROFILE) profiles/$(PROFILE).toml))
ifneq ($(filter-out auto minimal standard full,$(PROFILE)),)
$(error PROFILE=$(PROFILE) is not an install tier (auto, minimal, standard or full)$(if $(PROFILE_PRESET),; a preset is passed as B2B_CONFIG=$(PROFILE_PRESET)))
endif
ifeq ($(origin FEATURES),undefined)
FEATURES := $(call b2b_conf,B2B_FEATURES)
endif
ifeq ($(origin AI_MODE),undefined)
AI_MODE := $(or $(call b2b_conf,B2B_AI_MODE),off)
endif
ifeq ($(origin LOGIN),undefined)
LOGIN := $(or $(call b2b_conf,B2B_LOGIN),$(USER))
endif
# Where the VM lives. VirtualBox remembers a VM's disk itself; for QEMU the
# last create/boot recorded it (utils/vm_path.sh remember_vm_dir), so after a
# `make all VM_PATH=/mnt/storage/qemu` no later `make qemu_*` needs VM_PATH.
# An explicit VM_PATH (command line or environment) always wins.
#
# Three sources, most specific first: the registry, then [vm] path, then the
# repo's own disk_images/. The REGISTRY BEATS THE FILE on purpose -- it records
# where a VM actually is, the file records where the next one should go, and
# letting an edit to born2root.toml win would point every qemu_* target at an
# empty directory while a built 44 GB guest sat somewhere else, unreachable and
# uncounted by `make space`. Moving an existing VM therefore means moving the
# directory AND rewriting disk_images/.vm_path.<vm>, which is what vm_path.sh
# does when a build passes VM_PATH= explicitly.
ifeq ($(origin VM_PATH),undefined)
VM_PATH := $(shell cat $(CURDIR)/disk_images/.vm_path.$(VM_NAME) 2>/dev/null || echo $(or $(call b2b_conf,B2B_VM_PATH),$(CURDIR)/disk_images))
endif
VM_SCRIPT := ./setup/install/vms/install_vm_debian.sh
ISO_BUILDER := ./generate/create_custom_iso.sh
PRESEED_FILE := preseeds/preseed.cfg.in
RM := rm -rf
VMS_ISO_TAR := vms_iso.tar
# Inception (the project that runs *inside* this VM). LOGIN (B2B_LOGIN in
# born2root.toml) drives the subject-mandated domain; SRC optionally points
# `make inception` at a host-side copy of the repo instead of cloning from
# GitHub.
DOMAIN ?= $(LOGIN).42.fr
SRC ?=
# Force rebuilding the preseed ISO even if it already exists.
# `make all` sets this automatically so the ISO always matches the latest scripts/binaries.
FORCE_ISO ?= 0
# The guest's login shell: hellish, for every account, in every build. This
# names the binary that goes into the ISO -- the upstream release `make shell`
# downloads (setup/fetch_hellish.sh), or a hellish you built yourself. It is
# not a way to choose another shell: an empty value is refused below, and
# create_custom_iso.sh refuses anything that is not hellish.
CUSTOM_SHELL_PATH ?= dist/hellish
# Which hellish release to bake in. Empty = always resolve the newest release.
# Pin a tag to freeze it (and to skip the GitHub API entirely):
# make all HELLISH_VERSION=v2.7.6
HELLISH_VERSION ?=
# Force a re-download even when the cached copy already matches:
# make shell HELLISH_REFRESH=1
HELLISH_REFRESH ?=
# Neovim provisioning (see setup/install/nvim/install_nvim.sh).
# Debian 13 ships neovim 0.10.4; kickstart.nvim's master branch is built on
# `vim.pack`, which only exists from 0.12 — so the upstream release tarball is
# installed under /opt instead of the distro package. Pinned for reproducible
# builds; NVIM_VERSION=latest tracks the newest release instead.
# make nvim NVIM_VERSION=latest
NVIM_VERSION ?=
# Which users inside the VM get a kickstart config (space separated).
NVIM_USERS ?=
# Disk size for a NEW VM, in MB. Only affects a VM being created: an existing
# disk is kept.
#
# This used to be 122880 (120GB), on the theory that a dynamically-allocated
# disk is a ceiling rather than an allocation and so costs nothing until used.
# That theory was wrong here and the bill came to 42.7GB for a guest holding a
# small fraction of it. A thin disk only stays thin while something tells it
# which blocks are free, and on a LUKS guest nothing did: dm-crypt discards
# unless the mapping is opened with allow-discards, so every block the guest
# ever touched stayed allocated forever. preseeds/b2b-setup.sh now wires TRIM
# through crypttab, lvm.conf and fstrim.timer, which is what makes the host
# file track real usage instead of high-water mark.
#
# With that fixed the virtual size means what it says: a hard ceiling on what
# this VM can cost.
#
# ONE NUMBER DRIVES EVERYTHING: SIZE_B2B, in GB (B2B_SIZE_GB in
# born2root.toml, read above). From it are derived
# - the disk (DISK_SIZE_MB),
# - the partition layout (generate/partition_recipe.sh: floors, weighted
# shares, caps from born2root.toml's volume table, the `rest` volume takes
# the remainder — preview with `make partitions`),
# - what gets installed (generate/feature_profile.sh: minimal 8-14,
# standard 15-29, full 30+ — preview with `make features`), and
# - the footprint cap `make space` enforces.
# `make all` with nothing set builds a 15GB VM, the school quota. Anything
# below 8 is refused with the reason; a feature set that would not fit the
# layout is refused before a single byte is downloaded.
# make all SIZE_B2B=50 bigger disk, more features on
# make all VM_SIZE=50G the same
# make all SIZE_B2B=10 FEATURES=+docker refused: names the size that fits
DISK_SIZE_MB ?= $(shell echo $$(( $(SIZE_B2B) * 1024 )))
# Which installs go into the guest (PROFILE and FEATURES, B2B_PROFILE and
# B2B_FEATURES in born2root.toml). auto = from SIZE_B2B (see above); FEATURES
# adds/removes single ones on top, e.g. FEATURES="+docker -pytools". Base
# features (everything Born2beRoot mandates, nvim) cannot be turned off, and
# hellish is not a feature at all: it is the shell. `make features` shows the
# resolved set and whether it fits.
# Encrypt the guest's LVM with LUKS. ON is the default and is the only mode
# that satisfies the born2root mandatory requirement — the VM you hand in must
# be built this way.
#
# LUKS=OFF builds an unencrypted guest. It exists to measure disk behaviour
# with encryption out of the way: without dm-crypt in the path, freed blocks
# are zeroes rather than ciphertext, so discard needs no crypttab/initramfs
# step and `qemu-img convert` can compact the image offline. Handy for space
# experiments, never for a submission — `make all LUKS=OFF` says so loudly.
#
# This is baked into the ISO at build time (the preseed lives inside the
# initrd), so changing it requires a new ISO. The two modes write different
# ISO filenames so a cached one is never silently reused across modes.
LUKS ?= ON
# The whole project — source, ISOs and the VM disk — must fit in this many GB.
# `make space` reports the breakdown and fails when it is exceeded; `make all`
# checks it before building anything.
#
# auto (the default) measures the source tree and adds the disk you asked for,
# so the cap moves with SIZE_B2B and with anything the picker grows the disk
# to. It replaced `SIZE_B2B + 1`, which budgeted ~350MB for everything that is
# not the disk: because a bigger disk raised the cap and the cost by the same
# 1GB, a project heavier than that was over by a fixed amount at EVERY size —
# measured 3.3GB over at SIZE_B2B=15, 30 and 50 alike — so no size the picker
# could offer would have built. Leftover VM disks stay outside the auto cap on
# purpose; they are the one reclaimable line, and a cap that absorbed them
# would stop naming them.
#
# Set a number for a hard cap, which is what a real quota is.
SPACE_BUDGET_GB ?= auto
# `make slim COMPACT=1` also rewrites the qcow2 without its unreferenced
# clusters. Off by default because it requires the VM to be stopped.
COMPACT ?= 0
# VM_RAM_MB (B2B_VM_RAM_MB): the VM's RAM in MB. Empty = 25% of host RAM
# clamped to [2048,8192], which is sized to keep the HOST responsive. Raise it
# for a local model — the 2048 floor is below what any model needs, and
# AI_MODE=local will say so.
# make re VM_RAM_MB=6144 AI_MODE=local
# B2B_VM_CPUS: the VM's cores. Empty = half the host's, clamped to [2,8].
# B2B_VM_IDE_DISK_GB (vm.ide_disk_gb): GiB of a second QEMU disk for the IDE
# sandbox's dockerd, created with the root disk and set up at first boot.
# 0 = none. VirtualBox builds ignore it.
# AI_MODE (B2B_AI_MODE): optional AI, baked into the ISO so first boot
# honours it (default: off).
# off nothing installed, nothing downloaded
# client Ollama CLI pointed at an endpoint elsewhere (10.0.2.2 = the host)
# local Ollama server + a model chosen to FIT this VM's RAM
# The model is computed, never guessed: a 27B model needs ~17GB and will be
# refused rather than left to thrash swap. See setup/install/ai/install_ai.sh.
# Normalize to absolute path so ISO builder works from any cwd. Empty is not
# "keep bash": this VM has no such mode, and colleagues' guests were built
# without hellish exactly that way.
ifneq ($(strip $(CUSTOM_SHELL_PATH)),)
CUSTOM_SHELL_PATH := $(abspath $(CUSTOM_SHELL_PATH))
else
$(error CUSTOM_SHELL_PATH is empty. hellish is the login shell of this VM, not an option: leave CUSTOM_SHELL_PATH unset (make shell fetches dist/hellish) or point it at a hellish binary)
endif
# Colours (portable — works in bash/dash/zsh)
C_RESET := \033[0m
C_BOLD := \033[1m
C_GREEN := \033[32m
C_YELLOW := \033[33m
C_BLUE := \033[34m
C_RED := \033[31m
C_CYAN := \033[36m
# =========@@ Main target @@===================================================
.PHONY: all autostart autostart_handover seat_tailnet seat_tailnet_undo secrets_push secrets_pull firewall prepare pull shell deps extpack check_system check_driver guard_config guard_host backend fix_hwe fix_app_ports gen_iso setup_vm start_vm status help \
clean fclean re poweroff list_vms prune_vms console serial_log \
list_vms_iso extract_isos push_iso pop_iso rm_disk_image bstart_vm gui_vm \
host_access host_access_undo inception verify_access verif_access fresh \
groot groot_map groot_undo \
drawnosaurus drawnosaurus_status drawnosaurus_undo \
graph_render graph_render_status graph_render_undo graph_render_install graph_render_key \
nvim excalidraw hellish_plugins shell_vm provision nvim_health global_scope devtools claude_code claude_debug ai \
var_gc edge grobase backup_install grow_root ide_disk verify_platform baas_access baas_access_status baas_access_undo \
tailscale tailnet tailnet_status tailnet_down backup backup_verify restore_drill restore datacenter funnel_up funnel_status funnel_down tenant_key seed loadtest grobase_status \
llm_host llm_select llm_status llm_stop \
qemu_install qemu_start qemu_stop qemu_status qemu_console qemu_watch verify_guest \
qemu_create qemu_grow_disk qemu_ide_disk_create qemu_kill qemu_restart qemu_reset qemu_pause qemu_resume qemu_unlock \
qemu_screenshot qemu_ssh qemu_ssh_config qemu_list qemu_monitor no_root \
space slim partitions features features_select packages _build config
# Plain `make` prints the help instead of building. Building this project means
# downloading an ISO, creating a VM and running a ~20-minute install — too much
# to kick off by accident from a bare `make`. Use `make all` to build.
.DEFAULT_GOAL := help
# The orchestrator needs to invoke make for its sub-steps, so it is handed the
# make command as an argument. That argument must NOT be written as $(MAKE):
# GNU make scans the *unexpanded* recipe text for the literal string "$(MAKE)"
# and, on finding it, runs that line even under -n / -t / -q — the recursion
# escape hatch. With $(MAKE) spelled out here, `make -n all` was not a dry run
# at all: it really executed the orchestrator, which really touched VirtualBox.
# Assigning it to another variable first leaves no "$(MAKE)" in the recipe, so
# -n behaves the way anyone typing it expects.
MAKE_BIN := $(MAKE)
# VM_PATH is checked before either pipeline runs, so a root-owned storage
# directory is reported -- and, with permission, fixed with sudo -- before the
# ISO build rather than minutes after it. See utils/vm_path.sh.
# `sudo make all` "works" and quietly builds the wrong VM: the ISO gets ROOT's
# ~/.ssh key (so `ssh b2b` asks for a password), ~/.ssh/config is root's, and
# the disk, pidfile and monitor socket end up root-owned. Nothing in the build
# needs root; when VM_PATH does, make all asks for sudo for exactly that step.
# Same check as qemu_vm.sh create/install/start (utils/vm_path.sh refuse_sudo_build).
no_root:
@$(SCRIPT_SH) utils/vm_path.sh --no-root "make all VM_PATH=$(VM_PATH)"
# `all` is a two-step: pick, then build at the size the pick settled on.
#
# The picker can GROW the disk -- that is the point of it -- and SIZE_B2B is
# not one variable but three: DISK_SIZE_MB and SPACE_BUDGET_GB are derived from
# it at PARSE time, long before any recipe runs. Setting it from inside a
# recipe would leave the ISO carrying a 16 GB feature set and the qcow2 built
# at 15. So the picker runs here, and the build is a fresh sub-make given the
# size it chose, with B2B_NO_SELECT=1 so it does not ask twice.
# The saved size wins only when it is LARGER: the picker never shrinks below
# the size asked for, so a smaller one is a selection made for another disk.
# `!=` let a .b2b-features saved at 15 turn `make all SIZE_B2B=50` into a
# 15 GB build.
all: no_root prepare
@$(SCRIPT_SH) utils/luks_mode.sh --banner "$(LUKS)" || exit 1
@VM_RAM_MB="$(VM_RAM_MB)" B2B_VM_CPUS="$(B2B_VM_CPUS)" $(SCRIPT_SH) setup/host/llm_host.sh preflight || exit 1
@SIZE_B2B="$(SIZE_B2B)" VM_RAM_MB="$(VM_RAM_MB)" B2B_VM_CPUS="$(B2B_VM_CPUS)" AI_MODE="$(AI_MODE)" \
PROFILE="$(PROFILE)" FEATURES="$(FEATURES)" SCRIPT_SH="$(SCRIPT_SH)" \
VM_PATH="$(VM_PATH)" VM_NAME="$(VM_NAME)" \
$(SCRIPT_SH) generate/feature_select.sh || exit 1
@sel=$$(sed -n 's/^B2B_SELECT_SIZE_GB=//p' .b2b-features 2>/dev/null | head -n1); \
if [ -n "$$sel" ] && [ "$$sel" -gt "$(SIZE_B2B)" ] 2>/dev/null; then \
printf "$(C_BLUE)>$(C_RESET) building at SIZE_B2B=%s, the size your selection needs\n" "$$sel"; \
else \
sel="$(SIZE_B2B)"; \
fi; \
$(MAKE_BIN) --no-print-directory _build SIZE_B2B="$$sel" VM_SIZE= B2B_NO_SELECT=1
$(if $(filter true,$(call b2b_conf,B2B_HOST_AUTOSTART)),@$(MAKE_BIN) --no-print-directory autostart || printf ' ! autostart did not finish -- the VM is built; rerun make autostart\n')
# The build proper. Never call this directly -- `make all` is the entry point;
# this exists only so the size the picker chose can be a real make variable.
# The preflight counts the IDE disk too: it is created next to the root disk.
# Ponytail: also on VirtualBox, which never creates it -- over by ide_disk_gb there.
_build:
@SPACE_BUDGET_GB="$(SPACE_BUDGET_GB)" VM_NAME="$(VM_NAME)" VM_PATH="$(VM_PATH)" \
$(SCRIPT_SH) utils/space_budget.sh --preflight "$$(( $(DISK_SIZE_MB) + $(B2B_VM_IDE_DISK_GB) * 1024 ))" || exit 1
@backend=$$(BACKEND="$(BACKEND)" $(SCRIPT_SH) setup/host/select_backend.sh "$(BACKEND)") || exit 1; \
$(SCRIPT_SH) utils/vm_path.sh "$(VM_PATH)" "$(VM_NAME)" || exit 1; \
if [ "$$backend" = "qemu" ]; then \
CUSTOM_SHELL_PATH="$(CUSTOM_SHELL_PATH)" FORCE_ISO=1 AI_MODE="$(AI_MODE)" \
DISK_SIZE_MB="$(DISK_SIZE_MB)" VM_RAM_MB="$(VM_RAM_MB)" B2B_VM_CPUS="$(B2B_VM_CPUS)" VM_NAME="$(VM_NAME)" \
VM_PATH="$(VM_PATH)" MAKE_BIN="$(MAKE_BIN)" LUKS="$(LUKS)" B2B_VM_IDE_DISK_GB="$(B2B_VM_IDE_DISK_GB)" \
SIZE_B2B="$(SIZE_B2B)" PROFILE="$(PROFILE)" FEATURES="$(FEATURES)" \
$(SCRIPT_SH) setup/host/qemu_pipeline.sh; \
qrc=$$?; \
if [ "$$qrc" = 75 ]; then \
printf "\n$(C_BLUE)>$(C_RESET) not an error -- first boot is still provisioning. Run 'make all' again later, or:\n"; \
printf " $(C_BOLD)make qemu_watch$(C_RESET) follow it live\n"; \
printf " $(C_BOLD)make qemu_console$(C_RESET) see the guest's serial console\n\n"; \
exit 0; \
elif [ "$$qrc" != 0 ]; then \
exit 1; \
fi; \
else \
$(MAKE_BIN) --no-print-directory check_driver && \
CUSTOM_SHELL_PATH="$(CUSTOM_SHELL_PATH)" FORCE_ISO=1 AI_MODE="$(AI_MODE)" \
DISK_SIZE_MB="$(DISK_SIZE_MB)" VM_RAM_MB="$(VM_RAM_MB)" B2B_VM_CPUS="$(B2B_VM_CPUS)" LUKS="$(LUKS)" \
SIZE_B2B="$(SIZE_B2B)" PROFILE="$(PROFILE)" FEATURES="$(FEATURES)" \
$(SCRIPT_SH) generate/orchestrate.sh "$(VM_NAME)" "$(MAKE_BIN)"; \
fi
# Inception's host side edits the user's browsers, desktop proxy and
# ~/.local/bin, so it is opt-in: only a profile that includes the Inception
# feature gets it. A build of anything else (a project VM, a server) leaves the
# host alone; `make host_access` is the explicit way in.
@if SIZE_B2B="$(SIZE_B2B)" DISK_SIZE_MB="$(DISK_SIZE_MB)" VM_RAM_MB="$(VM_RAM_MB)" B2B_VM_CPUS="$(B2B_VM_CPUS)" \
PROFILE="$(PROFILE)" FEATURES="$(FEATURES)" AI_MODE="$(AI_MODE)" \
$(SCRIPT_SH) generate/feature_profile.sh --has inception-data; then \
VM_NAME="$(VM_NAME)" INCEPTION_DOMAIN="$(DOMAIN)" $(SCRIPT_SH) setup/host/inception_host_access.sh; \
else \
printf "$(C_BLUE)>$(C_RESET) host access for $(DOMAIN) skipped: no Inception in this profile ($(C_BOLD)make host_access$(C_RESET) to opt in)\n"; \
fi
@VM_PATH="$(VM_PATH)" VM_NAME="$(VM_NAME)" $(SCRIPT_SH) setup/host/llm_host.sh build
# Which backend would `make all` pick right now, and why?
backend:
@BACKEND="$(BACKEND)" $(SCRIPT_SH) setup/host/select_backend.sh "$(BACKEND)" >/dev/null
# =========@@ QEMU/KVM backend @@=============================================
# The same VM, run by QEMU instead of VirtualBox. Useful on its own when you
# want to drive the phases by hand rather than through `make all`.
QEMU_ENV = VM_NAME="$(VM_NAME)" VM_PATH="$(VM_PATH)" \
DISK_SIZE_MB="$(DISK_SIZE_MB)" VM_RAM_MB="$(VM_RAM_MB)" B2B_VM_CPUS="$(B2B_VM_CPUS)" LUKS="$(LUKS)" \
B2B_VM_IDE_DISK_GB="$(B2B_VM_IDE_DISK_GB)" \
SIZE_B2B="$(SIZE_B2B)" PROFILE="$(PROFILE)" FEATURES="$(FEATURES)"
# Boots the ISO and runs the unattended install, which REFORMATS the disk.
# A qcow2 that has grown past ~1GB already holds an installed system, so
# running this on it would destroy a working VM -- the same reasoning as
# install_vm_debian.sh keeping an existing VDI. Refuse, and say how to mean it.
qemu_install:
@dir="$(VM_PATH)/$(VM_NAME)"; disk="$$dir/$(VM_NAME).qcow2"; \
sz=0; [ -f "$$disk" ] && sz=$$(stat -c %s "$$disk" 2>/dev/null || echo 0); \
installed=0; [ -f "$$dir/.installed" ] && installed=1; \
[ "$$sz" -gt 1073741824 ] && [ ! -f "$$dir/.phase" ] && installed=1; \
if [ "$$installed" = 1 ] && [ "$(FORCE_INSTALL)" != "1" ]; then \
printf "$(C_RED)✗$(C_RESET) Refusing to reinstall over an existing system.\n\n"; \
printf " disk : %s (%s MB, %s)\n" "$$disk" "$$((sz / 1048576))" \
"$$([ -f "$$dir/.installed" ] && echo "installed $$(cat "$$dir/.installed")" || echo "no stamp, size says installed")"; \
printf "\n The installer reformats the disk, so this would destroy the VM\n"; \
printf " that is already installed there.\n\n"; \
printf " $(C_BOLD)make qemu_start$(C_RESET) boot what is already there\n"; \
printf " $(C_BOLD)make qemu_install FORCE_INSTALL=1$(C_RESET) wipe it and install again\n\n"; \
exit 1; \
fi
@if [ "$(FORCE_INSTALL)" = "1" ]; then \
dir="$(VM_PATH)/$(VM_NAME)"; ide="$$dir/$(VM_NAME)-ide.qcow2"; \
if [ -f "$$ide" ]; then \
printf "$(C_YELLOW)⚠$(C_RESET) FORCE_INSTALL=1: the IDE sandbox disk $$ide exists.\n"; \
printf " The old disk's LUKS key went with the old root, so first boot will\n"; \
printf " refuse it as \"not blank\". Before the reinstall, either:\n"; \
printf " wipe it: qemu-img create -f qcow2 $$ide $(B2B_VM_IDE_DISK_GB)G\n"; \
printf " or delete it: rm -f $$ide\n"; \
printf " then run make qemu_install FORCE_INSTALL=1 again.\n\n"; \
fi; \
fi
@$(QEMU_ENV) $(SCRIPT_SH) setup/host/qemu_vm.sh create
@$(QEMU_ENV) $(SCRIPT_SH) setup/host/qemu_vm.sh install
qemu_start:
@$(QEMU_ENV) $(SCRIPT_SH) setup/host/qemu_vm.sh start
@$(QEMU_ENV) $(SCRIPT_SH) setup/host/qemu_vm.sh ssh-config
qemu_stop:
@$(QEMU_ENV) $(SCRIPT_SH) setup/host/qemu_vm.sh stop
# The VM kept up as a service of this login and `make backup` hourly
# (setup/host/autostart.sh): systemd user units when linger is on, else a
# crontab block that needs no root. `make all` ends with it when the profile
# says [host] autostart = true. autostart_handover moves a VM started from a
# terminal, which dies at logout, under cron.
autostart:
@B2B_CONFIG="$(B2B_CONFIG)" VM_NAME="$(VM_NAME)" VM_PATH="$(VM_PATH)" $(SCRIPT_SH) setup/host/autostart.sh
autostart_handover:
@B2B_CONFIG="$(B2B_CONFIG)" VM_NAME="$(VM_NAME)" VM_PATH="$(VM_PATH)" $(SCRIPT_SH) setup/host/autostart.sh --handover
# This seat on the tailnet at every login (setup/host/seat_tailnet.sh): the
# user's userspace tailscaled, ephemeral, its proxy on localhost:1055. No root.
seat_tailnet:
@B2B_CONFIG="$(B2B_CONFIG)" $(SCRIPT_SH) setup/host/seat_tailnet.sh
seat_tailnet_undo:
@B2B_CONFIG="$(B2B_CONFIG)" $(SCRIPT_SH) setup/host/seat_tailnet.sh --undo
# The team's copy of the secrets file in vault42 ([dc] vault), VAULT42_* aside.
# A pull previews; APPLY=1 writes new keys, KEYS= names the ones to replace.
secrets_push:
@B2B_CONFIG="$(B2B_CONFIG)" $(SCRIPT_SH) setup/host/team_vault.sh push $(KEYS)
secrets_pull:
@B2B_CONFIG="$(B2B_CONFIG)" B2B_VAULT_APPLY="$(APPLY)" $(SCRIPT_SH) setup/host/team_vault.sh pull $(KEYS)
qemu_status:
@$(QEMU_ENV) $(SCRIPT_SH) setup/host/qemu_vm.sh status
qemu_console:
@$(QEMU_ENV) $(SCRIPT_SH) setup/host/qemu_vm.sh console
# Re-attach the install progress tracker (Ctrl+C only ever detaches it).
qemu_watch:
@$(QEMU_ENV) $(SCRIPT_SH) setup/host/qemu_vm.sh watch
# Just the disk (qemu_install = create + install).
qemu_create:
@$(QEMU_ENV) $(SCRIPT_SH) setup/host/qemu_vm.sh create
# Grow an EXISTING, stopped VM's root qcow2 to SIZE_GB GiB (the guest half is
# grow_root). A fresh build takes its size from vm.disk_gb and needs neither.
# COLD_COPY must be set to a verified cold copy of the disk (qemu-img compare exits 0).
qemu_grow_disk:
@$(QEMU_ENV) COLD_COPY="$(COLD_COPY)" $(SCRIPT_SH) setup/host/qemu_vm.sh grow-disk "$(SIZE_GB)"
# Add the IDE sandbox disk (vm.ide_disk_gb) to an EXISTING, stopped VM; a fresh
# build gets it from qemu_create. The guest half is ide_disk.
qemu_ide_disk_create:
@$(QEMU_ENV) $(SCRIPT_SH) setup/host/qemu_vm.sh ide-disk-create
qemu_kill:
@$(QEMU_ENV) $(SCRIPT_SH) setup/host/qemu_vm.sh kill
qemu_restart:
@$(QEMU_ENV) $(SCRIPT_SH) setup/host/qemu_vm.sh restart
@$(QEMU_ENV) $(SCRIPT_SH) setup/host/qemu_vm.sh ssh-config
# Hard reset, pause and resume go through the QEMU monitor.
qemu_reset:
@$(QEMU_ENV) $(SCRIPT_SH) setup/host/qemu_vm.sh reset
qemu_pause:
@$(QEMU_ENV) $(SCRIPT_SH) setup/host/qemu_vm.sh pause
qemu_resume:
@$(QEMU_ENV) $(SCRIPT_SH) setup/host/qemu_vm.sh resume
# Type the LUKS passphrase at the guest (qemu_start does this itself).
qemu_unlock:
@$(QEMU_ENV) $(SCRIPT_SH) setup/host/qemu_vm.sh unlock
# The VGA screen, where the LUKS prompt and boot errors live.
qemu_screenshot:
@$(QEMU_ENV) $(SCRIPT_SH) setup/host/qemu_vm.sh screenshot
# A shell in the guest on the port it actually got -- or one command:
# make qemu_ssh CMD="uname -a"
#
# CMD travels in the ENVIRONMENT, never on the command line. make expands a
# recipe into a shell, so an unquoted $(CMD) was re-parsed by that shell and
# every metacharacter in it acted on the HOST:
#
# make qemu_ssh CMD="hostname; grep PRETTY /etc/os-release"
# hostname -> ran in the guest (dlesieur42)
# grep PRETTY /etc/os-release -> ran on the HOST (Ubuntu 22.04)
#
# Half the output came from the seat while looking like guest output, and
# `CMD="cd /srv && rm -rf ."` would have deleted host files. Quoting the
# expansion ("$(CMD)") only moves the problem to any CMD containing a double
# quote -- `psql -c "select 1"` is ordinary here. An exported variable is
# handed to execve as one string and that shell never parses it at all.
#
# make still eats a lone $ on its own account, before any of this: write $$
# to send one to the guest (CMD='echo $$HOME', CMD='echo $$(id -un)').
qemu_ssh: export B2B_SSH_CMD = $(CMD)
qemu_ssh:
@$(QEMU_ENV) $(SCRIPT_SH) setup/host/qemu_vm.sh ssh
qemu_ssh_config:
@$(QEMU_ENV) $(SCRIPT_SH) setup/host/qemu_vm.sh ssh-config
# Every QEMU guest on this host, whoever started it, from whatever VM_PATH.
qemu_list:
@$(QEMU_ENV) $(SCRIPT_SH) setup/host/qemu_vm.sh list
# Any QEMU monitor command: make qemu_monitor CMD="info block"
qemu_monitor:
@$(QEMU_ENV) $(SCRIPT_SH) setup/host/qemu_vm.sh monitor "$(or $(CMD),info status)"
# Prove the guest is the same whichever backend built it: partitions, LUKS,
# LVM, UFW, the policy files and the login shell all come from the preseeded
# ISO. Run it on both and diff the output.
verify_guest:
@BACKEND_LABEL="$(BACKEND)" $(SCRIPT_SH) setup/host/verify_guest_parity.sh
# Prepare everything needed for a smooth `make all` experience:
# - check + install host dependencies (VirtualBox, xorriso, gcc, libreadline-dev, …)
# - update repo (if this is a git checkout)
# - download the latest published hellish release binary
#
# There is no submodule step any more: sh42 was the only one, and its whole
# nested tree — libft, ft_malloc, philosopher, scripts — existed purely to
# compile a binary we now download. A fresh clone needs no submodules and no
# SSH key (.gitmodules used git@github.com:); the release is plain HTTPS.
prepare: deps pull shell
# NOTE on --autostash: this used to be a hand-rolled `git stash` / `git stash
# pop` pair around the pull, and the two were NOT symmetric. `git stash` on a
# CLEAN tree saves nothing and creates no entry, but the pop ran unconditionally
# -- so it popped whatever unrelated entry happened to be on top of the stack.
# A stash left over from days ago was silently applied on top of an up-to-date
# checkout, and `make all` died in conflict markers over work that was already
# committed. Reproduced deterministically: stash something, commit past it, run
# the pair on the now-clean tree, and the stale WIP is back in your files.
#
# git's own --autostash has no such gap: it stashes only when there is something
# to stash, and restores exactly what it stashed, or nothing at all.
# A pull that changes the Makefile is a pull that changes the run it is part
# of: make has already read the old file and expanded its variables, so the
# rest of `make all` would build with yesterday's defaults against today's
# scripts. Measured: DISK_SIZE_MB=14336 from the parsed Makefile met the
# pre-flight from the freshly pulled utils/space_budget.sh, and the numbers
# on screen matched neither commit. So when the pull moved HEAD and the
# Makefile is among what moved, this stops and says to run make again.
pull:
@$(SCRIPT_SH) -c '\
if [ -d .git ]; then \
before=$$(git rev-parse HEAD 2>/dev/null); \
printf "$(C_BLUE)▶$(C_RESET) Pulling latest from origin/main...\n"; \
if git pull --autostash --ff-only origin main 2>/dev/null; then \
printf "$(C_GREEN)✓$(C_RESET) Repository up to date\n"; \
else \
printf "$(C_YELLOW)⚠$(C_RESET) Fast-forward failed — merging...\n"; \
git pull --autostash origin main 2>/dev/null || \
printf "$(C_YELLOW)⚠$(C_RESET) git pull failed (working offline?)\n"; \
fi; \
if [ -n "$$(git diff --name-only --diff-filter=U)" ]; then \
printf "$(C_RED)✗$(C_RESET) your local changes conflict with what was just pulled\n"; \
git diff --name-only --diff-filter=U | sed "s/^/ /"; \
printf " Resolve the conflict markers above, then: git add <files> && make all\n"; \
printf " Your work is still in the stash too: git stash list\n"; \
exit 1; \
fi; \
after=$$(git rev-parse HEAD 2>/dev/null); \
if [ -n "$$before" ] && [ "$$before" != "$$after" ] \
&& git diff --quiet "$$before" "$$after" -- Makefile 2>/dev/null; then :; \
elif [ -n "$$before" ] && [ "$$before" != "$$after" ]; then \
printf "$(C_YELLOW)⚠$(C_RESET) the pull updated the Makefile ($${before:0:7} → $${after:0:7}).\n"; \
printf " This run was parsed from the old one, so its defaults are stale.\n"; \
printf " Run the same command again: $(C_BOLD)make $(MAKECMDGOALS)$(C_RESET)\n"; \
exit 1; \
fi; \
fi'
# Sync + update ALL submodules (any depth) to the latest upstream commit, and repair
# orphan gitlinks (submodule paths an upstream repo committed without a .gitmodules
# entry, e.g. libft's srcs/memory/ft_malloc). Fully auto-detected — see the script.
#
# NOTE: no longer part of `prepare`, and this repo now registers NO submodules
# (sh42 was removed in favour of the downloaded hellish release). Kept as
# generic machinery in case one is ever added back; it is a no-op today.
update:
@$(SCRIPT_SH) setup/update_submodules.sh
# Fetch the custom shell: download the published hellish release binary.
# This replaced a full submodule checkout + ~550-file compile. The asset is
# the same one `hellish --update` pulls, verified against its published
# SHA-256 before it is allowed near the ISO.
#
# To build from source instead, clone hellish yourself and point the ISO at it:
# git clone --recursive https://github.com/Univers42/hellish
# make -C hellish all OPT=1 # `all` matters: its default goal is `help`
# make all CUSTOM_SHELL_PATH=hellish/build/bin/hellish
shell:
@HELLISH_VERSION="$(HELLISH_VERSION)" HELLISH_REFRESH="$(HELLISH_REFRESH)" \
OUT_BIN="$(CUSTOM_SHELL_PATH)" $(SCRIPT_SH) setup/fetch_hellish.sh
# =========@@ Install host developer dependencies @@==========================
# Checks for: VirtualBox + ext-pack, xorriso, curl, gcc, libreadline-dev,
# python3, openssl (hashes born2root.toml's passwords), git, openssh-client,
# make.
# Missing packages are installed via `sudo apt install` WITHOUT -y so the
# user reviews and confirms the apt plan themselves.
deps:
@BACKEND="$(BACKEND)" $(SCRIPT_SH) setup/install/check_deps.sh
# =========@@ VirtualBox Extension Pack (optional) @@=========================
# Deliberately NOT part of `make deps` / `make all`. The pack installs into
# /usr/lib/virtualbox, so it needs root, and a sudo prompt in the middle of the
# build is a trap: sudo asks for "password for dlesieur", which is also the VM's
# username, so the VM password gets typed in and the whole build looks broken.
#
# Nothing here uses the pack either. It adds USB 2.0/3.0 passthrough, VRDP,
# NVMe, PXE boot and VDI-level disk encryption; this VM runs on NAT networking,
# a SATA disk, guest-side LUKS and a serial console. Install it only if you want
# those extras:
extpack:
@BACKEND="$(BACKEND)" INSTALL_EXTPACK=1 $(SCRIPT_SH) setup/install/check_deps.sh
# =========@@ System compatibility pre-checks @@==============================
check_system:
@$(SCRIPT_SH) -c '\
ERRORS=0; \
KERN=$$(uname -r); \
printf "$(C_BLUE)▶$(C_RESET) Pre-flight checks (running kernel: $$KERN)\n"; \
VBOX_VER=""; \
VBOX_MAJOR=0; \
if command -v VBoxManage >/dev/null 2>&1; then \
VBOX_VER=$$(VBoxManage --version 2>/dev/null | awk "/^[0-9]+\\.[0-9]+/ {print \$$1; exit}" | cut -d r -f1); \
VBOX_MAJOR=$$(printf "%s\n" "$$VBOX_VER" | awk -F. "{if (\$$1 ~ /^[0-9]+$$/) print \$$1 \$$2; else print 0}"); \
VBOX_MAJOR=$${VBOX_MAJOR:-0}; \
fi; \
HWE_PKGS=$$(dpkg -l 2>/dev/null \
| awk "/^ii.*linux-image-[0-9]/{print \$$2}" \
| grep -E "linux-image-6\.(1[3-9]|[2-9][0-9])\.|linux-image-[7-9]\." \
| tr "\n" " "); \
if [ -n "$$HWE_PKGS" ] && [ "$$VBOX_MAJOR" -lt 71 ]; then \
printf "$(C_YELLOW)⚠$(C_RESET) Incompatible HWE kernel(s) installed: $$HWE_PKGS\n"; \
printf "$(C_YELLOW) VirtualBox 7.0.x DKMS cannot build against these kernels and\n$(C_RESET)"; \
printf "$(C_YELLOW) may break entirely even when booting an older kernel.\n$(C_RESET)"; \
printf "$(C_YELLOW) Fix:$(C_RESET) make fix_hwe\n"; \
elif [ -n "$$HWE_PKGS" ]; then \
printf "$(C_GREEN)✓$(C_RESET) VirtualBox $$VBOX_VER supports installed HWE kernel(s)\n"; \
fi; \
if ! test -c /dev/vboxdrv 2>/dev/null; then \
printf "$(C_RED)✗$(C_RESET) /dev/vboxdrv missing — VirtualBox kernel driver not loaded\n"; \
ERRORS=$$((ERRORS+1)); \
if command -v dkms >/dev/null 2>&1; then \
DKMS_BAD=$$(dkms status 2>/dev/null | grep -i vbox | grep -iv installed | head -5); \
if [ -n "$$DKMS_BAD" ]; then \
printf "$(C_RED) Broken DKMS entries:$(C_RESET) $$DKMS_BAD\n"; \
printf "$(C_YELLOW) Fix:$(C_RESET) make fix_hwe\n"; \
else \
printf "$(C_YELLOW) Run:$(C_RESET) make fix_hwe\n"; \
fi; \
else \
printf "$(C_YELLOW) Run:$(C_RESET) make fix_hwe\n"; \
fi; \
else \
printf "$(C_GREEN)✓$(C_RESET) /dev/vboxdrv OK\n"; \
fi; \
if command -v code >/dev/null 2>&1; then \
if ! code --list-extensions 2>/dev/null | grep -qi "ms-vscode-remote.remote-ssh"; then \
printf "$(C_YELLOW)⚠$(C_RESET) VS Code Remote-SSH extension not installed on host\n"; \
printf "$(C_YELLOW) Fix:$(C_RESET) code --install-extension ms-vscode-remote.remote-ssh\n"; \
else \
printf "$(C_GREEN)✓$(C_RESET) VS Code Remote-SSH extension present\n"; \
fi; \
else \
printf "$(C_YELLOW)⚠$(C_RESET) code not in PATH — verify ms-vscode-remote.remote-ssh is installed\n"; \
fi; \
if [ "$$ERRORS" -gt 0 ]; then \
printf "$(C_RED)✗$(C_RESET) Pre-flight failed ($$ERRORS error(s)). Fix the above then retry.\n"; \
exit 1; \
fi; \
printf "$(C_GREEN)✓$(C_RESET) All pre-flight checks passed\n"'
# =========@@ Can THIS machine run a VM at all? @@=============================
# The VirtualBox kernel driver is per-machine state. A 42 home directory is on
# NFS and follows you between workstations; vboxdrv does not. So a clone that
# builds on one machine can be unable to start a VM on the next one with
# nothing in the repo having changed -- and the old failure mode for that was a
# green "VirtualBox ready" row followed a minute later by a wall of VBoxManage
# errors, because `VBoxManage --version` prints its "module is not loaded"
# warning on stdout and it got captured as the version string.
#
# This says so up front instead, names the machine, and never changes anything.
# make check_driver diagnose this machine
# make all SKIP_DRIVER_CHECK=1 proceed anyway (the VM start will still fail)
check_driver:
@VM_NAME="$(VM_NAME)" VM_PATH="$(VM_PATH)" \
$(SCRIPT_SH) setup/host/check_vbox_driver.sh
# =========@@ Whose VM is this? @@============================================
# disk_images/ is inside the shared home, so every workstation sees the same
# disk -- but only one of them is running the VM. install_vm_debian.sh stamps
# the owning machine into disk_images/<vm>/.built-on, and every destructive
# path (rm_disk_image, and therefore fclean / re / fresh) goes through this
# guard first, so a build started on the wrong machine cannot silently delete
# a VM that is in use on another one. Override deliberately with FORCE_HOST=1.
#
# Only a disk that actually holds an installed system (>100MB) is protected. A
# freshly created VDI is ~2MB and holds nothing, so a build that got as far as
# creating the disk and then failed does not leave a guard behind to trip over.
# A TOML error makes every value unreadable, and `get` answers empty by
# contract (the Makefile calls it at parse time for every target, `make help`
# included). Empty means the literal fallbacks above apply, so a typo anywhere
# in born2root.toml would point the destructive targets at VM_NAME=debian --
# a machine the file never named. They ask first whether it parses at all.
# Reading a knob is never gated on this: only deleting something is.
guard_config:
@$(B2B_CONFIG_ENV)$(SCRIPT_SH) utils/b2b_config.sh --parses || { \
printf "$(C_RED)✗$(C_RESET) Refusing to delete anything: the config does not parse (above), so \"$(VM_NAME)\" is a guess, not your setting.\n"; \
printf " fix the line it names, then: make config\n"; \
exit 1; \
}
guard_host:
@stamp="$(VM_PATH)/$(VM_NAME)/.built-on"; \
sz=0; \
for d in "$(VM_PATH)/$(VM_NAME)/$(VM_NAME).vdi" "$(VM_PATH)/$(VM_NAME)/$(VM_NAME).qcow2"; do \
[ -f "$$d" ] || continue; \
n=$$(stat -c %s "$$d" 2>/dev/null || echo 0); \
[ "$$n" -gt "$$sz" ] && sz=$$n; \
done; \
if [ -r "$$stamp" ] && [ "$$sz" -gt 104857600 ]; then \
owner=$$(head -n1 "$$stamp" | awk '{print $$1}'); \
me=$$(hostname -f 2>/dev/null || hostname); \
if [ -n "$$owner" ] && [ "$$owner" != "$$me" ] && [ "$(FORCE_HOST)" != "1" ]; then \
printf "$(C_RED)✗$(C_RESET) Refusing to destroy VM \"$(VM_NAME)\" — it belongs to another machine.\n\n"; \
printf " built on : %s\n" "$$(head -n1 "$$stamp")"; \
printf " you are : %s\n" "$$me"; \
printf " disk : %s MB of installed system\n" "$$((sz / 1048576))"; \
printf "\n Your home is shared over NFS, so this is the SAME disk that machine\n"; \
printf " uses. Deleting it here would destroy a working VM over there.\n\n"; \
printf " Deliberately override by adding $(C_BOLD)FORCE_HOST=1$(C_RESET) to your command,\n"; \
printf " e.g. $(C_BOLD)make re FORCE_HOST=1$(C_RESET)\n\n"; \
exit 1; \
fi; \
fi
# =========@@ Fix incompatible HWE kernel (VirtualBox DKMS) @@=================
fix_hwe:
@$(SCRIPT_SH) fixes/fix_hwe_kernel.sh
fix_app_ports:
@$(SCRIPT_SH) fixes/fix_app_nat_forwarding.sh "$(VM_NAME)"
# =========@@ Build preseeded ISO @@============================================
gen_iso: shell
@FORCE_ISO="$(FORCE_ISO)" CUSTOM_SHELL_PATH="$(CUSTOM_SHELL_PATH)" \
AI_MODE="$(AI_MODE)" LUKS="$(LUKS)" \
SIZE_B2B="$(SIZE_B2B)" DISK_SIZE_MB="$(DISK_SIZE_MB)" VM_RAM_MB="$(VM_RAM_MB)" B2B_VM_CPUS="$(B2B_VM_CPUS)" \
PROFILE="$(PROFILE)" FEATURES="$(FEATURES)" VM_PATH="$(VM_PATH)" $(SCRIPT_SH) $(ISO_BUILDER)
# =========@@ Create the VM @@==================================================
setup_vm:
@VM_NAME="$(VM_NAME)" VM_PATH="$(VM_PATH)" \
DISK_SIZE_MB="$(DISK_SIZE_MB)" VM_RAM_MB="$(VM_RAM_MB)" B2B_VM_CPUS="$(B2B_VM_CPUS)" \
$(SCRIPT_SH) $(VM_SCRIPT) "$(VM_NAME)"
# =========@@ Start an existing VM @@===========================================
start_vm: check_system
@if ! VBoxManage showvminfo "$(VM_NAME)" >/dev/null 2>&1; then \
printf "$(C_RED)✗$(C_RESET) VM \"$(VM_NAME)\" does not exist. Run: make setup_vm\n"; \
exit 1; \
fi
@VM_NAME="$(VM_NAME)" $(SCRIPT_SH) unlock_vm.sh
# Escape hatch: opens the VirtualBox window. Use when you need the console --
# to watch the installer, or to type the passphrase by hand.
gui_vm: check_system
@if ! VBoxManage showvminfo "$(VM_NAME)" >/dev/null 2>&1; then \
printf "$(C_RED)✗$(C_RESET) VM \"$(VM_NAME)\" does not exist. Run: make setup_vm\n"; \
exit 1; \
fi; \
VM_STATE=$$(VBoxManage showvminfo "$(VM_NAME)" --machinereadable 2>/dev/null | grep "^VMState=" | cut -d\" -f2); \
if [ "$$VM_STATE" = "running" ]; then \
printf "$(C_GREEN)✓$(C_RESET) VM is already running\n"; \
else \
VBoxManage startvm "$(VM_NAME)" --type gui; \
fi
# =========@@ Status @@========================================================
status:
@VM_PATH="$(VM_PATH)" $(SCRIPT_SH) generate/status.sh "$(VM_NAME)" "$(PRESEED_FILE)"
# =========@@ Serial console @@================================================
# The whole pipeline is headless, so nothing ever renders the VM's screen. The
# VM's COM1 is wired to a file instead (see setup/install/vms/install_vm_debian.sh)
# and the guest is booted with console=ttyS0, so that file is the VM's console
# as plain text: the installer's progress during `make all`, the kernel's boot
# messages afterwards.
#
# make console follow it live (Ctrl+C stops watching, not the VM)
# make serial_log print what is in it and exit
console:
@$(SCRIPT_SH) generate/serial_console.sh "$(VM_NAME)" follow
serial_log:
@$(SCRIPT_SH) generate/serial_console.sh "$(VM_NAME)" dump
# =========@@ Headless boot with unlock @@======================================
# start_vm is headless already; kept so existing habits and docs keep working.
bstart_vm: start_vm
# =========@@ Power off @@=====================================================
poweroff:
@VBoxManage controlvm $(VM_NAME) acpipowerbutton 2>/dev/null || \
VBoxManage controlvm $(VM_NAME) poweroff 2>/dev/null || \
printf "$(C_YELLOW)VM is not running$(C_RESET)\n"
# =========@@ Listing / archive helpers @@=====================================
list_vms:
@VBoxManage list vms 2>/dev/null || echo "No VMs found"
list_vms_iso:
@tar -tf $(VMS_ISO_TAR) 2>/dev/null || echo "No ISO archive found"
extract_isos:
@tar -xvf $(VMS_ISO_TAR)
push_iso:
@tar -rf $(VMS_ISO_TAR) $(NEW_ISO)
pop_iso:
@tar --exclude=$(NEW_ISO) -cf tmp_$(VMS_ISO_TAR) $(VMS_ISO_TAR) && \
mv tmp_$(VMS_ISO_TAR) $(VMS_ISO_TAR)
# =========@@ Destroy helpers @@===============================================
# A running QEMU guest is stopped first. This target only knew VirtualBox, and
# `make re` / `make fclean` then rm -rf the VM directory -- so on the QEMU
# backend they deleted the qcow2 out from under a guest still running from it.
# QEMU keeps the unlinked file open: its space is not freed and the guest keeps
# its forwarded ports, running headless with nothing on disk left to name it.
# `qemu_vm.sh stop` flushes the qcow2 and exits, prints "not running" when
# there is nothing to stop, and finds a guest started from another VM_PATH.
rm_disk_image: guard_config guard_host
@if command -v qemu-system-x86_64 >/dev/null 2>&1; then \
$(QEMU_ENV) $(SCRIPT_SH) setup/host/qemu_vm.sh stop || exit 1; \
fi
@if VBoxManage showvminfo "$(VM_NAME)" >/dev/null 2>&1; then \
state=$$(VBoxManage showvminfo "$(VM_NAME)" --machinereadable 2>/dev/null \
| grep '^VMState=' | cut -d'"' -f2); \
if [ "$$state" = "running" ] || [ "$$state" = "paused" ] || [ "$$state" = "stuck" ]; then \
printf "$(C_YELLOW)▶$(C_RESET) Powering off VM \"$(VM_NAME)\"...\n"; \
VBoxManage controlvm "$(VM_NAME)" poweroff 2>/dev/null || true; \
sleep 3; \
i=0; while [ $$i -lt 10 ]; do \
if VBoxManage modifyvm "$(VM_NAME)" --description "" >/dev/null 2>&1; then break; fi; \
sleep 1; i=$$((i+1)); \
done; \
fi; \
if VBoxManage unregistervm "$(VM_NAME)" --delete >/dev/null 2>&1; then \
printf "$(C_GREEN)✓$(C_RESET) VM \"$(VM_NAME)\" removed\n"; \
else \
printf "$(C_RED)✗$(C_RESET) Failed to unregister VM — forcing cleanup\n"; \
VBoxManage unregistervm "$(VM_NAME)" 2>/dev/null || true; \
rm -rf "$(VM_PATH)/$(VM_NAME)" 2>/dev/null || true; \
printf "$(C_GREEN)✓$(C_RESET) VM \"$(VM_NAME)\" force-removed\n"; \
fi; \
else \
echo "VM '$(VM_NAME)' does not exist."; \
fi
prune_vms:
@for vm in $$(VBoxManage list vms 2>/dev/null | awk '{print $$1}' | tr -d '"'); do \
VBoxManage unregistervm "$$vm" --delete >/dev/null 2>&1; \
done; \
printf "$(C_GREEN)✓$(C_RESET) All VMs removed\n"
# The ISOs live in $(VM_PATH)/iso (utils/vm_path.sh, iso_dir); the repo-root
# names are where builds before that left theirs.
clean:
@chmod -R u+w "$(VM_PATH)/iso" debian_iso_extract 2>/dev/null || true
$(RM) "$(VM_PATH)/iso" debian-*-amd64-netinst.iso debian-*-amd64-*preseed*.iso debian_iso_extract
# =========@@ Space @@=========================================================
# What this project costs, and whether that is still allowed. Fails (exit 1)
# when the total exceeds SPACE_BUDGET_GB rather than warning about it -- a
# warning mid-build scrolls past unread, and the point is to stop before
# writing the thing that blows the quota. See utils/space_budget.sh.
space:
@SPACE_BUDGET_GB="$(SPACE_BUDGET_GB)" VM_NAME="$(VM_NAME)" VM_PATH="$(VM_PATH)" \
$(SCRIPT_SH) utils/space_budget.sh
# born2root.toml, resolved and validated: what the guest will be called, who
# logs in, the volume table. Fails, naming each key to fix, when it is not
# valid -- the same check `make all` runs before downloading anything.
config:
@$(B2B_CONFIG_ENV)$(SCRIPT_SH) utils/b2b_config.sh --show
@$(B2B_CONFIG_ENV)$(SCRIPT_SH) utils/b2b_config.sh --check
@# [packages] apt, resolved against the mirror's index (cached a day), so a
@# typo shows up here too and the size the ISO build will charge is visible.
@pkgs=$$($(B2B_CONFIG_ENV)$(SCRIPT_SH) utils/b2b_config.sh get B2B_APT_PACKAGES); \
if [ -n "$$pkgs" ]; then \
printf '\n[packages] apt:\n'; \
env B2B_APT_MIRROR="$$($(B2B_CONFIG_ENV)$(SCRIPT_SH) utils/b2b_config.sh get B2B_MIRROR)" \
python3 utils/b2b_apt.py resolve $$pkgs | sed 's/^/ /'; \
fi
# What the guest's disk will look like for this SIZE_B2B, without building.