[PATCH v2 4/8] Adjust partition layout to support updates

Demi Marie Obenour
2025-11-12T22:18:41Z

systemd-sysupdate has strict requirements on the partition layout:

- The label of the active partition must match the template in the
.transfer file. For instance, the root filesystem of Spectrum 0.0.0
will be in a partition with label Spectrum_0.0.0.
- The label of the inactive partition must either be that of the old
version of Spectrum or "_empty". The former indicates an incomplete
update.
- The partition type UUID must conform to the Discoverable Partition
Specification.

After installing an image to a partition, systemd-sysupdate updates the
label of the partition to match the image's version. However, it does
not update the partition UUID. Therefore, use the partition label, not
the partition UUID, to find the root filesystem and its verity metadata.

systemd-sysupdate will fail if the OS image does not fit in the
partitions that the installer created. Therefor, make the partitions
very large so that there is plenty of room for the OS to grow. This
requires rewriting the code that calculates the partition sizes.

Since the partition label includes the OS version, add an OS version
number. Use 0.0.0 to indicate that Spectrum OS is still in very early
development and should not be used. The version number can be
overridden in the build configuration file.

mkfs.ext4 is not able to produce images with files large enough to hold
both the primary and backup copy of the root partition [1]. Reducing
the sizes of partitions to be little greater than the size of the root
filesystem image does not help. The produced file is still too large.
Therefore, compress the image, which causes it to be small enough that
mkfs.ext4 can handle it. This breaks the live image, so remove it.
The live image will return once Spectrum switches to the GNOME OS
installer [2].

[1]: https://github.com/tytso/e2fsprogs/issues/254
[2]: https://spectrum-os.org/lists/archives/spectrum-devel/87wm4dlkhz.fsf@alyssa.is/

Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com>
---
Documentation/development/build-configuration.adoc | 11 ++++
host/efi.nix | 5 +-
host/initramfs/Makefile | 12 ++--
host/initramfs/default.nix | 1 +
host/initramfs/etc/init | 17 ++---
host/initramfs/etc/probe | 20 +++---
host/initramfs/shell.nix | 2 +
host/rootfs/Makefile | 21 ++++---
host/rootfs/default.nix | 1 +
host/rootfs/shell.nix | 2 +
img/app/Makefile | 2 +-
img/app/default.nix | 1 +
lib/config.default.nix | 1 +
lib/config.nix | 3 +-
lib/kcmdline-utils.mk | 5 ++
release/checks/integration/try.c | 4 ++
release/checks/no-roothash.nix | 2 +-
release/combined/eosimages.nix | 14 +++--
release/combined/grub.cfg.in | 5 --
release/live/Makefile | 9 +--
release/live/default.nix | 8 ++-
release/live/shell.nix | 3 +-
scripts/format-uuid.awk | 35 +++++++++++
scripts/make-gpt.bash | 72 ++++++++++++++++++++++
scripts/make-gpt.sh | 67 +-------------------
scripts/make-live-image.sh | 43 +++++++++++++
scripts/sfdisk-field.awk | 3 +-
vm/sys/net/Makefile | 2 +-
vm/sys/net/default.nix | 1 +
29 files changed, 248 insertions(+), 124 deletions(-)

diff --git a/Documentation/development/build-configuration.adoc b/Documentation/development/build-configuration.adoc
index 545aa8c05ac40a101b5ee280015cde7ec4f3a66f..0659d104efeeb8f483c24d8ea8d38a5d928d9358 100644
--- a/Documentation/development/build-configuration.adoc
+++ b/Documentation/development/build-configuration.adoc
@@ -40,3 +40,14 @@ for supported configuration attributes and their default values.
};
}
----
+
+.config.nix to adjust the version of the OS
+[example]
+[source,nix]
+----
+{ default, ... }:
+
+{
+ version = "0.0.1";
+}
+----
diff --git a/host/efi.nix b/host/efi.nix
index a2b47fd050fbf00050473a0d5a1373eb96c341b5..7ac3782fd15de1d1313f53d53239f46bf5dcc949 100644
--- a/host/efi.nix
+++ b/host/efi.nix
@@ -4,7 +4,7 @@

import ../lib/call-package.nix (
{ bash, callSpectrumPackage, cryptsetup, runCommand
-, stdenv, systemdUkify, rootfs
+, stdenv, systemdUkify, rootfs, config
}:
let
initramfs = callSpectrumPackage ./initramfs {};
@@ -27,6 +27,7 @@ runCommand "spectrum-efi" {
KERNEL = kernel;
INITRAMFS = initramfs;
ROOTFS = rootfs;
+ VERSION = config.version;
};
} ''
read -r roothash < "$ROOTFS/rootfs.verity.roothash"
@@ -41,6 +42,6 @@ runCommand "spectrum-efi" {
--linux "$KERNEL" \
--initrd "$INITRAMFS" \
--os-release $'NAME="Spectrum"\n' \
- --cmdline "ro intel_iommu=on roothash=$roothash"
+ --cmdline "ro intel_iommu=on roothash=$roothash x-spectrum-version=$VERSION"
''
) (_: {})
diff --git a/host/initramfs/Makefile b/host/initramfs/Makefile
index 102870ecba4456303414e2531ea592473ddfc1cf..bb3cdf49407c847832a849990ba1fdca79c60c30 100644
--- a/host/initramfs/Makefile
+++ b/host/initramfs/Makefile
@@ -4,6 +4,7 @@
.POSIX:

