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
|
#!/bin/sh
# mkinstall.sh - rawnix.org Netinstall Image Builder
# Run from /root/bin/ on the host build system.
#
# Requires in /root/bin/:
# vmlinuz - installer kernel (EFI stub)
# modules/ - kernel modules directory
# install - installer script
# setup-filesystem - filesystem skeleton builder
# setup-help - one-screen installation summary
# man/ - installer manual pages (installation.7,
# network.7, ..., genluks.8); see "Installer
# manual pages" below
# genfstab - fstab generator
# enter-chroot - chroot helper
# setup-boot - cmdline generator
# genluks - encrypted disk layout: format, mkfs, mount,
# keyfiles, crypttab
# ports.rsync - rsync config with token (active, no rename needed)
# *.pub - signify public keys for the trust store
#
# Usage: ./mkinstall.sh [output.img]
#
# The image boots on both firmwares, with nothing to choose: UEFI starts
# the kernel itself as EFI/BOOT/BOOTX64.EFI and never reads limine.conf;
# legacy BIOS (SeaBIOS: Hetzner Cloud, qemu without OVMF) runs Limine's
# stage 1 from the MBR, stage 2 from the 1 MiB BIOS boot partition, and
# then limine-bios.sys and limine.conf from the ESP. Needs the limine
# package built with --enable-bios on this host.
set -e
BINDIR="/root/bin"
# --bios used to select the layout that is now the only one. Accepted
# and ignored, so an old command line still builds the same image.
if [ "${1:-}" = "--bios" ]; then
shift
fi
IMG="${1:-/root/rawnix-install.img}"
WORK="/root/mkinstall"
ROOT="$WORK/rootfs"
# ── Kernel ────────────────────────────────────────────────────
# The installer kernel is a package. Build it and unpack it here:
#
# cd /usr/local/src/linux-7.2.11-img && mkpkg -is
# tar xf linux-7.2.11-img#7.2.11-1.pkg.tar.gz -C /root/
#
# which gives /root/boot/vmlinuz-7.2.11-img, /root/boot/config-7.2.11-img
# and /root/lib/modules/7.2.11-img/. All are read straight out of that
# layout -- no copying or renaming, and no second place for the version
# to be recorded.
KERNEL_ROOT="/root"
# ── Root partition UUID ───────────────────────────────────────
# This is NOT arbitrary and must not be changed on its own.
#
# The installer kernel is an EFI stub booted directly by the
# firmware: there is no bootloader on the ESP, no limine.conf, and
# therefore no way to pass a command line at boot. The kernel gets
# its root device from CONFIG_CMDLINE, compiled in as
#
# root=PARTUUID=a1b2c3d4-0000-0000-0000-000012345678
#
# Change this value without rebuilding the installer kernel and the
# image builds cleanly, boots, and panics with "unable to mount root
# fs". Change the kernel's CONFIG_CMDLINE without changing this and
# you get the same panic from the other direction. Both directions
# are now checked below, against the config shipped in the kernel
# package, before anything is built.
PARTUUID="a1b2c3d4-0000-0000-0000-000012345678"
ESP_SIZE=64 # MiB, ESP
GPT_SLACK=2 # MiB, 1 MiB alignment gap + backup GPT
# Partitions: the BIOS boot partition first, because bios-install
# records its location and Limine's own docs place it there; then the
# ESP; then root.
BIOS_SIZE=1 # MiB, BIOS boot partition (ef02): Limine stage 2
ESP_NR=2
ROOT_NR=3
LOOP=""
die() { echo "ERROR: $*" >&2; exit 1; }
# ── Cleanup ───────────────────────────────────────────────────
# Without this, any failure between losetup and losetup -d leaves
# $WORK/mnt mounted and the loop device attached. The next run's
# `rm -rf "$WORK"` then descends into the still-mounted image,
# deletes its contents, fails to rmdir the mountpoint with EBUSY,
# and aborts under set -e -- two runs away from the actual cause.
cleanup() {
if mountpoint -q "$WORK/esp" 2>/dev/null; then umount "$WORK/esp"; fi
if mountpoint -q "$WORK/mnt" 2>/dev/null; then umount "$WORK/mnt"; fi
[ -n "$LOOP" ] && losetup -d "$LOOP" 2>/dev/null
return 0
}
trap cleanup EXIT INT TERM
[ "$(id -u)" = 0 ] || die "Must run as root"
# Resolve the kernel out of the unpacked package. Extracting a
# second kernel into /root leaves two vmlinuz-* and two module
# trees; taking the newest version and then asserting the matching
# module tree exists is what stops an image shipping a bzImage and
# somebody else's modules.
KERNEL=$(ls -1 "$KERNEL_ROOT"/boot/vmlinuz-* 2>/dev/null | sort -V | tail -1)
[ -n "$KERNEL" ] && [ -f "$KERNEL" ] || \
die "No $KERNEL_ROOT/boot/vmlinuz-* — unpack the linux-installer package into $KERNEL_ROOT/ first"
KVER=${KERNEL##*/vmlinuz-}
MODULES="$KERNEL_ROOT"
[ -d "$KERNEL_ROOT/lib/modules/$KVER" ] || \
die "$KERNEL is $KVER but $KERNEL_ROOT/lib/modules/$KVER does not exist"
[ -f "$KERNEL_ROOT/lib/modules/$KVER/modules.dep" ] || \
die "$KERNEL_ROOT/lib/modules/$KVER has no modules.dep — incomplete kernel package?"
# The kernel's own compiled-in command line, from the config the
# package ships. This is the single source of truth: limine.conf below
# is generated from it (so UEFI and BIOS boots get identical arguments,
# console order and ro/rw included), and the PARTUUID is checked
# against it so the two halves of the coupling cannot drift apart.
KCONFIG="$KERNEL_ROOT/boot/config-$KVER"
[ -f "$KCONFIG" ] || die "$KCONFIG missing — the package ships the config next to vmlinuz"
KCMDLINE=$(sed -n 's/^CONFIG_CMDLINE="\(.*\)"$/\1/p' "$KCONFIG")
[ -n "$KCMDLINE" ] || die "$KCONFIG has no CONFIG_CMDLINE — the kernel has no root device"
case "$KCMDLINE" in
*"root=PARTUUID=$PARTUUID"*) ;;
*) die "CONFIG_CMDLINE ($KCMDLINE) does not carry root=PARTUUID=$PARTUUID" ;;
esac
[ -f "$BINDIR/install" ] || die "$BINDIR/install not found"
[ -f "$BINDIR/setup-filesystem" ] || die "$BINDIR/setup-filesystem not found"
[ -f "$BINDIR/setup-help" ] || die "$BINDIR/setup-help not found"
[ -f "$BINDIR/genfstab" ] || die "$BINDIR/genfstab not found"
[ -f "$BINDIR/enter-chroot" ] || die "$BINDIR/enter-chroot not found"
[ -f "$BINDIR/setup-boot" ] || die "$BINDIR/setup-boot not found"
[ -f "$BINDIR/genluks" ] || die "$BINDIR/genluks not found"
[ -f "$BINDIR/ports.rsync" ] || die "$BINDIR/ports.rsync not found"
[ -f "$BINDIR/man/installation.7" ] || die "$BINDIR/man/installation.7 not found — the image's manual pages"
# The BIOS half uses this host's limine: the host tool for bios-install
# and limine-bios.sys for the ESP. limine-bios.sys only exists when the
# package was built with --enable-bios, so it doubles as the check that
# bios-install will not refuse with "compiled without BIOS support".
command -v limine >/dev/null 2>&1 || \
die "no limine on this host (pkg add limine)"
[ -f /usr/share/limine/limine-bios.sys ] || \
die "/usr/share/limine/limine-bios.sys missing — limine was built without --enable-bios"
# The ports.rsync in the setup repo is a template with a placeholder
# token. An image built from it looks fine and fails every install at
# the first rsync ("Cannot reach rsync.rawnix.org"), so insist on a
# real token here: a token= line that is neither empty nor all X.
if ! grep -q '^token=' "$BINDIR/ports.rsync" || \
grep -q '^token=X*$' "$BINDIR/ports.rsync"; then
die "$BINDIR/ports.rsync has no real token — put the real one in before building"
fi
# The trust store is what makes a signature mean anything. Without a
# public key in the target, mkpkg's trust anchor has nothing to check a
# port's signify() block against and `pkg sysup` cannot verify what it
# downloads -- so an image with no key produces installs that can fetch
# ports but not trust them.
have_pub=""
for _pub in "$BINDIR"/*.pub; do
if [ -f "$_pub" ]; then have_pub=1; fi
done
[ -n "$have_pub" ] || die "no $BINDIR/*.pub — the image would have an empty trust store"
# A secret key in an image that gets written to USB sticks and handed
# out is unrecoverable: the only fix is a new keypair and resigning the
# whole tree. Refuse rather than rely on the glob above being careful.
for _sec in "$BINDIR"/*.sec; do
[ -e "$_sec" ] || continue
die "$_sec is in $BINDIR — move signing keys out before building an image"
done
echo "=== rawnix.org Netinstall Image Builder ==="
echo "Kernel: $KERNEL ($KVER)"
echo "Modules: $KERNEL_ROOT/lib/modules/$KVER"
echo "Cmdline: $KCMDLINE"
echo "Output: $IMG"
echo ""
# Refuse to rm -rf a work tree that still has something mounted in
# it, rather than deleting the contents of whatever that is.
for d in "$WORK/esp" "$WORK/mnt"; do
if mountpoint -q "$d" 2>/dev/null; then
die "$d is still mounted from a previous run — umount it first"
fi
done
rm -rf "$WORK"
mkdir -p "$ROOT"
# ============================================================
# Step 1: Filesystem skeleton
# ============================================================
echo ">>> Setting up filesystem skeleton..."
sh "$BINDIR/setup-filesystem" "$ROOT"
# ============================================================
# Step 2: Install core packages into live rootfs
# ============================================================
# addpkg's stderr is NOT discarded here. It is where the
# "reserved group X is missing from $ROOT — broken filesystem
# skeleton" warning comes out, which is the one diagnostic that
# catches setup-filesystem and mkpkg's reserved_groups[] having
# drifted apart. Failures are collected and the build aborts at
# the end of the step rather than producing an image that builds,
# reports success and does not boot.
echo ">>> Installing core packages into live rootfs..."
failed=""
done_pkgs=" "
add_one() {
_name="$1"; _pkg="$2"
case "$done_pkgs" in *" $_name "*) return 0 ;; esac
echo " + $_name"
if ! addpkg -f -r "$ROOT" "$_pkg"; then
failed="$failed $_name"
fi
done_pkgs="$done_pkgs$_name "
}
add_from() {
_dir="$1"; _name="$2"; _required="$3"
_pkg=$(ls -t "$_dir/$_name"/*.pkg.tar.gz 2>/dev/null | head -1)
if [ -f "$_pkg" ]; then
add_one "$_name" "$_pkg"
elif [ "$_required" = required ]; then
die "no built package for $_name in $_dir"
fi
}
# Order matters for the first two.
#
# `for portdir in /ports/core/*/` is alphabetical, which puts dhcpcd
# (10th) ahead of musl (11th) and shadow (12th). Every package with
# a groups= line that lands in that window fails twice:
#
# addpkg: useradd dhcpcd failed (exit 127) <- no shadow yet
# addpkg: install -d /var/lib/dhcpcd failed (127) <- coreutils is
# installed, but /lib/ld-musl-x86_64.so.1 is not, so nothing
# in the target can exec at all
#
# Both are exit 127 and both are silent about the real cause. musl
# first so the target can run its own binaries, shadow second so
# useradd/groupadd exist before any groups= is processed.
echo ">>> Installing bootstrap packages first..."
for boot_pkg in musl shadow; do
add_from /ports/core "$boot_pkg" required
done
for portdir in /ports/core/*/; do
[ -d "$portdir" ] || continue
name=$(basename "$portdir")
case "$name" in
openresolv) continue ;;
esac
pkg=$(ls -t "$portdir"*.pkg.tar.gz 2>/dev/null | head -1)
[ -f "$pkg" ] || continue
add_one "$name" "$pkg"
done
# udevd is systemd-udev; its libsystemd-shared needs libucontext,
# which lives in libs/ and is not in /ports/core. Without it the
# hwdb trigger dies at addpkg time with
#
# Error loading shared library libucontext.so.1
#
# and the image ships with no hwdb at all. Install the library
# before the package whose trigger needs it.
echo ">>> Installing extra packages..."
# libucontext: udevd's libsystemd-shared needs it, and udevd's hwdb
# trigger runs at addpkg time.
# json-c: cryptsetup links it. The installer now formats LUKS volumes,
# so a cryptsetup that exits on startup is a failed install, not a
# missing extra.
for libpkg in libucontext json-c libusb; do
add_from /usr/ports/libs "$libpkg" required
done
# which: `command -v` covers most of it, but enough of the tooling and
# enough muscle memory expects `which` that leaving it out costs more
# than the binary does.
# iputils: ping. Note it needs either a setuid bit or a
# net.ipv4.ping_group_range that includes the caller -- as root in the
# installer neither matters, but it is why ping is not free elsewhere.
# xfsprogs, btrfs-progs: genluks offers xfs and btrfs, so the mkfs for
# each has to be here or the option is a trap -- it parses, root is
# formatted, and the run dies at "mkfs.xfs: not found" with half a disk
# done. Mounting one still needs the driver in the installer kernel.
#
# nfs-utils: a shared /home or /data over NFS is common enough that not
# being able to mount one during an install is a real gap. Same
# caveat: CONFIG_NFS_FS in the installer kernel is what makes it work.
#
# mandoc: the installation guide is a set of manual pages (Step 4b),
# read with `man installation`. less, above, is its pager.
#
# wireless-regdb: regulatory.db and regulatory.db.p7s for cfg80211. The
# kernel is built with CFG80211_REQUIRE_SIGNED_REGDB, so without them
# wifi comes up on the restrictive world regulatory domain (no 5 GHz
# channels where the world domain forbids them, no country settings).
for optpkg in udevd openntpd which less pciutils usbutils iputils xfsprogs btrfs-progs nfs-utils mandoc iw wireless-regdb; do
add_from /usr/ports/opt "$optpkg" required
done
[ -z "$failed" ] || die "addpkg failed for:$failed"
# ============================================================
# Step 3: Kernel modules into live rootfs
# ============================================================
# The installer kernel is modular (CONFIG_MODULES=y): cfg80211 is built
# in, mac80211 and iwlwifi (iwldvm, iwlmvm, iwlmld) are modules. udev
# loads them on the MODALIAS uevent, which needs modules.dep and the
# alias maps, so the whole tree is copied and depmod is run again
# against the image: the package ran it for the build host's paths, and
# running it here is what guarantees the maps match what is shipped.
#
# The modules are signed with the key generated for this one kernel
# build, so they only load into the vmlinuz they came with -- which is
# why mkinstall.sh insists on the matching lib/modules/$KVER above.
echo ">>> Installing kernel modules..."
mkdir -p "$ROOT/lib/modules"
if [ -d "$MODULES/lib/modules" ] && [ -n "$(ls -A "$MODULES/lib/modules" 2>/dev/null)" ]; then
cp -a "$MODULES"/lib/modules/* "$ROOT/lib/modules/"
for moddir in "$ROOT/lib/modules"/*/; do
[ -d "$moddir" ] || continue
kver=$(basename "$moddir")
echo " + depmod $kver"
depmod -b "$ROOT" "$kver" || warn_depmod=1
done
[ -z "$warn_depmod" ] || echo " ! depmod reported problems"
else
die "$MODULES/lib/modules is empty — the -img kernel is modular, wifi would not load"
fi
# ============================================================
# Step 3a: Firmware
# ============================================================
# CONFIG_EXTRA_FIRMWARE is empty in the installer kernel, and firmware
# compression is off (FW_LOADER_COMPRESS=n), so everything here must be
# the plain uncompressed file. These devices are probed well after
# root is mounted, so FW_LOADER reads them from /lib/firmware the
# ordinary way, and a missing file is a card that does not come up, not
# a failed kernel build.
#
# The list is not kept by hand. It is what this kernel asks for:
#
# - the firmware= lines of every .ko in lib/modules/$KVER (iwlwifi,
# iwldvm, iwlmvm, iwlmld), and
# - the <driver>.firmware= lines of modules.builtin.modinfo, which
# covers the built-in drivers (r8169's rtl_nic files, ...).
#
# A kernel update therefore changes the list with no edit here.
#
# iwlwifi lists only the newest API of each family it knows
# (iwlwifi-so-a0-gf-a0-89.ucode) and, at run time, falls back through
# lower numbers until one exists. linux-firmware may be older than the
# kernel and carry only -83 and below. So for every requested
# <family>-<N>.ucode the newest file <= N is taken, which is exactly
# the one the kernel would end up loading; older API revisions the
# kernel would never reach first are left out. Anything that is not a
# numbered ucode (.pnvm, rtl_nic/*.fw) is matched by its exact path, or
# by file name when the kernel names it without a directory and
# linux-firmware keeps it in a subdirectory (intel/iwlwifi/).
#
# regulatory.db and regulatory.db.p7s come from the wireless-regdb
# package above, not from here.
#
# FIRMWARE_RE adds files by hand: an extended regular expression
# matched against the path under lib/firmware/, alternatives separated
# by |, for a device whose driver does not declare its firmware.
#
# Source: the linux-firmware package first, since it is versioned and
# does not require the firmware tree on the build host; the host's own
# /lib/firmware as a fallback.
echo ">>> Installing firmware..."
FIRMWARE_RE=''
command -v modinfo >/dev/null 2>&1 || \
die "no modinfo on this host (kmod) — cannot derive the firmware list"
KMODDIR="$KERNEL_ROOT/lib/modules/$KVER"
fwpkg=$(ls -t /usr/ports/opt/linux-firmware/*.pkg.tar.gz 2>/dev/null | head -1)
if [ -n "$fwpkg" ] && [ -f "$fwpkg" ]; then
fwsrc=pkg
# tar -tv, not -t: linux-firmware keeps compatibility links at the
# old paths (lib/firmware/iwlwifi-*.ucode -> intel/iwlwifi/...), and
# those old paths are the names the kernel asks for. Extracting
# only the link leaves it dangling, so every link is recorded with
# its resolved target (fw.links: name <TAB> target) and the target
# is added to the selection below.
: > "$WORK/fw.links"
tar -tvf "$fwpkg" 2>/dev/null | awk -v avail="$WORK/fw.avail" -v links="$WORK/fw.links" '
{
t = substr($1, 1, 1)
if (t == "d") next
name = ""
for (i = 1; i <= NF; i++)
if ($i ~ /^(\.\/)?lib\/firmware\//) { name = $i; k = i; break }
if (name == "") next
sub(/^\.\//, "", name)
print name > avail
if (t == "l" && $(k + 1) == "->") tgt = $(k + 2)
else if (t == "h" && $(k + 1) == "link" && $(k + 2) == "to") tgt = $(k + 3)
else next
sub(/^\.\//, "", tgt)
if (t == "l") {
if (tgt ~ /^\//) sub(/^\//, "", tgt)
else { d = name; sub(/\/[^\/]*$/, "", d); tgt = d "/" tgt }
}
n = split(tgt, a, "/"); m = 0
for (i = 1; i <= n; i++) {
if (a[i] == "" || a[i] == ".") continue
if (a[i] == "..") { if (m > 0) m--; continue }
o[++m] = a[i]
}
r = o[1]
for (i = 2; i <= m; i++) r = r "/" o[i]
print name "\t" r > links
}' || true
[ -f "$WORK/fw.avail" ] || : > "$WORK/fw.avail"
elif [ -d /lib/firmware ]; then
fwsrc=host
echo " . no linux-firmware package, using the host's /lib/firmware"
( cd /lib/firmware && find . \( -type f -o -type l \) | sed 's#^\./#lib/firmware/#' ) \
> "$WORK/fw.avail" || true
: > "$WORK/fw.links"
else
fwsrc=none
: > "$WORK/fw.avail"
: > "$WORK/fw.links"
fi
# What the kernel will ask for. Names with a glob character are
# skipped: modinfo prints them for drivers that load a family of files,
# and there is no way to know which one a given card wants.
{
find "$KMODDIR" -name '*.ko' -exec modinfo -F firmware {} + 2>/dev/null
tr '\0' '\n' < "$KMODDIR/modules.builtin.modinfo" | \
sed -n 's/^[^=]*\.firmware=//p'
} | grep -v '[*?[]' | sort -u > "$WORK/fw.want"
awk -v avail="$WORK/fw.avail" '
BEGIN {
while ((getline p < avail) > 0) {
path[p] = 1
b = p; sub(/^.*\//, "", b)
all[b] = all[b] " " p
if (b ~ /-[0-9]+\.ucode$/) {
s = b; sub(/-[0-9]+\.ucode$/, "", s)
v = b; sub(/\.ucode$/, "", v); sub(/^.*-/, "", v)
fam[s] = fam[s] " " v "|" p
}
}
}
{
b = $0; sub(/^.*\//, "", b)
if (b ~ /-[0-9]+\.ucode$/) {
s = b; sub(/-[0-9]+\.ucode$/, "", s)
v = b; sub(/\.ucode$/, "", v); sub(/^.*-/, "", v)
n = split(fam[s], e, " "); best = -1; bp = ""
for (i = 1; i <= n; i++) {
split(e[i], kv, "|")
if (kv[1] + 0 <= v + 0 && kv[1] + 0 > best) { best = kv[1] + 0; bp = kv[2] }
}
if (bp == "") print "X " $0
else {
print "M " bp
if (best != v + 0) print "O " $0 " -> " bp
}
next
}
if (("lib/firmware/" $0) in path) print "M lib/firmware/" $0
else if (index($0, "/") == 0 && (b in all)) {
n = split(all[b], e, " ")
for (i = 1; i <= n; i++) print "M " e[i]
}
else print "X " $0
}' "$WORK/fw.want" > "$WORK/fw.sel"
sed -n 's/^M //p' "$WORK/fw.sel" > "$WORK/fw.members"
if [ -n "$FIRMWARE_RE" ]; then
grep -E "^lib/firmware/($FIRMWARE_RE)\$" "$WORK/fw.avail" \
>> "$WORK/fw.members" || true
fi
# A selected link is useless without its target, and a selected real
# file is useless if the kernel asks for it under a link name: add the
# target of every selected link (following chains) and every link that
# points at a selected file.
awk -F'\t' '
NR == FNR { lnk[$1] = $2; rev[$2] = rev[$2] " " $1; next }
{
print
p = $0
for (i = 0; i < 5 && (p in lnk); i++) { p = lnk[p]; print p }
n = split(rev[$0], r, " ")
for (i = 1; i <= n; i++) print r[i]
}' "$WORK/fw.links" "$WORK/fw.members" | sort -u > "$WORK/fw.members.all"
mv "$WORK/fw.members.all" "$WORK/fw.members"
fw_want=$(wc -l < "$WORK/fw.want")
fw_sel=$(wc -l < "$WORK/fw.members")
fw_old=$(grep -c '^O ' "$WORK/fw.sel" || true)
fw_miss=$(grep -c '^X ' "$WORK/fw.sel" || true)
case "$fwsrc" in
pkg)
if [ -s "$WORK/fw.members" ]; then
tar -xf "$fwpkg" -C "$ROOT" -T "$WORK/fw.members" || \
echo " ! tar reported errors extracting firmware"
echo " + from $(basename "$fwpkg")"
fi
;;
host)
while read -r fw; do
mkdir -p "$(dirname "$ROOT/$fw")"
cp "/$fw" "$ROOT/$fw"
chmod 644 "$ROOT/$fw"
done < "$WORK/fw.members"
;;
esac
echo " kernel asks for $fw_want names; $fw_sel files selected (links and their targets)"
if [ "$fw_old" -gt 0 ]; then
echo " $fw_old requested ucode newer than linux-firmware has (older API used):"
sed -n 's/^O / /p' "$WORK/fw.sel" | head -n 10
fi
if [ "$fw_miss" -gt 0 ]; then
echo " ! $fw_miss requested files are not in linux-firmware (those devices"
echo " ! will run without them or not at all):"
sed -n 's/^X / /p' "$WORK/fw.sel" | head -n 15
fi
# Every selected path must exist in the image, and -e follows links, so
# a link whose target did not come along is counted here.
fw_bad=0
: > "$WORK/fw.bad"
while read -r _m; do
[ -e "$ROOT/$_m" ] && continue
fw_bad=$((fw_bad + 1))
echo "$_m" >> "$WORK/fw.bad"
done < "$WORK/fw.members"
if [ "$fw_bad" -gt 0 ]; then
echo " ! $fw_bad selected firmware paths are missing or dangling in the image:"
head -n 10 "$WORK/fw.bad" | sed 's/^/ /'
fi
fw_n=$(find "$ROOT/lib/firmware" \( -type f -o -type l \) 2>/dev/null | wc -l)
fw_iwl=$(find "$ROOT/lib/firmware" \( -type f -o -type l \) -name 'iwlwifi-*' 2>/dev/null | wc -l)
echo " $fw_n files in the image, $fw_iwl iwlwifi, $(du -sm "$ROOT/lib/firmware" 2>/dev/null | cut -f1)M"
if [ "$fw_iwl" -eq 0 ] || [ "$fw_bad" -gt 0 ]; then
die "firmware is incomplete (see above) — Intel wifi would not come up in this image"
fi
# ============================================================
# Step 4: Copy installer scripts to /bin
# ============================================================
echo ">>> Copying installer scripts to /bin..."
for script in install setup-filesystem setup-help genfstab enter-chroot setup-boot genluks; do
cp "$BINDIR/$script" "$ROOT/bin/$script"
chmod 755 "$ROOT/bin/$script"
done
# ============================================================
# Step 4 (cont.): Installer manual pages
# ============================================================
# The installation guide: installation(7) and the pages it links to,
# plus one page per installer script. Each file goes to the section
# directory its suffix names, installation.7 -> man7/.
#
# makewhatis builds the index behind `man -k` and apropos. `man
# <name>` finds a page by its file name without it, so a failure is
# reported and not fatal. It runs inside the rootfs, with the image's
# own mandoc, so the index matches the man that reads it.
echo ">>> Installing manual pages..."
for page in "$BINDIR"/man/*.[1-9]; do
[ -f "$page" ] || continue
_sec=${page##*.}
mkdir -p "$ROOT/usr/share/man/man$_sec"
cp "$page" "$ROOT/usr/share/man/man$_sec/"
chmod 644 "$ROOT/usr/share/man/man$_sec/${page##*/}"
echo " + ${page##*/}"
done
if chroot "$ROOT" env PATH=/sbin:/bin:/usr/sbin:/usr/bin makewhatis; then
echo " + makewhatis"
else
echo " ! makewhatis failed — man -k will not work, man <name> will"
fi
# ============================================================
# Step 4a: Autologin on the installer console
# ============================================================
# The installer is a single-user appliance with no root password, so
# the login prompt asks for a name, accepts any answer of "root", and
# protects nothing. Autologin removes the step.
#
# This patches the rootfs copy, not the runit package, so installed
# systems are unaffected -- only the image gets it.
#
# The tty name is read out of each existing agetty run script rather
# than assumed: the runit port names these services, and a rename here
# would silently produce an image with no console at all. ttyS0 is
# included when the port ships it, which is what makes `qemu -nographic`
# and a serial console on the VPS usable.
# The runit port ships agetty-ttyS0 as well (2.3.1-2 onwards), enabled,
# with a guard that sleeps when no UART is behind the port. So nothing
# is created here any more; a missing service means an older runit,
# and the image would have no serial console -- worth a loud warning on
# a VPS, where that is the only console.
echo ">>> Checking serial getty..."
if [ -f "$ROOT/etc/sv/agetty-ttyS0/run" ]; then
echo " = agetty-ttyS0 shipped by runit"
else
echo " ! agetty-ttyS0 missing — runit older than 2.3.1-2? no serial console in this image"
fi
echo ">>> Enabling root autologin on the installer console..."
tty_found=""
for run in "$ROOT"/etc/sv/*/run; do
[ -f "$run" ] || continue
grep -q agetty "$run" || continue
if grep -q -e --autologin -e ' -a ' "$run"; then
echo " = $(basename "$(dirname "$run")") already has autologin"
tty_found="$tty_found $(basename "$(dirname "$run")")"
continue
fi
# Insert the flag into the existing command rather than writing a
# new one. A rewritten run script loses whatever wrapper the runit
# port put in front of agetty -- `chpst -P`, `setsid` -- and without
# a new session agetty never becomes the controlling terminal, so
# the shell comes up with:
#
# bash: cannot set terminal process group (271)
# bash: no job control on this shell
#
# which is a working login with no ^C, no ^Z and no fg/bg. It also
# loses the tty name, the baud list and the TERM the port chose.
sed -i 's#agetty[[:space:]]#& --autologin root #' "$run"
grep -q -- --autologin "$run" || { echo " ! $(basename "$(dirname "$run")"): could not insert flag, left alone"; continue; }
echo " + $(basename "$(dirname "$run")") -> autologin root"
tty_found="$tty_found $(basename "$(dirname "$run")")"
done
[ -n "$tty_found" ] || echo " ! no agetty services found in $ROOT/etc/sv — image will have no console"
# Enable the serial getty. Without a link in the runsvdir the service
# exists but never runs, so a serial console shows the kernel and stage
# 1 and then goes silent at the point you actually want a shell -- on a
# VPS or under `qemu -nographic` that is the whole console.
#
# Linking rather than creating: the run script is the runit port's, with
# whatever wrapper and baud list it chose.
# Which directory runsvdir actually watches is the runit port's choice,
# and guessing wrong fails silently: the link is created, nothing
# supervises it, and the console is simply absent at boot. So follow
# the tty1 service -- whatever directory already holds a working getty
# link is by definition the live one.
#
# Found the hard way: this linked into /etc/runit/runsvdir/default while
# the live directory was /service, and the symptom was a boot that
# reached stage 2 and then went quiet.
SVDIR=""
for d in "$ROOT/service" "$ROOT/etc/runit/runsvdir/default" "$ROOT/var/service"; do
[ -d "$d" ] || continue
for l in "$d"/*tty1*; do
if [ -L "$l" ] || [ -d "$l" ]; then SVDIR="$d"; break; fi
done
[ -n "$SVDIR" ] && break
done
if [ -z "$SVDIR" ]; then
echo " ! no runsvdir holding a tty getty — serial getty not enabled"
else
echo " . service dir: ${SVDIR#$ROOT}"
for svc in "$ROOT"/etc/sv/*ttyS0*; do
[ -d "$svc" ] || continue
_n=$(basename "$svc")
# -L not -e: the link points at /etc/sv/... which is only valid
# once the image is booted, so -e is false on a link that exists.
if [ -L "$SVDIR/$_n" ] || [ -e "$SVDIR/$_n" ]; then
echo " = $_n already enabled"
else
ln -s "/etc/sv/$_n" "$SVDIR/$_n"
echo " + $_n enabled on serial"
fi
done
fi
# login refuses root on a tty that is not listed here, even with
# --autologin: agetty execs `login -f root`, which still consults
# securetty. The runit port ships tty1..tty6 services, so the skeleton
# lists those and nothing else -- a serial getty comes up, respawns and
# never gives a prompt.
if [ -f "$ROOT/etc/securetty" ]; then
for t in ttyS0 ttyS1; do
if grep -qx "$t" "$ROOT/etc/securetty"; then
echo " = $t already in securetty"
else
echo "$t" >> "$ROOT/etc/securetty"
echo " + $t added to securetty"
fi
done
else
echo " ! no $ROOT/etc/securetty — root may be refused on serial"
fi
# ============================================================
# Step 4a: Nothing networked starts by itself
# ============================================================
# The ports ship their own runsvdir links, and an installer that brings
# up dhcpcd, wpa_supplicant and ntpd on its own is worse than one that
# does not:
#
# - dhcpcd rewrites /etc/resolv.conf from the lease. openresolv is
# not in the image, so its own hook runs, and a lease carrying no
# domain_name_servers leaves the file empty -- then the installer
# dies on rsync with "check your network connection" while the link
# is up and the route is fine.
# - wpa_supplicant with no configuration sits scanning, and dhcpcd
# behind it loops carrier lost / carrier acquired forever, which
# reads like a driver fault and is not one.
# - ntpd steps the clock underneath a build that is already running.
#
# Started by hand, each one tells you what it is doing. setup-help
# carries the commands. Removing the link and not the service keeps
# `sv up <name>` available for anyone who wants it supervised.
echo ">>> Removing auto-start links (network and clock are manual)..."
_pruned=0
for _d in "$ROOT/service" "$ROOT/etc/runit/runsvdir/default" "$ROOT/var/service"; do
[ -d "$_d" ] || continue
for _pat in ntpd openntpd dhcpcd wpa_supplicant; do
# $_pat and $_pat-* : the ports name per-interface instances
# dhcpcd-eth0 / wpa_supplicant-wlan0 as well as the bare form.
for _l in "$_d/$_pat" "$_d/$_pat"-*; do
[ -L "$_l" ] || [ -d "$_l" ] || continue
# rm on a symlink removes the link, never its target, so
# /etc/sv/<name> survives for `sv up`.
rm -rf "$_l"
echo " - ${_l#$ROOT}"
_pruned=$((_pruned + 1))
done
done
done
[ "$_pruned" -gt 0 ] || echo " . none were linked"
# ============================================================
# Step 4b: Trust store
# ============================================================
# Public keys only, and only the top level: a per-collection key at
# keys/<collection>/<name>.pub is trusted for that collection alone,
# which is a scoping decision the image has no business making for the
# installed system.
echo ">>> Installing trust store..."
mkdir -p "$ROOT/etc/ports/keys"
for pub in "$BINDIR"/*.pub; do
[ -f "$pub" ] || continue
cp "$pub" "$ROOT/etc/ports/keys/"
chmod 644 "$ROOT/etc/ports/keys/$(basename "$pub")"
echo " + $(basename "$pub")"
done
# ============================================================
# Step 5: MOTD
# ============================================================
cat > "$ROOT/etc/motd" << 'EOF'
_
_ __ __ ___ ___ __ (_)_ __
| '__/ _` \ \ /\ / / '_ \| \ \/ /
| | | (_| |\ V V /| | | | |> <
|_| \__,_| \_/\_/ |_| |_|_/_/\_\
Welcome to rawnix Netinstaller
https://rawnix.org
Type man rawnix to learn what rawnix is.
Type man installation for the installation guide.
Type setup-help for a one-screen summary.
Type cat /etc/motd to see this message again.
EOF
# ============================================================
# Step 6: Verify the rootfs can actually boot
# ============================================================
# The input checks above are thorough; without this the output is
# not checked at all. A missing /sbin/init costs a full build, a
# dd to USB and a reboot to discover.
echo ">>> Verifying rootfs..."
[ -x "$ROOT/sbin/init" ] || [ -L "$ROOT/sbin/init" ] || \
die "no /sbin/init in rootfs — is runit installed?"
[ -x "$ROOT/bin/sh" ] || [ -L "$ROOT/bin/sh" ] || \
die "no /bin/sh in rootfs"
[ -x "$ROOT/bin/bash" ] || die "no /bin/bash in rootfs (enter-chroot needs it)"
[ -x "$ROOT/bin/addpkg" ] || [ -x "$ROOT/usr/bin/addpkg" ] || \
die "no addpkg in rootfs — the installer cannot run"
[ -s "$ROOT/etc/passwd" ] || die "empty /etc/passwd in rootfs"
[ -n "$(find "$ROOT/etc/ports/keys" -name '*.pub' -print -quit)" ] || \
die "no public key in rootfs trust store"
[ -z "$(find "$ROOT/etc/ports/keys" -name '*.sec' -print -quit)" ] || \
die "a .sec key reached the rootfs — do not ship this image"
# An installer that cannot run cryptsetup discovers it at luksFormat
# time, after the user has already destroyed the partition table.
[ -x "$ROOT/sbin/cryptsetup" ] || [ -x "$ROOT/bin/cryptsetup" ] || \
die "no cryptsetup in rootfs — encrypted installs will not work"
# The motd points at `man installation`; an image without man, or
# without the page, greets the user with a command that fails.
[ -x "$ROOT/usr/bin/man" ] || [ -x "$ROOT/bin/man" ] || \
die "no man in rootfs — is mandoc installed?"
[ -f "$ROOT/usr/share/man/man7/installation.7" ] || \
die "no installation(7) in rootfs"
[ -f "$ROOT/lib/modules/$KVER/modules.dep" ] || \
die "no modules.dep for $KVER in rootfs — udev could not load iwlwifi"
[ -f "$ROOT/lib/modules/$KVER/modules.alias.bin" ] || \
die "no modules.alias.bin for $KVER in rootfs — depmod did not run"
# Not fatal: wifi still works on the world regulatory domain, but it is
# worth knowing before the image is on a stick.
for _rd in regulatory.db regulatory.db.p7s; do
if [ -f "$ROOT/lib/firmware/$_rd" ] || [ -f "$ROOT/usr/lib/firmware/$_rd" ]; then
continue
fi
echo " ! $_rd not in rootfs — is wireless-regdb installing it elsewhere?"
done
echo " ok"
# ============================================================
# Step 7: Build disk image
# ============================================================
# Sizing. `du -sm` gives the payload; the filesystem takes its cut
# before any of it lands. On the previous fixed "+32" margin a
# 114M rootfs got a 144M partition, of which ext4 spent 4M on the
# journal, 9M on inode tables and 7.2M on the 5% root reserve --
# leaving 8M of real headroom, not 30M.
#
# So: scale the margin with the payload, and pass -m 0 to mkfs.
# Reserved blocks exist to keep a root shell usable on a full
# system disk; on a read-mostly installer image they are dead space.
echo ">>> Measuring rootfs..."
ROOTFS_SIZE=$(du -sm "$ROOT" | cut -f1)
ROOT_PART=$(( ROOTFS_SIZE * 5 / 4 + 32 ))
IMG_SIZE=$(( BIOS_SIZE + ESP_SIZE + ROOT_PART + GPT_SLACK ))
echo " rootfs: ${ROOTFS_SIZE}M"
echo " root part: ${ROOT_PART}M"
echo " image: ${IMG_SIZE}M"
echo ">>> Creating disk image..."
dd if=/dev/zero of="$IMG" bs=1M count="$IMG_SIZE" status=progress
# type=uefi and type=linux are sfdisk's aliases for the two GPT type
# GUIDs. They were spelled out in full here and the ESP one was wrong
# in three places at once -- close enough to read as correct, and the
# result was an image that booted from an NVRAM entry naming the
# partition directly but was invisible to any firmware doing a
# spec-compliant removable-media scan for a partition of type ESP.
# A 36-character constant that is wrong by one character still looks
# plausible; an alias cannot be.
# sfdisk has no alias for the BIOS boot type, so it is the GUID here,
# and asserted below like the ESP.
sfdisk "$IMG" << SFDISKEOF
label: gpt
size=${BIOS_SIZE}M, type=21686148-6449-6E6F-744E-656564454649
size=${ESP_SIZE}M, type=uefi
type=linux, uuid=$PARTUUID
SFDISKEOF
# Assert it rather than trust it: this is the failure that costs an
# hour because everything else succeeds.
_esptype=$(sfdisk --part-type "$IMG" "$ESP_NR")
[ "$_esptype" = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B" ] || \
die "partition $ESP_NR is type $_esptype, not an EFI System Partition"
_biostype=$(sfdisk --part-type "$IMG" 1)
[ "$_biostype" = "21686148-6449-6E6F-744E-656564454649" ] || \
die "partition 1 is type $_biostype, not a BIOS boot partition"
LOOP=$(losetup --show -fP "$IMG")
echo " loop: $LOOP"
# Partition nodes are created by udev, not by losetup, so they can
# lag behind the ioctl by enough to lose a race on a loaded box.
i=0
while [ ! -b "${LOOP}p${ESP_NR}" ] || [ ! -b "${LOOP}p${ROOT_NR}" ]; do
i=$((i + 1))
[ "$i" -gt 10 ] && die "partition nodes for $LOOP never appeared"
sleep 1
done
mkfs.fat -F 32 -n BOOT "${LOOP}p${ESP_NR}"
mkdir -p "$WORK/esp"
mount "${LOOP}p${ESP_NR}" "$WORK/esp"
mkdir -p "$WORK/esp/EFI/BOOT"
cp "$KERNEL" "$WORK/esp/EFI/BOOT/BOOTX64.EFI"
# BIOS: Limine reads only FAT, and looks for limine-bios.sys and
# limine.conf in the root of a partition it can read -- the ESP. The
# entry boots the same file UEFI does: an EFI stub kernel is a plain
# bzImage with a PE header, and Limine's linux protocol reads the
# bzImage header, so the kernel is stored once. The command line is
# the kernel's own CONFIG_CMDLINE, read from the package's config above:
# the compiled-in line is placed in front of whatever the bootloader
# passes, so repeating it here changes nothing -- and cannot disagree
# with it about ro/rw or the order of the console= arguments.
cp /usr/share/limine/limine-bios.sys "$WORK/esp/limine-bios.sys"
cat > "$WORK/esp/limine.conf" << LIMINECONF
# Generated by mkinstall.sh. Read only under BIOS: UEFI boots
# EFI/BOOT/BOOTX64.EFI directly and never opens this file.
timeout: 0
/rawnix installer
protocol: linux
path: boot():/EFI/BOOT/BOOTX64.EFI
cmdline: $KCMDLINE
LIMINECONF
umount "$WORK/esp"
mkfs.ext4 -m 0 -L rawnix-inst "${LOOP}p${ROOT_NR}"
mkdir -p "$WORK/mnt"
mount "${LOOP}p${ROOT_NR}" "$WORK/mnt"
echo ">>> Copying rootfs to image..."
# "$ROOT"/. rather than "$ROOT"/* so top-level dotfiles are not
# silently dropped. The skeleton creates none today; that is not a
# property worth depending on.
cp -a "$ROOT"/. "$WORK/mnt"/
echo ">>> Copying ports.rsync..."
mkdir -p "$WORK/mnt/etc/ports"
# cp + chmod, not `install -m600`: $BINDIR/install is the netinstaller
# and is copied to /bin/install in the image, so a bare `install` is
# ambiguous at best on the build host and wrong inside the image.
# 600 because it carries the rsync token and the image is handed out.
cp "$BINDIR/ports.rsync" "$WORK/mnt/etc/ports/ports.rsync"
chmod 600 "$WORK/mnt/etc/ports/ports.rsync"
echo ">>> Filesystem usage:"
df -h "$WORK/mnt" | tail -1 | awk '{printf " %s used of %s (%s), %s free\n", $3, $2, $5, $4}'
sync
umount "$WORK/mnt"
losetup -d "$LOOP"
LOOP=""
# Stage 1 into the MBR boot code, stage 2 into partition 1. After the
# loop device is detached, so nothing holds a stale view of the image
# while its first sector is rewritten.
echo ">>> Installing BIOS boot code..."
limine bios-install "$IMG" 1
echo ""
echo "=== Done ==="
echo "Image: $IMG ($(du -h "$IMG" | cut -f1))"
echo ""
echo "Write to USB:"
echo " dd if=$IMG of=/dev/sdX bs=4M status=progress"
echo ""
echo "Boots on UEFI and on legacy BIOS. A target installed under BIOS"
echo "needs its own boot code afterwards; see bootloader(7), Legacy BIOS."
|