[PATCH 4/7] Adjust partition layout to support updates

Demi Marie Obenour
2025-10-29T10:14:34Z

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 OS
0.0.0 will be in a partition with label Spectrum_OS_0.0.0.
- The label of the inactive partition must either be that of the old
version of the OS 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.

Signed-off-by: Demi Marie Obenour <demiobenour@gmail.com>
---
host/initramfs/Makefile | 17 +++++-----
host/initramfs/default.nix | 2 ++
host/initramfs/etc/init | 17 +++-------
host/initramfs/etc/probe | 20 ++++++++----
host/initramfs/shell.nix | 1 +
host/rootfs/Makefile | 23 ++++++++------
host/rootfs/default.nix | 3 ++
host/rootfs/shell.nix | 1 +
img/app/Makefile | 2 +-
img/app/default.nix | 5 +--
lib/kcmdline-utils.mk | 10 +++---
lib/version.nix | 15 +++++++++
release/checks/no-roothash.nix | 2 +-
release/live/Makefile | 15 ++++-----
release/live/default.nix | 11 +++++--
release/live/shell.nix | 4 ++-
scripts/format-uuid.awk | 35 ++++++++++++++++++++
scripts/format-uuid.sh | 1 +
scripts/make-gpt.bash | 72 ++++++++++++++++++++++++++++++++++++++++++
scripts/make-gpt.sh | 67 ++-------------------------------------
scripts/make-live-image.sh | 41 ++++++++++++++++++++++++
scripts/sfdisk-field.awk | 3 +-
version | 1 +
version.license | 2 ++
vm/sys/net/Makefile | 2 +-
vm/sys/net/default.nix | 5 +--
26 files changed, 252 insertions(+), 125 deletions(-)

diff --git a/host/initramfs/Makefile b/host/initramfs/Makefile
index cb13fbb35f065b67d291d4a35591d6f12720060c..798f675eb4f2ffde1c2eadc0a7b08ca59b65f347 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

@@ -51,16 +52,13 @@ build/rootfs.verity.roothash: build/rootfs.verity
build/rootfs.verity.superblock: build/rootfs.verity
tail -n +2 build/rootfs.verity > $@

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

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 $@
@@ -69,12 +67,13 @@ clean:
rm -rf build
.PHONY: clean

-run: $(dest) build/rootfs.verity.roothash $(RUN_IMAGE)
- @../../scripts/run-qemu.sh -m 4G \
+run: $(dest) build/rootfs.verity.roothash $(RUN_IMAGE) ../../lib/kcmdline-utils.mk
+ $(READ_ROOTHASH); \
+ ../../scripts/run-qemu.sh -m 4G \
-machine virtualization=on \
-kernel $(KERNEL) \
-initrd $(dest) \
- -append "ro earlycon console=hvc0 intel_iommu=on roothash=$$(< build/rootfs.verity.roothash) nokaslr" \
+ -append "ro earlycon console=hvc0 intel_iommu=on nokaslr x-spectrum-roothash=$$roothash x-spectrum-version=$$VERSION" \
-cpu max \
-gdb unix:build/gdb.sock,server,nowait \
-parallel none \
diff --git a/host/initramfs/default.nix b/host/initramfs/default.nix
index ac7efbe3e19ee73d757d041a6a1051fe06c1d069..a1a1abbf5395f5650585901e5d2a13a72100ac09 100644
--- a/host/initramfs/default.nix
+++ b/host/initramfs/default.nix
@@ -99,12 +99,14 @@ stdenvNoCC.mkDerivation {
fileset = lib.fileset.intersection src (lib.fileset.unions [
./.
../../lib/common.mk
+ ../../lib/kcmdline-utils.mk
]);
};
sourceRoot = "source/host/initramfs";