include ../../lib/common.mk
+include ../../lib/kcmdline-utils.mk

dest = build/initramfs

@@ -35,16 +36,13 @@ build/mountpoints:
cd build/mountpoints && mkdir -p $(MOUNTPOINTS)
find build/mountpoints -mindepth 1 -exec touch -d @0 {} ';'

-build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk $(ROOT_FS_VERITY) $(ROOT_FS_VERITY_ROOTHASH) $(ROOT_FS)
- ../../scripts/make-gpt.sh $@.tmp \
- "$$ROOT_FS_VERITY:verity:$$(../../scripts/format-uuid.sh "$$(dd "if=$$ROOT_FS_VERITY_ROOTHASH" bs=32 skip=1 count=1 status=none)")" \
- $(dest):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 "$$ROOT_FS_VERITY_ROOTHASH")")
- mv $@.tmp $@
+build/live.img: $(LIVE_IMAGE_DEPS) $(ROOT_FS) $(ROOT_FS_VERITY) $(ROOT_FS_VERITY_ROOTHASH)
+ ../../scripts/make-live-image.sh live $@

build/loop.tar: build/live.img
$(TAR) -cf $@ build/live.img

-build/loop.img: ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/loop.ext4
+build/loop.img: ../../scripts/make-gpt.sh ../../scripts/make-gpt.bash ../../scripts/sfdisk-field.awk build/loop.ext4
../../scripts/make-gpt.sh $@.tmp \
build/loop.ext4:56a3bbc3-aefa-43d9-a64d-7b3fd59bbc4e
mv $@.tmp $@
@@ -58,7 +56,7 @@ run: $(dest) $(RUN_IMAGE) $(ROOT_FS_VERITY_ROOTHASH)
-machine virtualization=on \
-kernel $(KERNEL) \
-initrd $(dest) \
- -append "ro earlycon console=hvc0 intel_iommu=on roothash=$$(< "$$ROOT_FS_VERITY_ROOTHASH") nokaslr" \
+ -append "ro earlycon console=hvc0 intel_iommu=on roothash=$$(< "$$ROOT_FS_VERITY_ROOTHASH") x-spectrum-version=$$VERSION nokaslr" \
-cpu max \
-gdb unix:build/gdb.sock,server,nowait \
-parallel none \
diff --git a/host/initramfs/default.nix b/host/initramfs/default.nix
index d35e1b514ec48015f5110e65e5ae944b28244c4f..dbbe3c5489a23b454c1a27e17aa7d431713e71fa 100644
--- a/host/initramfs/default.nix
+++ b/host/initramfs/default.nix
@@ -99,6 +99,7 @@ stdenvNoCC.mkDerivation {
fileset = lib.fileset.intersection src (lib.fileset.unions [
./.
../../lib/common.mk
+ ../../lib/kcmdline-utils.mk
]);
};
sourceRoot = "source/host/initramfs";
diff --git a/host/initramfs/etc/init b/host/initramfs/etc/init
index 719488741b6d31564c2c17c0e41f15d16b1c0a08..1488916120ad4042273e328c10686601a3abfcf1 100755
--- a/host/initramfs/etc/init
+++ b/host/initramfs/etc/init
@@ -6,22 +6,16 @@ export PATH /bin

if { mount -a }

-piperw 3 4
-if { fdmove 1 4 /etc/getuuids }
-fdclose 4
-# head -1 would be clearer, but it might use buffered I/O and consume
-# too much from the fifo. Ideally we'd have line(1) from illumos.
-backtick ROOTFS_UUID { fdmove 0 3 dd count=1 bs=37 status=none }
-backtick VERITY_UUID { fdmove 0 3 dd count=1 bs=37 status=none }
-fdclose 3
-
if { mkfifo /dev/rootfs.poll }

background {
- fdclose 3
mdevd -C -b134217728
}
-importas -iu mdevd_pid !
+
+multisubstitute {
+ importas -iu mdevd_pid !
+ importas -i roothash roothash
+}

if { modprobe erofs }

@@ -36,7 +30,6 @@ background { kill $mdevd_pid }
background { rm /dev/rootfs.poll }

if {
- importas -Si roothash
veritysetup open /dev/rootfs root-verity /dev/verity $roothash
}

