`clan machines build cursa --format sd-card` failed in clan-cli's post-build secret injection: Expected directory /nix/store/...-nixos-image-sd-card-...img.zst/sd-card not found in build output clan-cli assumed $out/<format>/ (only true for iso; sd-card writes $out/sd-image/) and tried to inject into every format although only iso has an addFilesScript. Fixed upstream in patches/upstream-sd-card-secret-injection.patch (pending PR); until it lands: - shell.nix: apply patches/clan-cli-image-filepath.patch (the clan-cli part of the upstream change) to the devshell's clan-cli. - modules/nixos/sd-card-image.nix: local copy of the upstream clan.core.image.sd-card.addFilesScript module. Writes the vars tree directly into the ext4 root partition with debugfs (no boot-time hook), verifies with cmp + e2fsck. Delete once clan-core ships it. - cursa imports it and builds its sd-card image without disko.
181 lines
6.3 KiB
Nix
181 lines
6.3 KiB
Nix
# Local copy of the clan-core module in patches/upstream-sd-card-secret-injection.patch.
|
|
# Delete once clan-core ships clan.core.image.sd-card.addFilesScript
|
|
# (the option declaration would conflict).
|
|
{
|
|
inputs,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
let
|
|
parseAddFilesArgs = "${inputs.clan-core}/nixosModules/clanCore/image/parse-add-files-args.sh";
|
|
|
|
# Writes extra files straight into the ext4 root partition of an SD image
|
|
# with debugfs, so no boot-time hook is needed: the files exist on the root
|
|
# filesystem from the first boot. Runs unprivileged and without mounting.
|
|
sdCardAddFilesScript = pkgs.writeShellApplication {
|
|
name = "add-files";
|
|
runtimeInputs = with pkgs; [
|
|
coreutils
|
|
diffutils
|
|
e2fsprogs
|
|
findutils
|
|
util-linux
|
|
zstd
|
|
];
|
|
extraShellCheckFlags = [ "-x" ];
|
|
text = ''
|
|
# shellcheck source=${parseAddFilesArgs}
|
|
source ${parseAddFilesArgs}
|
|
|
|
# If no extra paths, just copy source to output
|
|
if [[ ''${#extra_paths[@]} -eq 0 ]]; then
|
|
cp -- "$source_image" "$output_image"
|
|
exit 0
|
|
fi
|
|
|
|
# The injection sandbox mounts /tmp as a RAM-backed tmpfs and a raw SD
|
|
# image is several GiB, so stage it next to the output, the only
|
|
# writable on-disk path.
|
|
workdir=$(dirname "$output_image")
|
|
img="$workdir/.$(basename "$output_image").raw"
|
|
cmds=$(mktemp)
|
|
trap 'rm -f -- "$img" "$cmds"' EXIT
|
|
|
|
case "$source_image" in
|
|
*.zst)
|
|
zstd -d -q --no-progress -o "$img" -- "$source_image"
|
|
compress=1
|
|
;;
|
|
*)
|
|
cp --no-preserve=mode -- "$source_image" "$img"
|
|
compress=0
|
|
;;
|
|
esac
|
|
chmod u+w -- "$img"
|
|
|
|
# sd-image.nix writes an MBR whose ext4 root is the (only) type-83 partition.
|
|
root_start=""
|
|
while read -r _ start type; do
|
|
if [[ "$type" == "0x83" ]]; then
|
|
root_start=$start
|
|
fi
|
|
done < <(partx --show -g -o NR,START,TYPE -- "$img")
|
|
if [[ -z "$root_start" ]]; then
|
|
echo "No Linux (type 83) root partition found in $source_image" >&2
|
|
exit 1
|
|
fi
|
|
# e2fsprogs opens a partition inside a disk image via "<file>?offset=<bytes>".
|
|
rootfs="$img?offset=$((root_start * 512))"
|
|
|
|
# Build a debugfs command script. debugfs has neither mkdir -p nor a
|
|
# recursive write, so directories are created ancestor-first ("already
|
|
# exists" errors are harmless). write copies the source mode; ownership
|
|
# is forced to root since the script runs as an unprivileged user.
|
|
declare -A made_dirs=()
|
|
written=()
|
|
emit_mkdir_p() {
|
|
local dir="$1" path="" part
|
|
IFS=/ read -ra parts <<< "$dir"
|
|
for part in "''${parts[@]}"; do
|
|
[[ -z "$part" ]] && continue
|
|
path="$path/$part"
|
|
if [[ -z "''${made_dirs[$path]:-}" ]]; then
|
|
echo "mkdir \"$path\""
|
|
made_dirs[$path]=1
|
|
fi
|
|
done
|
|
}
|
|
emit_write() {
|
|
local src="$1" dest="$2"
|
|
echo "write \"$src\" \"$dest\""
|
|
echo "sif \"$dest\" uid 0"
|
|
echo "sif \"$dest\" gid 0"
|
|
written+=("$src" "$dest")
|
|
}
|
|
|
|
for ((i = 0; i < ''${#extra_paths[@]}; i += 2)); do
|
|
local_src="''${extra_paths[i]}"
|
|
# Normalize to an absolute path without trailing slash; "/" becomes "".
|
|
target="/''${extra_paths[i+1]#/}"
|
|
target="''${target%/}"
|
|
|
|
if [[ -d "$local_src" ]]; then
|
|
emit_mkdir_p "$target"
|
|
while IFS= read -r -d "" entry; do
|
|
dest="$target/''${entry#"$local_src"/}"
|
|
if [[ -d "$entry" ]]; then
|
|
emit_mkdir_p "$dest"
|
|
elif [[ -f "$entry" ]]; then
|
|
emit_write "$entry" "$dest"
|
|
else
|
|
echo "Unsupported file type (only regular files and directories): $entry" >&2
|
|
exit 1
|
|
fi
|
|
done < <(
|
|
# Never treat our own staging/output directory as input, even if
|
|
# the caller placed it inside an extra path.
|
|
find "$local_src" -mindepth 1 -path "$workdir" -prune -o -print0
|
|
)
|
|
elif [[ -f "$local_src" ]]; then
|
|
emit_mkdir_p "$(dirname "$target")"
|
|
emit_write "$local_src" "$target"
|
|
else
|
|
echo "Extra path does not exist: $local_src" >&2
|
|
exit 1
|
|
fi
|
|
done > "$cmds"
|
|
|
|
debugfs -w -f "$cmds" "$rootfs"
|
|
|
|
# debugfs exits 0 even when commands fail: verify every file landed
|
|
# intact and the filesystem is still consistent.
|
|
for ((i = 0; i < ''${#written[@]}; i += 2)); do
|
|
if ! debugfs -R "cat \"''${written[i+1]}\"" "$rootfs" 2>/dev/null | cmp -s -- - "''${written[i]}"; then
|
|
echo "Failed to write ''${written[i+1]} into the root filesystem" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
if ! fsck_output=$(EXT2FS_NO_MTAB_OK=yes e2fsck -fn "$rootfs" 2>&1); then
|
|
echo "Root filesystem is inconsistent after injection:" >&2
|
|
echo "$fsck_output" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "$compress" == 1 ]]; then
|
|
zstd -T0 -q --no-progress --rm -o "$output_image" -- "$img"
|
|
else
|
|
mv -- "$img" "$output_image"
|
|
fi
|
|
'';
|
|
};
|
|
in
|
|
{
|
|
options.clan.core.image = {
|
|
sd-card.addFilesScript = lib.mkOption {
|
|
type = lib.types.package;
|
|
default = sdCardAddFilesScript;
|
|
defaultText = lib.literalExpression "pkgs.writeShellApplication { ... }";
|
|
description = ''
|
|
Script to inject extra files into an SD card image
|
|
(`clan machines build <machine> --format sd-card`).
|
|
|
|
Files are written directly into the ext4 root partition, so they are
|
|
present from the first boot without any initrd hook. Both compressed
|
|
(`.img.zst`, the `sdImage.compressImage` default) and raw `.img`
|
|
images are supported; the output keeps the source's format.
|
|
|
|
Source `parse-add-files-args.sh` to get standardized argument parsing.
|
|
After sourcing, `$source_image`, `$output_image`, and `$extra_paths`
|
|
are available.
|
|
|
|
Accepts the following arguments:
|
|
- --source `<path>`: Path to the original built image (read-only)
|
|
- --output `<path>`: Path for the modified output image
|
|
- --extra-path `<local-source>` `<target-path>`: Repeatable, maps a local
|
|
file or directory to a target filesystem path inside the image
|
|
'';
|
|
};
|
|
};
|
|
}
|