env = {
PACKAGES_CPIO = packagesCpio;
+ VERSION = import ../../lib/version.nix;
} // lib.optionalAttrs stdenvNoCC.hostPlatform.isx86_64 {
MICROCODE = microcode;
};
diff --git a/host/initramfs/etc/init b/host/initramfs/etc/init
index 719488741b6d31564c2c17c0e41f15d16b1c0a08..8f36cd68e0450ff1a77ed5338e992323577a9f87 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 x-spectrum-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..34e82fe9fa81316f21125b8eb058cc2917de69d7 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_OS_${version} }
+backtick verity_dev { findfs PARTLABEL=Spectrum_OS_${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 eeba865e3ac793f67ae1808a92cf5eb1b37d57af..fa628f9c09eb266de247241b233286e756bd01d4 100644
--- a/host/initramfs/shell.nix
+++ b/host/initramfs/shell.nix
@@ -18,5 +18,6 @@ initramfs.overrideAttrs ({ nativeBuildInputs ? [], env ? {}, ... }: {
env = env // {
KERNEL = "${rootfs.kernel}/${stdenv.hostPlatform.linux-kernel.target}";
ROOT_FS = rootfs;
+ VERSION = import ../../lib/version.nix;
};
})) (_: {})
diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile
index 15752286f5924291768f0655a12b90c702730520..84f1b385198ecfa5905b69e4901e56150ea1b424 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/rootfs.erofs
@@ -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

@@ -99,11 +102,8 @@ build/rootfs.verity.roothash: build/rootfs.verity
build/rootfs.verity.superblock: build/rootfs.verity
tail -n +2 build/rootfs.verity > $@

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

debug:
$(GDB) -q \
@@ -112,14 +112,17 @@ debug:
$(VMLINUX)
.PHONY: debug

-run: build/live.img $(EXT_FS) build/rootfs.verity.roothash
+run: build/live.img $(EXT_FS) build/rootfs.verity.roothash ../../lib/kcmdline-utils.mk
@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 && \
+ set -x && $(READ_ROOTHASH) && \
exec ../../scripts/run-qemu.sh -cpu max -m 4G \
-machine virtualization=on \
-kernel $(KERNEL) \
@@ -133,7 +136,7 @@ run: build/live.img $(EXT_FS) build/rootfs.verity.roothash
-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=$$(< build/rootfs.verity.roothash) intel_iommu=on nokaslr" \
+ -append "earlycon console=hvc0 intel_iommu=on nokaslr x-spectrum-roothash=$$roothash 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 00052222507077b9e94a5ed0a3fbddd27caeefc3..bc364b930b30e00c55b17b5e4248a303392cf3a0 100644
--- a/host/rootfs/default.nix
+++ b/host/rootfs/default.nix
@@ -133,7 +133,9 @@ stdenvNoCC.mkDerivation {
fileset = fileset.intersection src (fileset.unions [
./.
../../lib/common.mk
+ ../../lib/kcmdline-utils.mk
../../scripts/make-erofs.sh
+ ../../version
]);
};
sourceRoot = "source/host/rootfs";
@@ -145,6 +147,7 @@ stdenvNoCC.mkDerivation {
printf "%s\n/\n" ${packagesSysroot} >$out
sed p ${writeClosure [ packagesSysroot] } >>$out
'';
+ VERSION = import ../../lib/version.nix;
};