diff --git a/host/initramfs/etc/probe b/host/initramfs/etc/probe
index 4cbd00db52c1a7128b5c619a43d415675feaee0b..11a81c9be8f1adaef3cee17efdba1eb80e9fe3c7 100755
--- a/host/initramfs/etc/probe
+++ b/host/initramfs/etc/probe
@@ -14,9 +14,13 @@ if -n {
forx -pE module { ext4 loop }
modprobe $module
}
- backtick -E uuid { lsblk -lnpo PARTUUID $mdev }
+ backtick uuid { lsblk -lnpo PARTUUID $mdev }
+ multisubstiute {
+ define mdev_ $mdev
+ importas -Si uuid
+ }
if { mkdir -p /mnt/${uuid} }
- if { mount $mdev /mnt/${uuid} }
+ if { mount $mdev_ /mnt/${uuid} }
find /mnt/${uuid} -name *.img -exec
losetup -Pf {}
;
@@ -24,11 +28,13 @@ if -n {

# Check whether we now have all the partitions we need to boot.

-importas -i rootfs_uuid ROOTFS_UUID
-importas -i verity_uuid VERITY_UUID
-
-backtick -E rootfs_dev { findfs PARTUUID=${rootfs_uuid} }
-backtick -E verity_dev { findfs PARTUUID=${verity_uuid} }
+importas -i version x-spectrum-version
+backtick rootfs_dev { findfs PARTLABEL=Spectrum_${version} }
+backtick verity_dev { findfs PARTLABEL=Spectrum_${version}.verity }
+multisubstitute {
+ importas -iS rootfs_dev
+ importas -iS verity_dev
+}

if { ln -s $rootfs_dev /dev/rootfs }
if { ln -s $verity_dev /dev/verity }
diff --git a/host/initramfs/shell.nix b/host/initramfs/shell.nix
index fef8198685564bef2d8d673e0dc9403ee9c9a444..192b1c1de870c9049d2c32717b85706601999064 100644
--- a/host/initramfs/shell.nix
+++ b/host/initramfs/shell.nix
@@ -4,6 +4,7 @@
import ../../lib/call-package.nix (
{ callSpectrumPackage, rootfs, pkgsStatic, stdenv
, cryptsetup, jq, qemu_kvm, tar2ext4, util-linux
+, config
}:

let
@@ -20,5 +21,6 @@ initramfs.overrideAttrs ({ nativeBuildInputs ? [], env ? {}, ... }: {
ROOT_FS = "${rootfs}/rootfs";
ROOT_FS_VERITY = "${rootfs}/rootfs.verity.superblock";
ROOT_FS_VERITY_ROOTHASH = "${rootfs}/rootfs.verity.roothash";
+ VERSION = config.version;
};
})) (_: {})
diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile
index d7764d9b796f1773b4bebd0d50eec52b9be29e42..76c8ff1628454d769e09e0bc915d198fece080e0 100644
--- a/host/rootfs/Makefile
+++ b/host/rootfs/Makefile
@@ -4,6 +4,7 @@
.POSIX:

include ../../lib/common.mk
+include ../../lib/kcmdline-utils.mk
include file-list.mk

dest = build
@@ -38,9 +39,11 @@ DIRS = \
etc/s6-linux-init/run-image/vm/by-id \
etc/s6-linux-init/run-image/vm/by-name \
ext \
+ home \
proc \
run \
- sys
+ sys \
+ tmp

FIFOS = etc/s6-linux-init/run-image/service/s6-svscan-log/fifo

@@ -95,11 +98,10 @@ clean:
rm -rf build
.PHONY: clean

-build/live.img: ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk $(dest)/timestamp
- ../../scripts/make-gpt.sh $@.tmp \
- $(dest)/rootfs.verity.superblock:verity:$$(../../scripts/format-uuid.sh "$$(dd if=$(dest)/rootfs.verity.roothash bs=32 skip=1 count=1 status=none)") \
- $(dest)/rootfs:root:$$(../../scripts/format-uuid.sh "$$(head -c 32 $(dest)/rootfs.verity.roothash)")
- mv $@.tmp $@
+build/live.img: $(LIVE_IMAGE_DEPS) $(dest)/timestamp
+ ROOT_FS=$(dest)/rootfs ROOT_FS_VERITY=$(dest)/rootfs.verity.superblock \
+ ROOT_FS_VERITY_ROOTHASH=$(dest)/rootfs.verity.roothash \
+ ../../scripts/make-live-image.sh live $@

