#!/bin/sh set -eu test "$#" -ge 10 || { echo 'usage: bootstrap-image.sh HOLYPKG STATIC_HOLYPKG STATIC_CC BUSYBOX DINIT MDEVD KERNEL KERNEL_VERSION LIMINE_DIR OUTPUT [--local PACKAGE.holy | --source ALIAS PACKAGE ...]' >&2 exit 2 } if test "${HOLY_IMAGE_NAMESPACE:-}" != 1; then test "$(id -u)" != 0 || { echo 'run the builder as an ordinary user' >&2; exit 6; } export HOLY_IMAGE_NAMESPACE=1 exec unshare --map-root-user -- sh "$0" "$@" fi test "$(id -u)" = 0 && awk '$1 == 0 && $2 != 0 && $3 == 1 { ok = 1 } END { exit !ok }' /proc/self/uid_map || exit 6 umask 022 PATH=$PATH:/usr/sbin:/sbin export PATH bin=$(realpath "$1") static=$(realpath "$2") installer=$(realpath "${STATIC_HOLYINSTALL:?STATIC_HOLYINSTALL required}") cc=$(realpath "$3") core_input() { case "$1" in /*|./*|../*) realpath "$1" ;; *:*) printf '%s\n' "$1" ;; *) realpath "$1" ;; esac } busybox=$(core_input "$4") dinit=$(core_input "$5") mdevd=$(core_input "$6") kernel=$(core_input "$7") version=$8 limine_dir=$(realpath "$9") shift 9 image_output=$1 shift case "$version" in ''|*[!a-zA-Z0-9._+-]*) exit 2 ;; esac test "$(uname -m)" = x86_64 || exit 6 arch=${ARCH:-x86_64} case "$arch" in x86_64) qemu=qemu-system-x86_64; package_arch=x86_64 ;; i686) qemu=qemu-system-i386; package_arch=x86 ;; *) echo 'ARCH must be i686 or x86_64' >&2; exit 2 ;; esac boot_test=${IMAGE_BOOT_TEST:-required} case "$boot_test" in required|build-only) ;; *) echo 'IMAGE_BOOT_TEST must be required or build-only' >&2; exit 2 ;; esac if test "$boot_test" = required && ! command -v "$qemu" >/dev/null; then boot_test=build-only echo "$qemu unavailable; image will remain untested" >&2 fi for tool in dracut ldconfig limine sha256sum cpio gzip python3; do command -v "$tool" >/dev/null || { echo "$tool required" >&2; exit 6; } done validate_kernel() { python3 - "$kernel" "$arch" <<'PY' import struct import sys with open(sys.argv[1], 'rb') as source: header = source.read(0x238) if (len(header) < 0x238 or header[0x202:0x206] != b'HdrS' or struct.unpack_from('&2; exit 2 ;; esac if test "$install_test" = 1; then case "$arch:$install_firmware" in x86_64:bios|x86_64:both|i686:bios) ;; *) echo 'INSTALL_FIRMWARE requires BIOS for i686, BIOS or both for x86_64' >&2; exit 2 ;; esac doas_package=$(realpath "${DOAS_PACKAGE:?DOAS_PACKAGE required}") test -f "$doas_package" || exit 6 fi case "$network_recovery" in off) ;; fixture) test "$profile" = dual-libc && test "$boot_state" = both && test "$storage" = ram || { echo 'network fixture requires dual-libc, both absent and ram root' >&2 exit 2 } command -v openssl >/dev/null || exit 6 ;; *) echo 'NETWORK_RECOVERY must be off or fixture' >&2; exit 2 ;; esac if test "$boot_state" = remove-both && { test "$storage" = ram || test "$profile" != dual-libc || test "$network_recovery" != off; }; then echo 'remove-both requires persistent dual-libc root without network fixture' >&2 exit 2 fi case "$storage:$profile" in ram:*) ;; ext4:dual-libc|gpt-ext4:dual-libc) for tool in mke2fs qemu-img; do command -v "$tool" >/dev/null || exit 6; done if test "$storage" = gpt-ext4; then for tool in sfdisk mkfs.fat mcopy mmd; do command -v "$tool" >/dev/null || exit 6; done fi ;; *) echo 'ROOT_STORAGE must be ram, or ext4/gpt-ext4 with dual-libc' >&2; exit 2 ;; esac if test "$storage" != gpt-ext4; then command -v xorriso >/dev/null || exit 6; fi extra_packages= install_doas= case "$profile:$boot_state" in static-core:present) ;; dual-libc:present|dual-libc:glibc|dual-libc:musl|dual-libc:both|dual-libc:remove-both) glibc=$(core_input "${GLIBC_PACKAGE:?GLIBC_PACKAGE required}") musl=$(core_input "${MUSL_PACKAGE:?MUSL_PACKAGE required}") glibc_cc=$(command -v "${GLIBC_CC:-gcc}") musl_cc=$(realpath "${MUSL_CC:?MUSL_CC required}") test -x "$musl_cc" || exit 6 command -v patchelf >/dev/null || exit 6 extra_packages='glibc musl probe-glibc probe-musl' ;; *) echo 'unsupported image profile or libc boot state' >&2; exit 2 ;; esac mkdir -p "$(dirname "$image_output")" mkdir -m 0700 "$image_output" out=$(realpath "$image_output") work="$out/work" root="$out/root" mkdir "$work" "$root" "$out/packages" "$out/inputs" "$out/reports" started=$(date +%s) record="$out/build.record" printf 'format holy-bootstrap-image-1\narch %s\nprofile %s\nlibc-boot-state %s\nkernel-version %s\n' "$arch" "$profile" "$boot_state" "$version" > "$record" printf 'root-storage %s\n' "$storage" >> "$record" printf 'boot-test %s\n' "$boot_test" >> "$record" if test -n "${HOLY_IMAGE_CONFIG_SHA256:-}"; then case "$HOLY_IMAGE_CONFIG_SHA256" in *[!0-9a-f]*|'') exit 2 ;; esac test "${#HOLY_IMAGE_CONFIG_SHA256}" -eq 64 || exit 2 printf 'image-config-sha256 %s\n' "$HOLY_IMAGE_CONFIG_SHA256" >> "$record" test -n "${HOLY_IMAGE_CONFIG_FILE:-}" || exit 6 cp "$HOLY_IMAGE_CONFIG_FILE" "$out/inputs/image.conf" config_copy_hash=$(sha256sum "$out/inputs/image.conf") test "${config_copy_hash%% *}" = "$HOLY_IMAGE_CONFIG_SHA256" || { echo 'image config changed after validation' >&2 exit 3 } fi if test -n "${HOLY_IMAGE_ANSWERS:-}"; then expected_answers=${HOLY_IMAGE_ANSWERS_SHA256:-} case "$expected_answers" in *[!0-9a-f]*|'') exit 2 ;; esac test "${#expected_answers}" -eq 64 || exit 2 cp "$HOLY_IMAGE_ANSWERS" "$out/inputs/resolver-answers" answers_hash=$(sha256sum "$out/inputs/resolver-answers") test "${answers_hash%% *}" = "$expected_answers" || { echo 'resolver answers changed after validation' >&2 exit 3 } printf 'resolver-answers-sha256 %s\n' "$expected_answers" >> "$record" fi printf 'network-recovery %s\n' "$network_recovery" >> "$record" printf 'install-test %s\n' "$install_test" >> "$record" if test "$install_test" = 1; then printf 'install-firmware %s\n' "$install_firmware" >> "$record"; fi finish() { rc=$? trap - EXIT printf 'exit %s\nelapsed-seconds %s\n' "$rc" "$(($(date +%s) - started))" >> "$record" if test "$rc" -ne 0 && test "${image_untested:-0}" != 1; then printf 'result incomplete\n' >> "$record" fi exit "$rc" } trap finish EXIT trap 'exit 1' HUP INT TERM exec > "$out/build.log" 2>&1 ( set -- "$bin" "$installer" "$cc" dracut ldconfig limine sha256sum cpio gzip python3 unshare if test "$profile" = dual-libc; then set -- "$@" "$glibc_cc" "$musl_cc" patchelf; fi if test "$storage" != ram; then set -- "$@" mke2fs qemu-img; fi if test "$storage" = gpt-ext4; then set -- "$@" sfdisk mkfs.fat mcopy mmd; else set -- "$@" xorriso; fi if test "$network_recovery" = fixture; then set -- "$@" openssl; fi if test "$boot_test" = required; then set -- "$@" "$qemu"; fi python3 "$project/tools/image-host-tools.py" "$out/host-tools.jsonl" "$@" ) host_tools_hash=$(sha256sum "$out/host-tools.jsonl") printf 'host-tools-sha256 %s\n' "${host_tools_hash%% *}" >> "$record" "$bin" db init --root "$root" if test -n "${HOLY_IMAGE_SOURCE_DIR:-}"; then sh "$project/tools/image-source-stage.sh" "$bin" "$root" "$out" "$HOLY_IMAGE_SOURCE_DIR" fi "$bin" elf "$static" > "$out/core.elf" grep -qx 'runtime nolibc' "$out/core.elf" grep -qx "machine $package_arch" "$out/core.elf" "$bin" elf "$installer" > "$out/installer.elf" grep -qx 'runtime nolibc' "$out/installer.elf" grep -qx "machine $package_arch" "$out/installer.elf" "$cc" --version > "$out/compiler.record" "$cc" -std=c99 -Wall -Wextra -Werror -pedantic -Os -static -fno-pie -no-pie \ "$project/src/early-init.c" -o "$work/holy-init" "$bin" elf "$work/holy-init" > "$out/init.elf" grep -qx 'runtime nolibc' "$out/init.elf" grep -qx "machine $package_arch" "$out/init.elf" if "$work/holy-init" > "$work/init.out" 2> "$work/init.err"; then exit 1; else test "$?" -eq 2; fi tree="$work/tree" metadata() { mkdir -p "$tree/HOLY" "$tree/DATA" printf 'format holy-package-1\nname %s\nversion %s\nrelease 1\nos linux\narch %s\nlibc nolibc\n' \ "$1" "$2" "$3" > "$tree/HOLY/meta" for field in deps provides hooks origin transform; do : > "$tree/HOLY/$field"; done } pack() { "$bin" manifest generate "$tree" --output "$work/files" cp "$work/files" "$tree/HOLY/files" rm "$work/files" "$bin" pack "$tree" --output "$out/packages/$1.holy" rm -rf "$tree" } sh "$project/tools/image-package-stage.sh" "$bin" "$out" "$arch" "$@" module_probe=off if test -f "$work/core-kernel.holy"; then cp "$work/core-kernel.holy" "$out/inputs/kernel.holy" "$bin" info "local:$out/inputs/kernel.holy" > "$work/kernel-info" grep -qx 'name linux' "$work/kernel-info" && grep -qx "version $version" "$work/kernel-info" && grep -qx "arch $package_arch" "$work/kernel-info" && grep -qx 'libc nolibc' "$work/kernel-info" || { echo 'source kernel package does not match image target/version' >&2 exit 4 } "$bin" fetch "local:$out/inputs/kernel.holy" --extract \ --output "$work/kernel-package" > "$work/kernel-extract.record" kernel=$(realpath "$work/kernel-package/DATA/boot/vmlinuz") case "$kernel" in "$work/kernel-package/DATA/"*) ;; *) exit 4 ;; esac module="$work/kernel-package/DATA/usr/lib/modules/$version/kernel/drivers/net/dummy.ko" if test -f "$module"; then test ! -L "$module" && grep -qx 'kernel/drivers/net/dummy.ko:' \ "$work/kernel-package/DATA/usr/lib/modules/$version/modules.dep" || exit 4 module_probe=on module_digest=$(sha256sum "$module") module_digest=${module_digest%% *} "$cc" -std=c99 -Wall -Wextra -Werror -pedantic -Os -static -fno-pie -no-pie \ "$project/tests/module-probe.c" -o "$work/holy-module-probe" "$bin" elf "$work/holy-module-probe" > "$work/module-probe.elf" grep -qx 'runtime nolibc' "$work/module-probe.elf" grep -qx "machine $package_arch" "$work/module-probe.elf" printf 'kernel-module dummy %s\n' "$module_digest" >> "$record" fi fi test -f "$kernel" || exit 6 validate_kernel for role in busybox dinit mdevd glibc musl; do if test -f "$work/core-$role.holy"; then case "$role" in busybox) busybox="$work/core-$role.holy" ;; dinit) dinit="$work/core-$role.holy" ;; mdevd) mdevd="$work/core-$role.holy" ;; glibc) glibc="$work/core-$role.holy" ;; musl) musl="$work/core-$role.holy" ;; esac fi done test -f "$busybox" && test -f "$dinit" && test -f "$mdevd" || exit 6 if test "$profile" = dual-libc; then test -f "$glibc" && test -f "$musl" || exit 6 fi additional_packages=$(cat "$work/additional-packages") for name in busybox dinit mdevd $extra_packages; do case "$name" in busybox) input=$busybox ;; dinit) input=$dinit ;; mdevd) input=$mdevd ;; glibc) input=$glibc ;; musl) input=$musl ;; probe-*) continue ;; esac cp "$input" "$out/inputs/$name.holy" "$bin" info "local:$out/inputs/$name.holy" > "$work/input-info" grep -qx "arch $package_arch" "$work/input-info" || exit 6 parent=$(sha256sum "$out/inputs/$name.holy") parent=${parent%% *} "$bin" fetch "local:$out/inputs/$name.holy" --extract --output "$tree" test ! -s "$tree/HOLY/transform" || exit 6 if test "$name" = busybox && test "$install_test" = 1; then for applet in chown login getty su passwd adduser addgroup; do "$tree/DATA/usr/bin/busybox" --list | grep -qx "$applet" || { echo "BusyBox applet $applet required for install test" >&2 exit 6 } done fi if test "$name" = busybox && test -f "$tree/DATA/usr/share/licenses/musl/COPYRIGHT"; then mv "$tree/DATA/usr/share/licenses/musl/COPYRIGHT" "$tree/DATA/usr/share/licenses/busybox/musl.COPYRIGHT" printf '\nbootstrap-file-mapping usr/share/licenses/musl/COPYRIGHT usr/share/licenses/busybox/musl.COPYRIGHT\n' >> "$tree/HOLY/origin" fi find "$tree/DATA" -type d -exec chmod 0755 '{}' + printf '\nbootstrap-parent-sha256 %s\nbootstrap-ownership 0 0\nbootstrap-directory-mode 0755\n' "$parent" >> "$tree/HOLY/origin" pack "$name" done if test "$profile" = dual-libc; then for abi in glibc musl; do case "$abi" in glibc) compiler=$glibc_cc; needed=libc.so.6 if test "$arch" = i686; then loader=/usr/lib/holy/i686-linux-gnu/ld-linux.so.2 provider=/usr/lib/holy/i686-linux-gnu/libc.so.6 else loader=/usr/lib/holy/x86_64-linux-gnu/ld-linux-x86-64.so.2 provider=/usr/lib/holy/x86_64-linux-gnu/libc.so.6 fi ;; musl) compiler=$musl_cc; needed=libc.so if test "$arch" = i686; then loader=/usr/lib/holy/i686-linux-musl/ld-musl-i386.so.1 else loader=/usr/lib/holy/x86_64-linux-musl/ld-musl-x86_64.so.1 fi provider=$loader ;; esac metadata "probe-$abi" 1 "$package_arch" sed "s/libc nolibc/libc $abi/" "$tree/HOLY/meta" > "$work/meta" mv "$work/meta" "$tree/HOLY/meta" mkdir -p "$tree/DATA/usr/bin" probe="$tree/DATA/usr/bin/holy-probe-$abi" "$compiler" -std=c99 -Wall -Wextra -Werror -pedantic -O2 -pthread \ "-DHOLY_LIBC=\"$abi\"" "$project/tests/libc-probe.c" -o "$probe" sha256sum "$project/tests/libc-probe.c" "$probe" >> "$tree/HOLY/origin" patchelf --set-interpreter "$loader" --replace-needed "$needed" "$provider" "$probe" printf 'bootstrap-patchelf interpreter %s\nbootstrap-patchelf needed %s %s\n' "$loader" "$needed" "$provider" >> "$tree/HOLY/origin" sha256sum "$probe" >> "$tree/HOLY/origin" pack "probe-$abi" done fi metadata holypkg bootstrap "$package_arch" mkdir -p "$tree/DATA/usr/bin" "$tree/DATA/usr/share/man/man5" \ "$tree/DATA/usr/share/man/man7" "$tree/DATA/usr/share/man/man8" "$tree/DATA/usr/share/holy" cp "$static" "$out/inputs/holypkg" cp "$out/inputs/holypkg" "$tree/DATA/usr/bin/holypkg" for section in 5 7 8; do for page in "$project/man/"*."$section"; do test "${page##*/}" = holyinstall.8 || cp "$page" "$tree/DATA/usr/share/man/man$section/" done done sha256sum "$out/inputs/holypkg" >> "$tree/HOLY/origin" pack holypkg metadata holyinstall bootstrap "$package_arch" mkdir -p "$tree/DATA/usr/bin" "$tree/DATA/usr/share/man/man8" cp "$installer" "$out/inputs/holyinstall" cp "$out/inputs/holyinstall" "$tree/DATA/usr/bin/holyinstall" cp "$project/man/holyinstall.8" "$tree/DATA/usr/share/man/man8/holyinstall.8" sha256sum "$out/inputs/holyinstall" "$project/man/holyinstall.8" > "$tree/HOLY/origin" pack holyinstall if test "$install_test" = 1; then "$bin" info "local:$doas_package" > "$work/doas-info" grep -qx 'name doas' "$work/doas-info" grep -qx "arch $package_arch" "$work/doas-info" grep -qx 'libc nolibc' "$work/doas-info" cp "$doas_package" "$out/packages/doas.holy" doas_digest=$(sha256sum "$out/packages/doas.holy") doas_digest=${doas_digest%% *} printf 'guest-package doas %s\n' "$doas_digest" >> "$record" install_doas=doas fi cp "$kernel" "$out/inputs/kernel" if test -f "$work/core-kernel.holy"; then cp "$work/core-kernel.holy" "$out/packages/linux.holy" else metadata linux "$version" "$package_arch" mkdir -p "$tree/DATA/boot" cp "$out/inputs/kernel" "$tree/DATA/boot/vmlinuz" chmod 0644 "$tree/DATA/boot/vmlinuz" sha256sum "$out/inputs/kernel" > "$tree/HOLY/origin" pack linux fi metadata limine bootstrap "$package_arch" mkdir -p "$tree/DATA/usr/share/limine" set -- limine-bios.sys limine-bios-cd.bin limine-uefi-cd.bin if test "$arch" = x86_64; then set -- "$@" BOOTX64.EFI; fi for file do cp "$limine_dir/$file" "$tree/DATA/usr/share/limine/$file" chmod 0644 "$tree/DATA/usr/share/limine/$file" sha256sum "$tree/DATA/usr/share/limine/$file" >> "$tree/HOLY/origin" done limine --version >> "$tree/HOLY/origin" pack limine metadata boot-fixture 1 noarch mkdir -p "$tree/DATA/usr/share/holy" printf 'installed-in-guest\n' > "$tree/DATA/usr/share/holy/fixture-installed" pack boot-fixture metadata boot-fixture-root 1 noarch printf 'require fixture-1 boot-fixture-root package boot-fixture any any any - boot-fixture metadata\n' > "$tree/HOLY/deps" pack boot-fixture-root metadata holy-boot bootstrap "$package_arch" mkdir -p "$tree/DATA/usr/bin" "$tree/DATA/usr/lib/holy" "$tree/DATA/etc/dinit.d" \ "$tree/DATA/etc/holy" "$tree/DATA/usr/share/holy" cp "$work/holy-init" "$tree/DATA/usr/bin/holy-init" if test "$module_probe" = on; then cp "$work/holy-module-probe" "$tree/DATA/usr/bin/holy-module-probe" printf '%s\n' "$module_digest" > "$tree/DATA/etc/holy/dummy-module.sha256" sha256sum "$project/tests/module-probe.c" "$work/holy-module-probe" \ >> "$tree/HOLY/origin" fi cp "$project/profiles/dinit/"* "$tree/DATA/etc/dinit.d/" cp "$project/tests/boot-probe.sh" "$tree/DATA/usr/lib/holy/boot-probe.sh" cp "$project/tests/install-probe.sh" "$tree/DATA/usr/lib/holy/install-probe.sh" cp "$project/tools/install-source-stage.sh" \ "$tree/DATA/usr/lib/holy/install-source-stage.sh" chmod 0644 "$tree/DATA/usr/lib/holy/boot-probe.sh" chmod 0644 "$tree/DATA/usr/lib/holy/install-probe.sh" chmod 0644 "$tree/DATA/usr/lib/holy/install-source-stage.sh" cp "$out/packages/boot-fixture.holy" "$tree/DATA/usr/share/holy/fixture.holy" chmod 0644 "$tree/DATA/usr/share/holy/fixture.holy" cp "$out/packages/boot-fixture-root.holy" "$tree/DATA/usr/share/holy/fixture-root.holy" chmod 0644 "$tree/DATA/usr/share/holy/fixture-root.holy" printf '%s\n' "$version" > "$tree/DATA/etc/holy/kernel-version" printf '%s\n' "$profile" > "$tree/DATA/etc/holy/image-profile" case "$storage" in gpt-ext4) printf 'ext4\n' ;; *) printf '%s\n' "$storage" ;; esac > "$tree/DATA/etc/holy/root-storage" if test "$storage" = gpt-ext4; then printf '/dev/vda2\n' > "$tree/DATA/etc/holy/esp-device"; fi printf '%s\n' "$boot_state" > "$tree/DATA/etc/holy/libc-boot-state" printf '%s\n' "$network_recovery" > "$tree/DATA/etc/holy/network-recovery" if test "$network_recovery" = fixture; then mkdir "$out/network" printf 'nameserver 10.0.2.3\noptions timeout:2 attempts:2\n' > "$tree/DATA/etc/resolv.conf" openssl req -x509 -newkey rsa:2048 -nodes -days 2 \ -keyout "$out/network/key.pem" -out "$out/network/ca.pem" \ -subj '/CN=Holy recovery fixture' \ -addext 'subjectAltName=DNS:fixture.holy.test,IP:10.0.2.2' \ > "$work/openssl.out" 2> "$work/openssl.err" chmod 0600 "$out/network/key.pem" cp "$out/network/ca.pem" "$tree/DATA/etc/holy/recovery-ca.pem" sha256sum "$out/network/ca.pem" >> "$record" fi if test "$profile" = dual-libc; then for name in $extra_packages; do hash=$(sha256sum "$out/packages/$name.holy") printf '%s\n' "${hash%% *}" > "$tree/DATA/etc/holy/$name.sha256" source_id=$(awk -v label="$name" '$1 == label {print $2}' "$work/add-sources") if test -n "$source_id"; then printf '%s\n' "$source_id" > "$tree/DATA/etc/holy/$name.source-id" fi done fi printf 'root:x:0:0:root:/root:/bin/sh\n' > "$tree/DATA/etc/passwd" printf 'root:x:0:\n' > "$tree/DATA/etc/group" printf 'root:!:0:0:99999:7:::\n' > "$tree/DATA/etc/shadow" chmod 0600 "$tree/DATA/etc/shadow" if test "$install_test" = 1; then printf 'holytest:x:10001:10001:Holy fixture account:/home/holytest:/usr/bin/sh\n' >> "$tree/DATA/etc/passwd" printf 'holytest:x:10001:\n' >> "$tree/DATA/etc/group" cat >> "$tree/DATA/etc/shadow" <<'EOF' holytest:$6$holyfixture$dWRvmlTx76Ezgh55faR0FP7brdbDJrBSlGfNEW5bbcQDRvj4uwCOeDgUSHkXHQEedvoyZH55dtVIl9Aie1eMh.:0:0:99999:7::: EOF printf 'permit holytest as root cmd /usr/bin/busybox args id -u\n' > "$tree/DATA/etc/doas.conf" chmod 0400 "$tree/DATA/etc/doas.conf" printf '%s\n' "$doas_digest" > "$tree/DATA/etc/holy/doas.sha256" "$cc" -O2 -std=c99 -Wall -Wextra -Werror -pedantic -static \ "$project/tests/login-probe.c" -o "$tree/DATA/usr/lib/holy/login-probe" "$bin" elf "$tree/DATA/usr/lib/holy/login-probe" > "$out/login-probe.elf" grep -qx 'runtime nolibc' "$out/login-probe.elf" sha256sum "$project/tests/login-probe.c" "$tree/DATA/usr/lib/holy/login-probe" \ >> "$tree/HOLY/origin" fi printf 'null 0:0 0600\n.* 0:0 0600\n' > "$tree/DATA/etc/mdev.conf" for name in bin sbin; do ln -s usr/bin "$tree/DATA/$name"; done for name in lib lib32 lib64; do ln -s "usr/$name" "$tree/DATA/$name"; done ln -s bin "$tree/DATA/usr/sbin" ln -s dinit "$tree/DATA/usr/bin/init" ln -s busybox "$tree/DATA/usr/bin/sh" ln -s usr/bin/holy-init "$tree/DATA/init" sha256sum "$project/src/early-init.c" "$project/tests/boot-probe.sh" \ "$project/tests/install-probe.sh" \ "$project/tools/install-source-stage.sh" \ "$project/profiles/dinit/"* > "$tree/HOLY/origin" pack holy-boot metadata holy-base bootstrap noarch : > "$work/package-names" for name in busybox dinit mdevd holypkg holyinstall linux limine holy-boot $extra_packages $install_doas $additional_packages; do "$bin" info "local:$out/packages/$name.holy" > "$work/package-info" actual_name=$(sed -n 's/^name //p' "$work/package-info") case "$actual_name" in ''|*[!a-zA-Z0-9._+-]*) echo 'unsupported bootstrap package name' >&2; exit 6 ;; esac if grep -Fxq -e "$actual_name" "$work/package-names"; then echo "duplicate image package name $actual_name" >&2 exit 4 fi printf '%s\n' "$actual_name" >> "$work/package-names" printf 'require base-%s holy-base package %s any any any - %s metadata\n' "$name" "$actual_name" "$actual_name" >> "$tree/HOLY/deps" done pack holy-base mkdir -p "$root/usr/bin" "$root/usr/lib/holy" "$root/usr/lib32" "$root/usr/lib64" \ "$root/usr/share/man/man5" "$root/usr/share/man/man7" "$root/usr/share/man/man8" "$root/usr/share/holy" \ "$root/usr/share/licenses/busybox" "$root/usr/share/licenses/musl" \ "$root/usr/share/licenses/dinit" "$root/usr/share/licenses/mdevd" \ "$root/usr/share/licenses/skalibs" "$root/usr/share/doc/mdevd" \ "$root/usr/include/mdevd" "$root/usr/share/limine" "$root/etc/dinit.d" \ "$root/etc/holy" "$root/boot" "$root/dev" "$root/proc" "$root/sys" \ "$root/run" "$root/tmp" "$root/root" if test "$profile" = dual-libc; then if test "$arch" = i686; then mkdir -p "$root/usr/lib/holy/i686-linux-gnu" "$root/usr/lib/holy/i686-linux-musl" \ "$root/usr/share/licenses/glibc-i686" "$root/usr/share/doc/glibc-i686" \ "$root/usr/share/licenses/musl-i686" "$root/usr/share/doc/musl-i686" else mkdir -p "$root/usr/lib/holy/x86_64-linux-gnu" "$root/usr/lib/holy/x86_64-linux-musl" \ "$root/usr/share/licenses/glibc" "$root/usr/share/doc/glibc" "$root/usr/share/doc/musl" fi fi python3 - "$root" "$work/install.conf" <<'PY' import sys def quoted(value): return '"' + ''.join('\\x%02x' % ord(char) if ord(char) < 32 or ord(char) == 127 else '\\' + char if char in ('"', '\\') else char for char in value) + '"' with open(sys.argv[2], 'w', encoding='utf-8') as output: output.write('[install]\nroot ' + quoted(sys.argv[1]) + '\n') PY accepted_arch= for name in holy-base busybox dinit mdevd holypkg holyinstall linux limine holy-boot $extra_packages $install_doas $additional_packages; do package="$out/packages/$name.holy" digest=$(sha256sum "$package") digest=${digest%% *} printf 'package %s %s\n' "$name" "$digest" >> "$record" "$bin" cache stage "local:$package" --root "$root" printf 'artifact %s\n' "$digest" >> "$work/install.conf" if test -f "$work/add-sources"; then source_id=$(awk -v label="$name" '$1 == label {print $2}' "$work/add-sources") if test -n "$source_id"; then printf 'source %s %s\n' "$digest" "$source_id" >> "$work/install.conf" fi fi if test "$arch" = i686; then "$bin" info "local:$package" > "$work/package-info" if grep -qx 'arch x86' "$work/package-info"; then accepted_arch="$accepted_arch $digest"; fi fi done if test "$install_test" = 1; then printf 'accept-privileged %s\n' "$doas_digest" >> "$work/install.conf" fi for digest in $accepted_arch; do printf 'architecture-placement host x86_64 target x86 artifact %s accepted-unverified\n' "$digest" >> "$record" printf 'accept-arch %s\n' "$digest" >> "$work/install.conf" done "$installer" --config "$work/install.conf" --plan "$out/install.plan" --holypkg "$bin" > "$out/install.preview" cat "$out/install.preview" plan=$(sed -n 's/^set-sha256 \([0-9a-f]*\)$/\1/p' "$out/install.plan") test "${#plan}" -eq 64 printf 'install-plan %s\n' "$plan" >> "$record" install_plan_file=$(sha256sum "$out/install.plan") printf 'install-plan-file-sha256 %s\n' "${install_plan_file%% *}" >> "$record" "$installer" --apply "$out/install.plan" --holypkg "$bin" > "$out/install.apply" test "$(cat "$root/var/lib/holypkg/generation")" -eq 1 "$bin" db check --all --root "$root" > "$out/root-check.record" if test "$install_test" = 1; then storage_tools=$(realpath "${STORAGE_TOOLS_PACKAGE:?STORAGE_TOOLS_PACKAGE required}") "$bin" info "local:$storage_tools" > "$work/storage-info" grep -qx 'name holy-storage-tools' "$work/storage-info" grep -qx "arch $package_arch" "$work/storage-info" grep -qx 'libc nolibc' "$work/storage-info" cp "$storage_tools" "$out/inputs/holy-storage-tools.holy" storage_parent=$(sha256sum "$out/inputs/holy-storage-tools.holy") storage_parent=${storage_parent%% *} "$bin" fetch "local:$out/inputs/holy-storage-tools.holy" --extract --output "$tree" find "$tree/DATA" -type d -exec chmod 0755 '{}' + printf '\nbootstrap-parent-sha256 %s\nbootstrap-ownership 0 0\nbootstrap-directory-mode 0755\n' \ "$storage_parent" >> "$tree/HOLY/origin" pack holy-storage-tools mkdir -p "$root/usr/share/man/man1" "$root/usr/share/licenses/holy-storage-tools" storage_digest=$(sha256sum "$out/packages/holy-storage-tools.holy") storage_digest=${storage_digest%% *} printf 'live-only-package holy-storage-tools %s parent %s\n' \ "$storage_digest" "$storage_parent" >> "$record" "$bin" cache stage "local:$out/packages/holy-storage-tools.holy" --root "$root" printf '[install]\nroot "%s"\nartifact %s\n' "$root" "$storage_digest" > "$work/storage.conf" if test "$arch" = i686; then printf 'accept-arch %s\n' "$storage_digest" >> "$work/storage.conf" fi "$installer" --config "$work/storage.conf" --plan "$out/storage.plan" \ --holypkg "$bin" > "$out/storage.preview" "$installer" --apply "$out/storage.plan" --holypkg "$bin" > "$out/storage.apply" "$bin" db check --all --root "$root" > "$out/root-check.record" fi "$bin" docs --root "$root" --output "$root/usr/share/holy/llm.txt" chmod 0644 "$root/usr/share/holy/llm.txt" cp "$root/usr/share/holy/llm.txt" "$out/llm.txt" docs_hash=$(sha256sum "$out/llm.txt") printf '%s\n' "${docs_hash%% *}" > "$root/etc/holy/docs.sha256" printf 'documentation-sha256 %s\n' "${docs_hash%% *}" >> "$record" tail -n 1 "$out/llm.txt" > "$out/docs.record" if test "$install_test" = 1; then sed -n 's/^artifact //p' "$work/install.conf" > "$root/usr/share/holy/install-artifacts" sed -n 's/^source /source /p' "$work/install.conf" \ > "$root/usr/share/holy/install-source-bindings" if test -f "$out/inputs/sources.conf"; then cp "$out/inputs/sources.conf" "$root/usr/share/holy/install-sources.conf" cp "$out/inputs/source-aliases" "$root/usr/share/holy/install-source-aliases" sha256sum "$root/usr/share/holy/install-sources.conf" \ "$root/usr/share/holy/install-source-aliases" >> "$record" else test ! -s "$root/usr/share/holy/install-source-bindings" || exit 6 fi python3 - "$root" "$root/usr/share/holy/install-directories" <<'PY' import os import pathlib import stat import sys root = pathlib.Path(sys.argv[1]) with open(sys.argv[2], 'w', encoding='utf-8') as output: for parent, dirs, files in os.walk(root): for name in sorted(dirs): path = pathlib.Path(parent) / name if path.is_symlink(): continue relative = path.relative_to(root) if relative.parts[:3] in (('var', 'lib', 'holypkg'), ('var', 'cache', 'holypkg')): continue if any(char.isspace() for char in str(relative)): raise SystemExit('unsupported installation directory name') mode = stat.S_IMODE(path.stat().st_mode) output.write(f'{mode:04o} {relative}\n') PY sha256sum "$root/usr/share/holy/install-artifacts" \ "$root/usr/share/holy/install-directories" \ "$root/usr/share/holy/install-source-bindings" >> "$record" fi if test "$network_recovery" = fixture; then for abi in glibc musl; do digest=$(cat "$root/etc/holy/$abi.sha256") cache="$root/var/cache/holypkg/objects/sha256/$digest.holy" test -f "$cache" && cmp "$cache" "$out/packages/$abi.holy" cp "$cache" "$out/network/$abi.holy" rm "$cache" printf 'network-only-artifact %s %s\n' "$abi" "$digest" >> "$record" done fi for abi in glibc musl; do case "$boot_state:$abi" in both:*|glibc:glibc|musl:musl) case "$abi" in glibc) if test "$arch" = i686; then paths='usr/lib/holy/i686-linux-gnu/libc.so.6 usr/lib/holy/i686-linux-gnu/ld-linux.so.2 usr/lib/ld-linux.so.2' else paths='usr/lib/holy/x86_64-linux-gnu/libc.so.6 usr/lib/holy/x86_64-linux-gnu/ld-linux-x86-64.so.2 usr/lib64/ld-linux-x86-64.so.2' fi ;; musl) if test "$arch" = i686; then paths='usr/lib/holy/i686-linux-musl/ld-musl-i386.so.1 usr/lib/ld-musl-i386.so.1' else paths='usr/lib/holy/x86_64-linux-musl/ld-musl-x86_64.so.1 usr/lib/ld-musl-x86_64.so.1' fi ;; esac for path in $paths; do rm "$root/$path" printf 'omitted-payload %s\n' "$path" >> "$record" done ;; esac done sha256sum "$project/tools/bootstrap-image.sh" "$project/profiles/dracut/module-setup.sh" >> "$record" ( cd "$out" set -- inputs packages if test -d mirrors; then set -- "$@" mirrors; fi find "$@" -type f -print0 | sort -z | xargs -0 sha256sum > input-lock.sha256 ) input_lock=$(sha256sum "$out/input-lock.sha256") printf 'input-lock-sha256 %s\n' "${input_lock%% *}" >> "$record" if test "$storage" = gpt-ext4; then truncate -s 1G "$out/disk.raw" printf '[disk]\nimage "%s"\nlayout gpt-ext4\n' "$out/disk.raw" > "$work/disk.conf" "$installer" disk plan --config "$work/disk.conf" --output "$out/disk.plan" cat "$out/disk.plan" sha256sum "$out/disk.plan" >> "$record" fi cp "$record" "$out/plan" plan=$(sha256sum "$out/plan") plan=${plan%% *} printf '%s\n' "$plan" > "$root/etc/holy/boot-plan" printf '%s\n' "$plan" > "$out/boot-plan" root_disk= root_cmdline= if test "$storage" != ram; then root_disk="$out/root.ext4" if test "$storage" = gpt-ext4; then truncate -s 765M "$root_disk" else truncate -s 512M "$root_disk" fi mke2fs -q -t ext4 -F -d "$root" "$root_disk" chmod 0444 "$root_disk" sha256sum "$root_disk" >> "$record" root_cmdline='holy.root=/dev/vda holy.rootfstype=ext4' if test "$storage" = gpt-ext4; then root_cmdline='holy.root=/dev/vda3 holy.rootfstype=ext4 holy.esp=/dev/vda2'; fi fi mkdir -p "$work/dracut/modules.d/90holy" "$work/dracut/dracut.conf.d" "$work/empty-conf" dracut_base=${DRACUT_BASE:-/usr/lib64/dracut} for file in dracut-functions.sh dracut-logger.sh dracut-install dracut-util dracut-cpio; do test ! -e "$dracut_base/$file" || cp "$dracut_base/$file" "$work/dracut/" done cp "$project/profiles/dracut/module-setup.sh" "$work/dracut/modules.d/90holy/" mkdir -p "$root/usr/lib/modules/$version" "$work/dracut-tmp" HOLY_ROOT="$root" DRACUT_LDCONFIG='ldconfig -X' DRACUT_NO_MKNOD=1 DRACUT_TESTBIN=/usr/bin/busybox dracutbasedir="$work/dracut" dracut --conf /dev/null \ --sysroot "$root" --tmpdir "$work/dracut-tmp" \ --confdir "$work/empty-conf" --modules holy --no-kernel --no-hostonly \ --no-hostonly-cmdline --no-early-microcode --nohardlink --nostrip --gzip \ "$out/initramfs.img" "$version" mkdir "$work/audit" gzip -dc "$out/initramfs.img" > "$work/initramfs.cpio" (cd "$work/audit" && cpio -id --no-absolute-filenames < "$work/initramfs.cpio") python3 - "$root" "$work/audit" "$bin" "$profile" "$arch" "$version" > "$out/initramfs.audit" <<'PY' import hashlib, os, pathlib, stat, subprocess, sys root, unpacked = map(pathlib.Path, sys.argv[1:3]) dynamic = set() if sys.argv[4] == 'dual-libc': if sys.argv[5] == 'i686': dynamic.update({'usr/lib/holy/i686-linux-gnu/libc.so.6', 'usr/lib/holy/i686-linux-gnu/ld-linux.so.2', 'usr/lib/holy/i686-linux-musl/ld-musl-i386.so.1'}) else: dynamic.update({'usr/lib/holy/x86_64-linux-gnu/libc.so.6', 'usr/lib/holy/x86_64-linux-gnu/ld-linux-x86-64.so.2', 'usr/lib/holy/x86_64-linux-musl/ld-musl-x86_64.so.1'}) dynamic.update({'usr/bin/holy-probe-glibc', 'usr/bin/holy-probe-musl'}) generated = {'etc/ld.so.cache', 'var/cache/ldconfig/aux-cache', 'usr/lib/dracut/modules.txt', 'usr/lib/dracut/build-parameter.txt'} def digest(path): h = hashlib.sha256() with path.open('rb') as f: for block in iter(lambda: f.read(1048576), b''): h.update(block) return h.digest() for parent, dirs, files in os.walk(unpacked): for name in dirs + files: path = pathlib.Path(parent) / name relative = path.relative_to(unpacked) original = root / relative actual = path.lstat() mode = actual.st_mode if stat.S_ISDIR(mode): continue if relative.as_posix() in generated: if not stat.S_ISREG(mode): raise SystemExit('invalid generated file: ' + str(relative)) continue before = original.lstat() if (mode, actual.st_uid, actual.st_gid) != (before.st_mode, before.st_uid, before.st_gid): raise SystemExit('changed mode or owner: ' + str(relative)) if stat.S_ISLNK(mode): if os.readlink(path) != os.readlink(original): raise SystemExit('changed link: ' + str(relative)) elif stat.S_ISREG(mode): if digest(path) != digest(original): raise SystemExit('changed content: ' + str(relative)) with path.open('rb') as f: elf = f.read(4) == b'\x7fELF' if elf: facts = subprocess.check_output([sys.argv[3], 'elf', str(path)], text=True) module = (len(relative.parts) >= 5 and relative.parts[:3] == ('usr', 'lib', 'modules') and relative.parts[3] == sys.argv[6] and relative.suffix == '.ko') if module: machine = 'x86' if sys.argv[5] == 'i686' else 'x86_64' if 'e_type 1' not in facts.splitlines() or \ 'machine ' + machine not in facts.splitlines(): raise SystemExit('invalid kernel module: ' + str(relative)) print('kernel-module-elf', relative) elif relative.as_posix() in dynamic: print('dynamic-fixture-elf', relative) elif 'runtime nolibc' not in facts.splitlines(): raise SystemExit('non-static ELF: ' + str(relative)) else: print('static-elf', relative) else: raise SystemExit('unexpected object: ' + str(relative)) for parent, dirs, files in os.walk(root): for name in dirs + files: relative = (pathlib.Path(parent) / name).relative_to(root) if not os.path.lexists(unpacked / relative): raise SystemExit('missing path: ' + str(relative)) print('result pass') PY rm "$work/initramfs.cpio" mkdir -p "$work/iso/boot/limine" "$work/iso/EFI/BOOT" cp "$root/boot/vmlinuz" "$work/iso/boot/vmlinuz" cp "$out/initramfs.img" "$work/iso/boot/initramfs.img" cp "$root/usr/share/limine/"*.bin "$root/usr/share/limine/limine-bios.sys" "$work/iso/boot/limine/" if test "$arch" = x86_64; then cp "$root/usr/share/limine/BOOTX64.EFI" "$work/iso/EFI/BOOT/"; fi boot_service=holy.test=1 if test "$install_test" = 1; then boot_service=holy.install-test=1; fi cat > "$work/iso/boot/limine/limine.conf" < "$out/disk-layout.json" python3 - "$out/disk-layout.json" <<'PY' import json, sys table = json.load(open(sys.argv[1]))['partitiontable'] assert table['label'] == 'gpt' and table['sectorsize'] == 512 assert [(p['start'], p['size']) for p in table['partitions']] == [ (2048, 2048), (4096, 524288), (528384, 1566720)] PY mkfs.fat -C -F 32 -s 4 -n HOLYBOOT "$work/esp.fat" 262144 mmd -i "$work/esp.fat" ::/EFI ::/EFI/BOOT if test "$arch" = x86_64; then mcopy -i "$work/esp.fat" "$root/usr/share/limine/BOOTX64.EFI" ::/EFI/BOOT/BOOTX64.EFI fi mcopy -i "$work/esp.fat" "$root/usr/share/limine/limine-bios.sys" ::/limine-bios.sys mcopy -i "$work/esp.fat" "$root/boot/vmlinuz" ::/vmlinuz mcopy -i "$work/esp.fat" "$out/initramfs.img" ::/initramfs.img sed -e 's@boot():/boot/vmlinuz@boot():/vmlinuz@' \ -e 's@boot():/boot/initramfs.img@boot():/initramfs.img@' \ "$work/iso/boot/limine/limine.conf" > "$out/limine.conf" mcopy -i "$work/esp.fat" "$out/limine.conf" ::/limine.conf for file in vmlinuz initramfs.img limine.conf; do mcopy -i "$work/esp.fat" "::/$file" "$work/readback" case "$file" in vmlinuz) cmp "$root/boot/vmlinuz" "$work/readback" ;; *) cmp "$out/$file" "$work/readback" ;; esac rm "$work/readback" done "$installer" disk finalize-plan --disk-plan "$out/disk.plan" \ --esp "$work/esp.fat" --root-image "$root_disk" \ --output "$out/disk-finalize.plan" sha256sum "$out/disk-finalize.plan" >> "$record" "$installer" disk finalize-apply --plan "$out/disk-finalize.plan" \ --confirm "$out/disk.raw" test "$(tail -n 1 "$out/disk-finalize.plan.journal")" = committed sfdisk --verify "$out/disk.raw" chmod 0444 "$out/disk.raw" root_disk="$out/disk.raw" sha256sum "$out/disk.raw" "$out/disk-layout.json" "$out/limine.conf" >> "$record" else if test "$arch" = i686; then xorriso -as mkisofs -R -r -J -b boot/limine/limine-bios-cd.bin \ -no-emul-boot -boot-load-size 4 -boot-info-table \ "$work/iso" -o "$iso_image" else xorriso -as mkisofs -R -r -J -b boot/limine/limine-bios-cd.bin \ -no-emul-boot -boot-load-size 4 -boot-info-table \ --efi-boot boot/limine/limine-uefi-cd.bin -efi-boot-part --efi-boot-image \ --protective-msdos-label "$work/iso" -o "$iso_image" fi if test "$arch" = x86_64; then limine bios-install "$iso_image" else printf 'bios-el-torito optical-only\n' >> "$record" fi sha256sum "$iso_image" >> "$record" fi sha256sum "$out/initramfs.img" "$root/boot/vmlinuz" >> "$record" if test "$boot_test" = build-only; then printf 'result untested\nnot-tested qemu-boot-contract\n' >> "$record" image_untested=1 exit 6 fi if test "$install_test" = 1; then ARCH="$arch" ISO="$iso_image" BOOT_PLAN="$plan" REPORT_DIR="$out/reports" \ STATIC_HOLYINSTALL="$installer" INSTALL_FIRMWARE="$install_firmware" \ python3 "$project/tests/install-vm.py" else ARCH="$arch" BOOT_MEDIA="$boot_media" ISO="$iso_image" BOOT_PLAN="$plan" REPORT_DIR="$out/reports" \ IMAGE_PROFILE="$profile" LIBC_BOOT_STATE="$boot_state" \ NETWORK_RECOVERY="$network_recovery" NETWORK_DIR="$out/network" \ ROOT_DISK="$root_disk" \ KERNEL_IMAGE="$root/boot/vmlinuz" KERNEL_VERSION="$version" INITRAMFS="$out/initramfs.img" \ KERNEL_MODULE_PROBE="$module_probe" \ sh "$project/tests/qemu.sh" fi printf 'result boot-tested-%s\n' "$profile" >> "$record" if test "$storage" = ram; then printf 'not-tested libc-recovery-reboot\n' >> "$record"; fi if test "$install_test" = 1; then printf 'not-tested installer-interactive-menu pam-nss network graphics\n' >> "$record" elif test "$network_recovery" = fixture; then printf 'not-tested installer-full-flow public-network-dns graphics\n' >> "$record" else printf 'not-tested installer-full-flow network graphics\n' >> "$record" fi if test "$module_probe" = off; then printf 'not-tested kernel-module-load\n' >> "$record"; fi