makeFlags = [ "dest=$(out)" ];
diff --git a/host/rootfs/shell.nix b/host/rootfs/shell.nix
index 3d986f7327823cb855e5980759ad2a3935793340..bd234e90ee19bdfa6591d29c518cb0dc393b01c8 100644
--- a/host/rootfs/shell.nix
+++ b/host/rootfs/shell.nix
@@ -20,5 +20,6 @@ rootfs.overrideAttrs (
KERNEL = "${passthru.kernel}/${stdenv.hostPlatform.linux-kernel.target}";
LINUX_SRC = srcOnly passthru.kernel;
VMLINUX = "${passthru.kernel.dev}/vmlinux";
+ VERSION = import ../../lib/version.nix;
};
})) (_: {})
diff --git a/img/app/Makefile b/img/app/Makefile
index 981889ebe55d9ba03228977f3dc0ea3f26d5c4fb..e380fc173f580f00e9f4008da36533b645345f9b 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 253fef08f5db29757da9d11fda67ac23fe6040c3..06764356d4126d3a2cd6a3e590accfeab6cffda4 100644
--- a/img/app/default.nix
+++ b/img/app/default.nix
@@ -8,7 +8,7 @@ pkgsStatic.callPackage (
{ lib, stdenvNoCC, runCommand, writeClosure
, erofs-utils, jq, s6-rc, util-linux
, busybox, cacert, dejavu_fonts, execline, kmod, linux_latest, mdevd, s6
-, s6-linux-init, spectrum-app-tools
+, s6-linux-init, spectrum-app-tools, bash
}:

let
@@ -106,13 +106,14 @@ stdenvNoCC.mkDerivation {
./.
../../lib/common.mk
../../scripts/make-erofs.sh
+ ../../scripts/make-gpt.bash
../../scripts/make-gpt.sh
../../scripts/sfdisk-field.awk
]);
};
sourceRoot = "source/img/app";

- nativeBuildInputs = [ erofs-utils jq spectrum-build-tools s6-rc util-linux ];
+ nativeBuildInputs = [ erofs-utils jq spectrum-build-tools s6-rc util-linux bash ];

env = {
KERNEL = "${kernel}/${baseNameOf kernelTarget}";
diff --git a/lib/kcmdline-utils.mk b/lib/kcmdline-utils.mk
index fa228552e583f15fc77a746985060ad5d04cdf2c..7f1ef7d197ccf68c17640f4fdf44c167939fca13 100644
--- a/lib/kcmdline-utils.mk
+++ b/lib/kcmdline-utils.mk
@@ -1,6 +1,8 @@
# SPDX-License-Identifier: EUPL-1.2+
# SPDX-FileCopyrightText: 2021-2024 Alyssa Ross <hi@alyssa.is>
-READ_ROOTHASH = { \
- set -eufo pipefail; \
- read -r version < ../../version; \
- LC_ALL=C expr "x$$version" : '^\(x0\|x[1-9][0-9]*\)\(\.\(0\|[1-9][0-9]*\)\)\{2\}$$' >/dev/null; }
+# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com>
+READ_ROOTHASH = { set -euo pipefail; \
+ read -r roothash < build/rootfs.verity.roothash; \
+ LC_ALL=C expr "x$$roothash" : '^x[a-f0-9]\{64\}$$' >/dev/null; }
+
+LIVE_IMAGE_DEPS = ../../scripts/format-uuid.awk ../../scripts/make-gpt.sh ../../scripts/make-gpt.bash ../../scripts/sfdisk-field.awk build/rootfs.verity.superblock build/rootfs.verity.roothash ../../scripts/make-live-image.sh ../../lib/kcmdline-utils.mk
diff --git a/lib/version.nix b/lib/version.nix
new file mode 100644
index 0000000000000000000000000000000000000000..1c1568137313c37c4e1377a063992f7bf6856e57
--- /dev/null
+++ b/lib/version.nix
@@ -0,0 +1,15 @@
+# SPDX-License-Identifier: MIT
+# SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com>
+
+let
+ raw_version = builtins.readFile ../version;
+ version_length = builtins.stringLength raw_version - 1;
+ version = builtins.substring 0 version_length raw_version;
+ number_re = "(0|[1-9][0-9]{0,2})";
+in
+if version_length < 0 || builtins.substring version_length 1 raw_version != "\n" then
+ builtins.abort "Version file missing trailing newline (contents ${builtins.toJSON raw_version})"
+else if builtins.match "^(${number_re}\\.){2}${number_re}$" version == null then
+ builtins.abort "Version ${builtins.toJSON version} is invalid"
+else
+ version
diff --git a/release/checks/no-roothash.nix b/release/checks/no-roothash.nix
index 76d1e8b88ba74e6981775f3d4b8d10138c342d84..1d044cb04828ea221e7d1656e5eb7942669fe73c 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("x-spectrum-roothash not set")
'';
}))) (_: {})
diff --git a/release/live/Makefile b/release/live/Makefile
index 6dcbdeedda5d6ccf293f60dc62043f46c81ecf83..3072d869f13efbf5ea196d191881aeab85726d2e 100644
--- a/release/live/Makefile
+++ b/release/live/Makefile
@@ -4,22 +4,21 @@
.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 build/rootfs.verity.superblock build/rootfs.verity.roothash $(ROOT_FS)
- ../../scripts/make-gpt.sh $@.tmp \
- build/boot.fat:c12a7328-f81f-11d2-ba4b-00a0c93ec93b \
- build/rootfs.verity.superblock:verity:$$(../../scripts/format-uuid.sh "$$(dd if=build/rootfs.verity.roothash bs=32 skip=1 count=1 status=none)") \
- $(ROOT_FS):root:$$(../../scripts/format-uuid.sh "$$(head -c 32 build/rootfs.verity.roothash)")
- mv $@.tmp $@
+$(dest): $(LIVE_IMAGE_DEPS) build/boot.fat
+ ../../scripts/make-live-image.sh release $(dest) $(ROOT_FS)