debug:
$(GDB) -q \
@@ -112,7 +114,10 @@ run: build/live.img
@set -x && \
ext="$$(mktemp build/spectrum-rootfs-extfs.XXXXXXXXXX.img)" && \
truncate -s 10G "$$ext" && \
- mkfs.btrfs "$$ext" && \
+ dir=$$(mktemp -d) && \
+ mkdir -- "$$dir/tmp" "$$dir/home" && \
+ mkfs.btrfs --rootdir "$$dir" --subvol tmp --subvol home -- "$$ext" && \
+ rm -rf -- "$$dir" && \
exec 3<>"$$ext" && \
rm -f "$$ext" && \
set +x && \
@@ -129,7 +134,7 @@ run: build/live.img
-device virtconsole,chardev=virtiocon0 \
-drive file=build/live.img,if=virtio,format=raw,readonly=on \
-drive file=/proc/self/fd/3,if=virtio,format=raw \
- -append "earlycon console=hvc0 roothash=$$(< $(dest)/rootfs.verity.roothash) intel_iommu=on nokaslr" \
+ -append "earlycon console=hvc0 roothash=$$(< $(dest)/rootfs.verity.roothash) intel_iommu=on nokaslr x-spectrum-version=$$VERSION" \
-device virtio-keyboard \
-device virtio-mouse \
-device virtio-gpu \
diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix
index c2045ad96cca37a1bf1a7b82aa35a583cc5aee93..b574b8ddf5858867156507429a55b7f537e3c485 100644
--- a/host/rootfs/default.nix
+++ b/host/rootfs/default.nix
@@ -134,6 +134,7 @@ stdenvNoCC.mkDerivation {
fileset = fileset.intersection src (fileset.unions [
./.
../../lib/common.mk
+ ../../lib/kcmdline-utils.mk
../../scripts/make-erofs.sh
]);
};
diff --git a/host/rootfs/shell.nix b/host/rootfs/shell.nix
index 6df2f575fdfc7cdf8067ccfdb5fecaad9f6ea5e6..27f93e05fce036257d27cf9992fee8c925073f80 100644
--- a/host/rootfs/shell.nix
+++ b/host/rootfs/shell.nix
@@ -5,6 +5,7 @@
import ../../lib/call-package.nix (
{ callSpectrumPackage, rootfs, pkgsStatic, srcOnly, stdenv
, btrfs-progs, cryptsetup, jq, netcat, qemu_kvm, reuse, util-linux
+, config
}:

rootfs.overrideAttrs (
@@ -20,5 +21,6 @@ rootfs.overrideAttrs (
KERNEL = "${passthru.kernel}/${stdenv.hostPlatform.linux-kernel.target}";
LINUX_SRC = srcOnly passthru.kernel.configfile;
VMLINUX = "${passthru.kernel.dev}/vmlinux";
+ VERSION = config.version;
};
})) (_: {})
diff --git a/img/app/Makefile b/img/app/Makefile
index 48eba871339d314479f730101246ace3fa39e2db..547b46c92661a900d02d2c5c8cae60f4008a7b7d 100644
--- a/img/app/Makefile
+++ b/img/app/Makefile
@@ -24,7 +24,7 @@ $(imgdir)/appvm/vmlinux: $(KERNEL)
mkdir -p $$(dirname $@)
cp $(KERNEL) $@

-$(imgdir)/appvm/blk/root.img: ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/rootfs.erofs
+$(imgdir)/appvm/blk/root.img: ../../scripts/make-gpt.sh ../../scripts/make-gpt.bash ../../scripts/sfdisk-field.awk build/rootfs.erofs
mkdir -p $$(dirname $@)
../../scripts/make-gpt.sh $@.tmp \
build/rootfs.erofs:root:5460386f-2203-4911-8694-91400125c604:root
diff --git a/img/app/default.nix b/img/app/default.nix
index d9c4389d196ab19910849f012631461626775ef8..332569315db5a6be6101e6df127f6478d9302b17 100644
--- a/img/app/default.nix
+++ b/img/app/default.nix
@@ -98,6 +98,7 @@ stdenvNoCC.mkDerivation {
./.
../../lib/common.mk
../../scripts/make-erofs.sh
+ ../../scripts/make-gpt.bash
../../scripts/make-gpt.sh
../../scripts/sfdisk-field.awk
]);
diff --git a/lib/config.default.nix b/lib/config.default.nix
index a8422345cc00f9413bb19ec968fd89c82fed801b..489c231490a8b66aa01f50053b25646060f7f963 100644
--- a/lib/config.default.nix
+++ b/lib/config.default.nix
@@ -4,4 +4,5 @@
{
pkgsFun = import ./nixpkgs.default.nix;
pkgsArgs = {};
+ version = "0.0.0";
}
diff --git a/lib/config.nix b/lib/config.nix
index e437cdbe9aa22dd0f9c8d7052ac331c8fccf6ce6..01bcfa2bb2d5c412e212f5a60d9032e89c8a7442 100644
--- a/lib/config.nix
+++ b/lib/config.nix
@@ -18,5 +18,4 @@ let
inherit default;
} else config;
in
-
-default // callConfig config
+ default // callConfig config;
diff --git a/lib/kcmdline-utils.mk b/lib/kcmdline-utils.mk
new file mode 100644
index 0000000000000000000000000000000000000000..5ed97c1a4b0c93d427fbb67f58736eee7fe09259
--- /dev/null
+++ b/lib/kcmdline-utils.mk
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: EUPL-1.2+
+# SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is>
+# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com>
+
+LIVE_IMAGE_DEPS = ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/make-gpt.bash ../../scripts/sfdisk-field.awk ../../scripts/make-live-image.sh ../../lib/kcmdline-utils.mk
diff --git a/release/checks/integration/try.c b/release/checks/integration/try.c
index 4b874c0a7e9b48324497450fb5488e04576fd43b..c34b582230f75ff3374446468d2461a78c0099a6 100644
--- a/release/checks/integration/try.c
+++ b/release/checks/integration/try.c
@@ -10,6 +10,10 @@ void test(struct config c)
{
struct vm *vm;

+ // Spectrum's live image doesn't work right now.
+ // Mark the test as skipped.
+ exit(77);
+
c.drives.img = getenv_or_die("COMBINED_PATH");

vm = start_qemu(c);
diff --git a/release/checks/no-roothash.nix b/release/checks/no-roothash.nix
index 91e3beff1956265a2445d9eeaf69f9f206ec6347..aa08cf9dc4a4fc7386aa92d374300d4a0f011efc 100644
--- a/release/checks/no-roothash.nix
+++ b/release/checks/no-roothash.nix
@@ -28,6 +28,6 @@ in {
machine = create_machine(flags)

machine.start()
- machine.wait_for_console_text("roothash invalid or missing")
+ machine.wait_for_console_text("roothash not set")
'';
}))) (_: {})
diff --git a/release/combined/eosimages.nix b/release/combined/eosimages.nix
index 0ac4c48374e7098a2b91f61fc07cebb2042ffbdc..ba44d9cd82d55d491293ed36cc0402db8ebd3ffe 100644
--- a/release/combined/eosimages.nix
+++ b/release/combined/eosimages.nix
@@ -12,11 +12,15 @@ runCommand "eosimages.img" {
unsafeDiscardReferences = { out = true; };
dontFixup = true;
} ''
+ set -o pipefail
mkdir dir
cd dir
- ln -s $image $imageName
- sha256sum $imageName > $imageName.sha256
- tar -chf $NIX_BUILD_TOP/eosimages.tar *
- tar2ext4 -i $NIX_BUILD_TOP/eosimages.tar -o $out
- e2label $out eosimages
+ ln -s -- "$image" "$imageName"
+ sha256sum -- "$imageName" > "$imageName.sha256" &
+ pid=$!
+ gzip -9 < "$image" > "$imageName.gz"
+ sha256sum -- "$imageName.gz" > "$imageName.gz.sha256"
+ wait "$pid"
+ tar -ch -- "$imageName.gz" "$imageName.gz.sha256" "$imageName.sha256" | tar2ext4 -o "$out"
+ e2label "$out" eosimages
'') (_: {})
diff --git a/release/combined/grub.cfg.in b/release/combined/grub.cfg.in
index a8e73a3b4dc0d643cf575e3cc545ec9ff72380cb..a22f5fc96ba6451d44c0f9768a15a1f48c5dce1c 100644
--- a/release/combined/grub.cfg.in
+++ b/release/combined/grub.cfg.in
@@ -15,11 +15,6 @@ set gfxpayload=keep
terminal_output gfxterm
terminal_output console

-menuentry "Try Spectrum" {
- loopback live (hd0,gpt3)/Spectrum-0.0-x86_64-generic.0.Live.img
- chainloader (live,gpt1)/EFI/Linux/spectrum.efi
-}
-
menuentry "Install Spectrum" {
set root=(hd0,gpt2)
linux @linux@ @kernelParams@
diff --git a/release/live/Makefile b/release/live/Makefile
index 4de8743f42dec65aa863c3020cd70124316a6118..367010c86c5e64272a404a480ed8a43213b3a875 100644
--- a/release/live/Makefile
+++ b/release/live/Makefile
@@ -4,17 +4,14 @@
.POSIX:

include ../../lib/common.mk
+include ../../lib/kcmdline-utils.mk

DTBS ?= build/empty

dest = build/live.img

-$(dest): ../../scripts/format-uuid.sh ../../scripts/make-gpt.sh ../../scripts/sfdisk-field.awk build/boot.fat $(ROOT_FS_VERITY) $(ROOT_FS_VERITY_ROOTHASH) $(ROOT_FS)
- ../../scripts/make-gpt.sh $@.tmp \
- build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \
- "$$ROOT_FS_VERITY":verity:$$(../../scripts/format-uuid.sh "$$(dd if="$$ROOT_FS_VERITY_ROOTHASH" bs=32 skip=1 count=1 status=none)") \
- $(ROOT_FS):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 "$$ROOT_FS_VERITY_ROOTHASH")")
- mv $@.tmp $@
+$(dest): build/boot.fat $(LIVE_IMAGE_DEPS) $(ROOT_FS) $(ROOT_FS_VERITY) $(ROOT_FS_VERITY_ROOTHASH)
+ ../../scripts/make-live-image.sh release $@

build/empty:
mkdir -p $@
diff --git a/release/live/default.nix b/release/live/default.nix
index c234d87e62cc9ae65ba60f94bab6e58b43beddbc..dc649732ffa46a998a4a66360aa8ff7ef6bccae0 100644
--- a/release/live/default.nix
+++ b/release/live/default.nix
@@ -1,12 +1,13 @@
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: 2021-2023, 2025 Alyssa Ross <hi@alyssa.is>
# SPDX-FileCopyrightText: 2022 Unikie
+# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com>