build/empty:
mkdir -p $@

-build/spectrum.efi: build/rootfs.verity.roothash $(DTBS) $(KERNEL) $(INITRAMFS)
+build/spectrum.efi: build/rootfs.verity.roothash $(DTBS) $(KERNEL) $(INITRAMFS) ../../lib/kcmdline-utils.mk
+ set -euo pipefail; \
+ $(READ_ROOTHASH) && \
{ \
printf "[UKI]\nDeviceTreeAuto=" && \
find $(DTBS) -name '*.dtb' -print0 | tr '\0' ' ' ;\
@@ -29,7 +28,7 @@ build/spectrum.efi: build/rootfs.verity.roothash $(DTBS) $(KERNEL) $(INITRAMFS)
--linux $(KERNEL) \
--initrd $(INITRAMFS) \
--os-release $$'NAME="Spectrum"\n' \
- --cmdline "ro intel_iommu=on roothash=$$(cat build/rootfs.verity.roothash)"
+ --cmdline "ro intel_iommu=on x-spectrum-roothash=$$roothash x-spectrum-version=$$VERSION"

build/boot.fat: $(SYSTEMD_BOOT_EFI) build/spectrum.efi
$(TRUNCATE) -s 440401920 $@
diff --git a/release/live/default.nix b/release/live/default.nix
index 2a1dc3e1dd939f21edac582bf39737eb4d46eb0c..b5c0c8df31d4c6cb7fdd2337e8169f36655dd1a8 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
+, systemdUkify, bash
}:

let
@@ -32,15 +33,20 @@ 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
+ ../../version
]);
};
sourceRoot = "source/release/live";

nativeBuildInputs = [
- cryptsetup dosfstools jq spectrum-build-tools mtools systemd util-linux
+ bash cryptsetup dosfstools jq spectrum-build-tools mtools systemd util-linux
];

env = {
@@ -49,6 +55,7 @@ stdenv.mkDerivation {
ROOT_FS = rootfs;
SYSTEMD_BOOT_EFI = "${systemd}/lib/systemd/boot/efi/systemd-boot${efiArch}.efi";
EFINAME = "BOOT${toUpper efiArch}.EFI";
+ VERSION = import ../../lib/version.nix;
} // lib.optionalAttrs stdenv.hostPlatform.linux-kernel.DTB or false {
DTBS = "${rootfs.kernel}/dtbs";
};
diff --git a/release/live/shell.nix b/release/live/shell.nix
index 5acaa8c5b113fd2789aaea9268487b193bab37af..e1e78214871c0e4681bff0d5a894c8ae3b8c3c02 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,7 +9,9 @@ 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";
+ VERSION = import ../../lib/version.nix;
};
}
)) (_: {})
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/format-uuid.sh b/scripts/format-uuid.sh
index 497a5f2daeef88e0143f5021cd64fa2181ffe163..f589b3340252c653df97a82ce429528beee43b1a 100755
--- a/scripts/format-uuid.sh
+++ b/scripts/format-uuid.sh
@@ -4,6 +4,7 @@
# SPDX-FileCopyrightText: 2022 Unikie
# SPDX-License-Identifier: EUPL-1.2+