import ../../lib/call-package.nix (
{ callSpectrumPackage, spectrum-build-tools, rootfs, src
, lib, pkgsStatic, stdenvNoCC
, cryptsetup, dosfstools, jq, mtools, util-linux
-, systemdUkify, efi
+, systemdUkify, version, efi
}:

let
@@ -25,8 +26,12 @@ stdenv.mkDerivation {
fileset = lib.fileset.intersection src (lib.fileset.unions [
./.
../../lib/common.mk
+ ../../lib/kcmdline-utils.mk
+ ../../scripts/format-uuid.awk
../../scripts/format-uuid.sh
+ ../../scripts/make-gpt.bash
../../scripts/make-gpt.sh
+ ../../scripts/make-live-image.sh
../../scripts/sfdisk-field.awk
]);
};
@@ -44,6 +49,7 @@ stdenv.mkDerivation {
SYSTEMD_BOOT_EFI = "${efi.systemd}/lib/systemd/boot/efi/systemd-boot${efiArch}.efi";
EFI_IMAGE = efi;
EFINAME = "BOOT${toUpper efiArch}.EFI";
+ VERSION = version;
};

buildFlags = [ "dest=$(out)" ];
diff --git a/release/live/shell.nix b/release/live/shell.nix
index 5acaa8c5b113fd2789aaea9268487b193bab37af..05250525defa0e8a10cde45b5e49f878fcec599f 100644
--- a/release/live/shell.nix
+++ b/release/live/shell.nix
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is>

-import ../../lib/call-package.nix ({ callSpectrumPackage, stdenv, qemu_kvm }:
+import ../../lib/call-package.nix ({ callSpectrumPackage, stdenv, qemu_kvm, rootfs }:

(callSpectrumPackage ./. {}).overrideAttrs (
{ nativeBuildInputs ? [], env ? {}, ... }:
@@ -9,6 +9,7 @@ import ../../lib/call-package.nix ({ callSpectrumPackage, stdenv, qemu_kvm }:
nativeBuildInputs = nativeBuildInputs ++ [ qemu_kvm ];

env = env // {
+ ROOT_FS = rootfs;
OVMF_CODE = "${qemu_kvm}/share/qemu/edk2-${stdenv.hostPlatform.qemuArch}-code.fd";
};
}
diff --git a/scripts/format-uuid.awk b/scripts/format-uuid.awk
new file mode 100644
index 0000000000000000000000000000000000000000..a5349d68a4d29be5f750650236420c9b5a7257eb
--- /dev/null
+++ b/scripts/format-uuid.awk
@@ -0,0 +1,35 @@
+# SPDX-License-Identifier: EUPL-1.2+
+# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com>
+function format_uuid(arg) {
+ if (arg in found_so_far) {
+ fail("Duplicate UUID, try changing the image (by even 1 bit)");
+ }
+ found_so_far[arg] = 1;
+ print (substr(arg, 1, 8) "-" \
+ substr(arg, 9, 4) "-" \
+ substr(arg, 13, 4) "-" \
+ substr(arg, 17, 4) "-" \
+ substr(arg, 21, 12));
+}
+
+function fail(msg) {
+ print msg > "/dev/stderr";
+ exit 1;
+}
+
+BEGIN {
+ FS = "";
+ RS = "\n";
+ if ((getline) != 1)
+ fail("Empty input file");
+ roothash = $0;
+ if (roothash !~ /^[a-f0-9]{64}$/)
+ fail("Invalid root hash");
+ if (getline)
+ fail("Junk after root hash");
+ found_so_far[""] = "";
+ for (i = 1; i != 49; i += 16) {
+ format_uuid(substr($0, i, 32));
+ }
+ format_uuid(substr($0, 49, 16) substr($0, 1, 16));
+}
diff --git a/scripts/make-gpt.bash b/scripts/make-gpt.bash
new file mode 100644
index 0000000000000000000000000000000000000000..f9d53817e3cc4342cac5d4c832cf4aa129880399
--- /dev/null
+++ b/scripts/make-gpt.bash
@@ -0,0 +1,72 @@
+#!/usr/bin/bash --
+# SPDX-FileCopyrightText: 2021-2023 Alyssa Ross <hi@alyssa.is>
+# SPDX-FileCopyrightText: 2022 Unikie
+# SPDX-License-Identifier: EUPL-1.2+
+#
+# usage: make-gpt.sh GPT_PATH PATH:PARTTYPE[:PARTUUID[:PARTLABEL]]...
+
+set -xeuo pipefail
+ONE_MiB=1048576
+
+# Prints the number of 1MiB blocks required to store the file named
+# $1. We use 1MiB blocks because that's what sfdisk uses for
+# alignment. It would be possible to get a slightly smaller image
+# using actual normal-sized 512-byte blocks, but it's probably not
+# worth it to configure sfdisk to do that.
+sizeMiB() {
+ wc -c "$1" | awk -v ONE_MiB=$ONE_MiB \
+ '{printf "%d\n", ($1 + ONE_MiB - 1) / ONE_MiB}'
+}
+
+# Copies from path $3 into partition number $2 in partition table $1.
+fillPartition() {
+ start="$(sfdisk -J "$1" | jq -r --argjson index "$2" \
+ '.partitiontable.partitions[$index].start * 512')"
+
+ # GNU cat will use copy_file_range(2) if possible, whereas dd
+ # will always do a userspace copy, which is significantly slower.
+ lseek -S 1 "$start" cat "$3" 1<>"$1"
+}
+
+# Prints the partition path from a PATH:PARTTYPE[:PARTUUID[:PARTLABEL]] string.
+partitionPath() {
+ awk -F: '{print $1}' <<EOF
+$1
+EOF
+}
+
+scriptsDir="$(dirname "$0")"
+
+out="$1"
+shift
+
+table="label: gpt"
+
+# Keep 1MiB free at the start, and 1MiB free at the end.
+gptBytes=$((ONE_MiB * 2))
+for partition; do
+ if [[ "$partition" =~ :([1-9][0-9]*)MiB$ ]]; then
+ sizeMiB=${BASH_REMATCH[1]}
+ partition=${partition%:*}
+ else
+ partitionPath=$(partitionPath "$partition")
+ sizeMiB=$(sizeMiB "$partitionPath")
+ fi
+ table=$table'
+size='${sizeMiB}MiB$(awk -f "$scriptsDir/sfdisk-field.awk" -v partition="$partition")
+ gptBytes=$((gptBytes + sizeMiB * ONE_MiB))
+done
+
+rm -f "$out"
+truncate -s "$gptBytes" "$out"
+printf %s\\n "$table"
+sfdisk --no-reread --no-tell-kernel "$out" <<EOF
+$table
+EOF
+
+n=0
+for partition; do
+ partitionPath=$(partitionPath "$partition")
+ fillPartition "$out" "$n" "$partitionPath"
+ n=$((n + 1))
+done
diff --git a/scripts/make-gpt.sh b/scripts/make-gpt.sh
index 96f0d2c8494c093558c0e32e7e920b569bb078ef..665057da8281d2b5282081e4999098fbaa29e6ca 100755
--- a/scripts/make-gpt.sh
+++ b/scripts/make-gpt.sh
@@ -1,65 +1,4 @@
-#!/bin/sh -eu
-#
-# SPDX-FileCopyrightText: 2021-2023 Alyssa Ross <hi@alyssa.is>
-# SPDX-FileCopyrightText: 2022 Unikie
+#!/bin/sh --
# SPDX-License-Identifier: EUPL-1.2+
-#
-# usage: make-gpt.sh GPT_PATH PATH:PARTTYPE[:PARTUUID[:PARTLABEL]]...
-
-ONE_MiB=1048576
-
-# Prints the number of 1MiB blocks required to store the file named
-# $1. We use 1MiB blocks because that's what sfdisk uses for
-# alignment. It would be possible to get a slightly smaller image
-# using actual normal-sized 512-byte blocks, but it's probably not
-# worth it to configure sfdisk to do that.
-sizeMiB() {
- wc -c "$1" | awk -v ONE_MiB=$ONE_MiB \
- '{printf "%d\n", ($1 + ONE_MiB - 1) / ONE_MiB}'
-}
-
-# Copies from path $3 into partition number $2 in partition table $1.
-fillPartition() {
- start="$(sfdisk -J "$1" | jq -r --argjson index "$2" \
- '.partitiontable.partitions[$index].start * 512')"
-
- # GNU cat will use copy_file_range(2) if possible, whereas dd
- # will always do a userspace copy, which is significantly slower.
- lseek -S 1 "$start" cat "$3" 1<>"$1"
-}
-
-# Prints the partition path from a PATH:PARTTYPE[:PARTUUID[:PARTLABEL]] string.
-partitionPath() {
- awk -F: '{print $1}' <<EOF
-$1
-EOF
-}
-
-scriptsDir="$(dirname "$0")"
-
-out="$1"
-shift
-
-nl='
-'
-table="label: gpt"
-
-# Keep 1MiB free at the start, and 1MiB free at the end.
-gptBytes=$((ONE_MiB * 2))
-for partition; do
- sizeMiB="$(sizeMiB "$(partitionPath "$partition")")"
- table="$table${nl}size=${sizeMiB}MiB,$(awk -f "$scriptsDir/sfdisk-field.awk" -v partition="$partition")"
- gptBytes="$((gptBytes + sizeMiB * ONE_MiB))"
-done
-
-rm -f "$out"
-truncate -s "$gptBytes" "$out"
-sfdisk --no-reread --no-tell-kernel "$out" <<EOF
-$table
-EOF
-
-n=0
-for partition; do
- fillPartition "$out" "$n" "$(partitionPath "$partition")"
- n="$((n + 1))"
-done
+# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com>
+exec bash -- "${0%.sh}.bash" "$@"
diff --git a/scripts/make-live-image.sh b/scripts/make-live-image.sh
new file mode 100755
index 0000000000000000000000000000000000000000..6608cc35b7a15178adf5ff3d3917b5243c5da6cd
--- /dev/null
+++ b/scripts/make-live-image.sh
@@ -0,0 +1,43 @@
+#!/bin/sh --
+# SPDX-License-Identifier: EUPL-1.2+
+# SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is>
+# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com>
+set -euo pipefail
+case $0 in
+(/*) dir=${0%/*}/;;
+(*/*) dir=./${0%/*};;
+(*) dir=.;;
+esac
+usage () {
+ echo 'Usage: make-live-image.sh [release|live] OUTPUT_FILE' >&2
+ exit 1
+}
+if [ "$#" != 2 ]; then usage; fi
+file_type=$1 output_file=$2
+for i in "$ROOT_FS" "$ROOT_FS_VERITY" "$ROOT_FS_VERITY_ROOTHASH" "$VERSION"; do
+ # Some characters not special to the shell can't be handled by this code.
+ case $i in
+ (-*|*[!A-Za-z0-9._/+@-]*) printf 'Forbidden characters in "%s"\n' "$i" >&2; exit 1;;
+ esac
+done
+root_hashes=$(LC_ALL=C awk -f "${dir}/format-uuid.awk" < "$ROOT_FS_VERITY_ROOTHASH")
+# The awk script produces output that is meant for field splitting
+# and has no characters special for globbing.
+# shellcheck disable=SC2086
+set -- $root_hashes
+case $file_type in
+(release)
+ "$dir/make-gpt.sh" "$output_file.tmp" \
+ build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \
+ "$ROOT_FS_VERITY:verity:$1:Spectrum_$VERSION.verity:1024MiB" \
+ "$ROOT_FS:root:$2:Spectrum_$VERSION:20480MiB" \
+ "/dev/null:verity:$3:_empty:1024MiB" \
+ "/dev/null:root:$4:_empty:20480MiB"
+ ;;
+(live)
+ "$dir/make-gpt.sh" "$output_file.tmp" \
+ "$ROOT_FS_VERITY:verity:$1:Spectrum_$VERSION.verity" \
+ "$ROOT_FS:root:$2:Spectrum_$VERSION";;
+(*) usage;;
+esac
+mv -- "$output_file.tmp" "$output_file"
diff --git a/scripts/sfdisk-field.awk b/scripts/sfdisk-field.awk
index e13c86d2fb11a066eebd043808e659b08dbd269c..72eec9a0a770563d32da14440fe2552eb2e39b68 100644
--- a/scripts/sfdisk-field.awk
+++ b/scripts/sfdisk-field.awk
@@ -24,6 +24,7 @@ BEGIN {
arch = _arch
}

+ comma = ""
for (n in fields) {
if (n <= skip)
continue
@@ -33,6 +34,6 @@ BEGIN {
fields[n] = uuid
}

- printf "%s=%s,", keys[n - skip], fields[n]
+ printf ",%s%s=%s", comma, keys[n - skip], fields[n]
}
}
diff --git a/vm/sys/net/Makefile b/vm/sys/net/Makefile
index d71c2325eff3bae921f33c61f799846d35e401c2..20675dda7436394ebbf08e685323bdf8532618f3 100644
--- a/vm/sys/net/Makefile
+++ b/vm/sys/net/Makefile
@@ -23,7 +23,7 @@ $(vmdir)/netvm/vmlinux: $(KERNEL)
mkdir -p $$(dirname $@)
cp $(KERNEL) $@

-$(vmdir)/netvm/blk/root.img: ../../../scripts/make-gpt.sh ../../../scripts/sfdisk-field.awk build/rootfs.erofs
+$(vmdir)/netvm/blk/root.img: ../../../scripts/make-gpt.sh ../../../scripts/make-gpt.bash ../../../scripts/sfdisk-field.awk build/rootfs.erofs
mkdir -p $$(dirname $@)
../../../scripts/make-gpt.sh $@.tmp \
build/rootfs.erofs:root:ea21da27-0391-48da-9235-9d2ab2ca7844:root
diff --git a/vm/sys/net/default.nix b/vm/sys/net/default.nix
index de273e55360c29614e61aa4d1646019bbbfd2c25..994c19a11e37420c111b979d3dece0ad2037a5a1 100644
--- a/vm/sys/net/default.nix
+++ b/vm/sys/net/default.nix
@@ -106,6 +106,7 @@ stdenvNoCC.mkDerivation {
./.
../../../lib/common.mk
../../../scripts/make-erofs.sh
+ ../../../scripts/make-gpt.bash
../../../scripts/make-gpt.sh
../../../scripts/sfdisk-field.awk
]);

--
2.51.2