+set -o pipefail
substr () {
str=$1
beg=$2
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..2d8f5140fd23280d0f8ff2c0cb1640875dab4e8e
--- /dev/null
+++ b/scripts/make-live-image.sh
@@ -0,0 +1,41 @@
+#!/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
+if [ ! -f build/rootfs.verity.superblock ]; then
+ echo 'No superblock found' >&2
+ exit 1
+fi
+case $0 in
+(/*) dir=${0%/*}/;;
+(*/*) dir=./${0%/*};;
+(*) dir=.;;
+esac
+usage () {
+ echo 'Usage: make-live-image.sh [release|live] OUTPUT_FILE ROOT_FILESYSTEM' >&2
+ exit 1
+}
+if [ "$#" != 3 ]; then usage; fi
+file_type=$1 output_file=$2 root_filesystem=$3
+root_hashes=$(LC_ALL=C awk -f "${dir}/format-uuid.awk" < build/rootfs.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 \
+ "build/rootfs.verity.superblock:verity:$1:Spectrum_OS_$VERSION.verity:1024MiB" \
+ "$root_filesystem:root:$2:Spectrum_OS_$VERSION:20480MiB" \
+ "/dev/null:verity:$3:_empty:1024MiB" \
+ "/dev/null:root:$4:_empty:20480MiB"
+ ;;
+(live)
+ "$dir/make-gpt.sh" "$output_file.tmp" \
+ "build/rootfs.verity.superblock:verity:$1:Spectrum_OS_$VERSION.verity" \
+ "$root_filesystem:root:$2:Spectrum_OS_$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/version b/version
new file mode 100644
index 0000000000000000000000000000000000000000..77d6f4ca23711533e724789a0a0045eab28c5ea6
--- /dev/null
+++ b/version
@@ -0,0 +1 @@
+0.0.0
diff --git a/version.license b/version.license
new file mode 100644
index 0000000000000000000000000000000000000000..e9aa5bf149a1b426dba78c7df37b92c0a992a7dd
--- /dev/null
+++ b/version.license
@@ -0,0 +1,2 @@
+SPDX-License-Identifier: EUPL-1.2+
+SPDX-FileCopyrightText: 2025 Demi Marie Obenour <demiobenour@gmail.com>
diff --git a/vm/sys/net/Makefile b/vm/sys/net/Makefile
index b377e12bba8f062026e997de18e19c9af8e07cb5..c29a8faee4e0a7eb170325e4d1eaeeba4532df41 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 9d9df0b001060085239e00ffa59f8e091f0b88bf..2f3eea176928315ac0cd6e81bb788e965613e3a9 100644
--- a/vm/sys/net/default.nix
+++ b/vm/sys/net/default.nix
@@ -8,7 +8,7 @@ pkgsStatic.callPackage (
{ lib, stdenvNoCC, nixos, runCommand, writeClosure
, erofs-utils, jq, s6-rc, util-linux, xorg
, busybox, connmanMinimal, dbus, execline, kmod, linux_latest, mdevd, nftables
-, s6, s6-linux-init
+, s6, s6-linux-init, bash
}:

let
@@ -106,13 +106,14 @@ stdenvNoCC.mkDerivation {
./.
../../../lib/common.mk
../../../scripts/make-erofs.sh
+ ../../../scripts/make-gpt.bash
../../../scripts/make-gpt.sh
../../../scripts/sfdisk-field.awk
]);
};
sourceRoot = "source/vm/sys/net";

- nativeBuildInputs = [ erofs-utils jq spectrum-build-tools s6-rc util-linux ];
+ nativeBuildInputs = [ erofs-utils jq spectrum-build-tools s6-rc util-linux bash ];

env = {
KERNEL = "${kernel}/${baseNameOf kernelTarget}";

--
2.51.2