summaryrefslogtreecommitdiff
path: root/bin/debefivm-ukify
diff options
context:
space:
mode:
authorHelmut Grohne <helmut@subdivi.de>2025-04-06 07:59:25 +0200
committerHelmut Grohne <helmut@subdivi.de>2025-04-06 17:44:08 +0200
commit6f2a356ca10ab97ed257097996593d707eca9bd7 (patch)
tree4ba99a0bfa2c9f5967b47c17ab4e819756a0f787 /bin/debefivm-ukify
parent1c6688cb9aeffdf2027003423d0ab792045952a8 (diff)
downloaddebvm-6f2a356ca10ab97ed257097996593d707eca9bd7.tar.gz
add a new family of wrappers for EFI based images
debefivm-create is based on mmdebstrap-autopkgtest-build-qemu, which is is co-authored with Johannes Schauer Marin Rodrigues. Also thanks to Jochen Sprickerhof for suggesting the --rootsize option for use in Debusine.
Diffstat (limited to 'bin/debefivm-ukify')
-rwxr-xr-xbin/debefivm-ukify329
1 files changed, 329 insertions, 0 deletions
diff --git a/bin/debefivm-ukify b/bin/debefivm-ukify
new file mode 100755
index 0000000..9267158
--- /dev/null
+++ b/bin/debefivm-ukify
@@ -0,0 +1,329 @@
+#!/bin/sh
+# SPDX-FileCopyrightText: 2023-2025 Helmut Grohne <helmut@subdivi.de>
+# SPDX-License-Identifier: MIT
+#
+
+: <<'POD2MAN'
+=head1 NAME
+
+debefivm-ukify - Create Unified Kernel Images for UEFI systems in a similar way to systemd's ukify
+
+=head1 SYNOPSIS
+
+B<debefivm-ukify> B<build> B<--linux>=F<vmlinuz> B<--output>=F<uki.efi> [options]
+
+=head1 DESCRIPTION
+
+B<debefivm-ukify> is a reimplementation of parts of B<systemd-ukify> and mainly meant for backports.
+Please prefer B<systemd>'s implementation.
+It only supports the B<build> subcommand, does not implement any functionality related to secure boot and lacks configuration file support.
+Other than that, it can be used to assemble a linux kernel image, an initrd and an EFI stub image from B<systemd> into an EFI-bootable Unified Kernel Image.
+
+=head1 OPTIONS
+
+=over 8
+
+=item B<--deb-arch>=I<debian_architecture>
+
+Set the architecture of the supplied and produced artifacts using Debian's architecture nomenclature.
+See B<--efi-arch> for another supplying it using the EFI nomenclature instead.
+This option is not available on the B<systemd> implementation.
+
+=item B<--devicetree>=F<devicetree_blob>
+
+Add a device tree file as a dtb section to the generated UKI.
+There can be at most one device tree.
+
+=item B<--cmdline>=I<kernel_cmdline>
+
+Supply extra arguments to be appended to the Linux kernel command line during boot.
+Can be given given at most once.
+If the value starts with an B<@>, it is treated as a filename whose contents will be added.
+
+=item B<--efi-arch>=I<efi_architecture>
+
+Set the architecture of the supplied and produced artifacts using EFI's architecture nomenclature.
+See B<--deb-arch> for another supplying it using the Debian nomenclature instead.
+
+=item B<--initrd>=F<initrd>
+
+Supply an initrd image to be embedded.
+There can be at most one initrd.
+
+=item B<--linux>=F<vmlinuz>
+
+Supply a Linux kernel image to be embedded.
+This option must be given exactly once.
+
+=item B<--os-release>=I<release_information>
+
+Set the os-release description of in the UKI.
+Can be given given at most once.
+If the value starts with an B<@>, it is treated as a filename whose contents will be added.
+
+=item B<--output>=<uki.efi>
+
+Set the name of the resulting UKI image.
+This option must be given exactly once and the target file must not exist already.
+
+=item B<--section>=<name:value>
+
+Add an arbitrary ELF section to the resulting UKI.
+If the value starts with an B<@>, it is treated as a filename whose contents will be added.
+Most image components actually are such ELF sections.
+For instance B<--os-release>, adds a section named B<osrel>.
+
+=item B<--stub>=F<efi_stub>
+
+Supply the location of B<systemd>'s EFI stub image.
+Unlike the B<systemd> implementation, this option accepts a directory containing stubs named by architecture.
+By default, stubs will be looked up in B</usr/lib/systemd/boot/efi>.
+
+=item B<--verbose>
+
+Print diagnostic information during image conversion.
+This option is not available on the B<systemd> implementation.
+
+=back
+
+=head1 SEE ALSO
+
+ ukify(1)
+
+=cut
+POD2MAN
+
+set -eu
+
+die() {
+ echo "$*" >&2
+ exit 1
+}
+usage() {
+ die "usage: $0 [build] <--linux=|--initrd=|--stub=> [--cmdline=|--deb-arch=|--devicetree=|--efi-arch=|--os-release=|--output=|--section=|--verbose]"
+}
+usage_error() {
+ echo "error: $*" >&2
+ usage
+}
+
+DEBARCH=
+BOOTSTUB=/usr/lib/systemd/boot/efi
+OUTPUT=
+VERBOSE=
+
+TDIR=
+cleanup() {
+ if test -n "$TDIR"; then
+ rm -Rf "$TDIR"
+ fi
+}
+cleanup_abort() {
+ cleanup
+ echo aborted >&2
+ exit 2
+}
+trap cleanup EXIT
+trap cleanup_abort HUP INT TERM QUIT
+
+# The TDIR will contain one file per section. It can be a symbolic link or
+# regular file. The section name is the filename with a leading dot.
+TDIR=$(mktemp --directory --tmpdir mini-ukify.XXXXXXXXXX)
+
+add_section_content() {
+ test -e "$TDIR/$1" && die "section '$1' already defined"
+ printf "%s" "$2" > "$TDIR/$1"
+}
+add_section_file() {
+ test -e "$TDIR/$1" && die "section '$1' already defined"
+ test -f "$2" || die "file '$2' does not exist"
+ if test "${2#/}" = "$2"; then
+ ln --symbolic --relative "$2" "$TDIR/$1"
+ else
+ ln --symbolic "$2" "$TDIR/$1"
+ fi
+}
+add_section_either() {
+ if test "${2#@}" = "$2"; then
+ add_section_content "$1" "$2"
+ else
+ add_section_file "$1" "${2#@}"
+ fi
+}
+
+opt_cmdline() {
+ add_section_either cmdline "$1"
+}
+opt_deb_arch() {
+ case "$1" in
+ amd64)
+ EFIARCH=x64
+ GNU_TYPE=x86_64-linux-gnu
+ ;;
+ arm64)
+ EFIARCH=aa64
+ GNU_TYPE=aarch64-linux-gnu
+ ;;
+ armhf)
+ EFIARCH=arm
+ GNU_TYPE=arm-linux-gnueabihf
+ ;;
+ i386)
+ EFIARCH=ia32
+ GNU_TYPE=i686-linux-gnu
+ ;;
+ riscv64)
+ EFIARCH=riscv64
+ GNU_TYPE=riscv64-linux-gnu
+ ;;
+ *)
+ die "unsupported Debian architecture: $1"
+ ;;
+ esac
+ DEBARCH="$1"
+}
+opt_devicetree() {
+ add_section_file dtb "$1"
+}
+opt_efi_arch() {
+ case "$1" in
+ aa64) opt_deb_arch arm64 ;;
+ arm) opt_deb_arch armhf ;;
+ ia32) opt_deb_arch i386 ;;
+ x64) opt_deb_arch amd64 ;;
+ riscv64) opt_deb_arch "$1" ;;
+ *)
+ die "unsupported EFI architecture: $1"
+ ;;
+ esac
+}
+opt_initrd() {
+ add_section_file initrd "$1"
+}
+opt_linux() {
+ add_section_file linux "$1"
+ test -z "$OUTPUT" && OUTPUT="$1.unsigned.efi"
+}
+opt_os_release() {
+ add_section_either osrel "$1"
+}
+opt_output() {
+ OUTPUT="$1"
+}
+opt_section() {
+ case "$1" in
+ *[!a-z]*:*)
+ die "invalid section name '${1%%:*}'"
+ ;;
+ *:*)
+ ;;
+ *)
+ die "missing section name separated by colon in '$1'"
+ ;;
+ esac
+ add_section_either "${1%%:*}" "${1#*:}"
+}
+opt_stub() {
+ BOOTSTUB="$1"
+}
+opt_verbose() {
+ VERBOSE=yes
+}
+positional=1
+positional_1() {
+ test "$1" = "build" || usage_error "command not understood: '$1'"
+}
+positional_2() {
+ usage_error "too many positional arguments"
+}
+
+while test "$#" -gt 0; do
+ case "$1" in
+ --cmdline=*|--deb-arch=*|--devicetree=*|--efi-arch=*|--initrd=*|--linux=*|--os-release=*|--output=*|--section=*|--stub=*)
+ optname="${1%%=*}"
+ optname="${optname#--}"
+ test "${optname#*-}" = "$optname" ||
+ optname="${optname%%-*}_${optname#*-}"
+ "opt_$optname" "${1#*=}"
+ ;;
+ --cmdline|--deb-arch|--devicetree|--efi-arch|--initrd|--linux|--os-release|--output|--section|--stub)
+ test "$#" -ge 2 || usage_error "missing argument for $1"
+ optname="${1#--}"
+ test "${optname#*-}" = "$optname" ||
+ optname="${optname%%-*}_${optname#*-}"
+ "opt_$optname" "$2"
+ shift
+ ;;
+ --verbose)
+ "opt_${1#--}"
+ ;;
+ --*)
+ usage_error "unrecognized option $1"
+ ;;
+ *)
+ "positional_$positional" "$1"
+ positional=$((positional + 1))
+ ;;
+ esac
+ shift
+done
+
+test "$positional" = 2 || usage_error "missing subcommand, only 'build' is supported"
+test -z "$DEBARCH" && opt_deb_arch "$(dpkg --print-architecture)"
+test -e "$TDIR/linux" || usage_error "missing --linux argument"
+test -e "$BOOTSTUB" || die "stub image or directory '$BOOTSTUB' does not exist"
+test -e "$OUTPUT" && die "output '$OUTPUT' already exists"
+
+test -d "$BOOTSTUB" && BOOTSTUB="$BOOTSTUB/linux$EFIARCH.efi.stub"
+test -f "$BOOTSTUB" || die "efi boot stub $BOOTSTUB not found"
+
+GNU_PREFIX="$GNU_TYPE-"
+test "$(dpkg-query --showformat='${db:Status-Status}' --show binutils-multiarch)" = installed &&
+ GNU_PREFIX=
+
+# Compute the next multiple of $2 greater than or equal to $1.
+align_size() {
+ echo "$((($1) + ($2) - 1 - (($1) + ($2) - 1) % ($2)))"
+}
+alignment=$("${GNU_PREFIX}objdump" --private-headers "$BOOTSTUB" | sed 's/^SectionAlignment\s\+\([0-9]\)/0x/;t;d')
+test -z "$alignment" &&
+ die "failed to discover the alignment of the efi stub"
+test -n "$VERBOSE" &&
+ echo "determined efi vma alignment as $alignment"
+
+# Discover the last section in terms of vma + size. We'll append sections
+# beyond.
+lastoffset=0
+# shellcheck disable=SC2034 # unused variables serve documentation
+lastoffset="$("${GNU_PREFIX}objdump" --section-headers "$BOOTSTUB" \
+ | while read -r idx name size vma lma fileoff algn behind; do
+ test -z "$behind" -a "${algn#"2**"}" != "$algn" || continue
+ offset=$((0x$vma + 0x$size))
+ test "$offset" -gt "$lastoffset" || continue
+ lastoffset="$offset"
+ echo "$lastoffset"
+ done | tail -n1)"
+lastoffset=$(align_size "$lastoffset" "$alignment")
+test -n "$VERBOSE" &&
+ echo "determined minimum efi vma offset as $lastoffset"
+
+# Compute the objdump invocation that constructs the UKI. Successively add
+# sections and compute non-overlapping VMAs.
+set -- \
+ "${GNU_PREFIX}objcopy" \
+ --enable-deterministic-archives
+for sectionfile in "$TDIR/"*; do
+ section="${sectionfile##*/}"
+ size=$(stat -Lc%s "$sectionfile")
+ size=$(align_size "$size" "$alignment")
+ set -- "$@" \
+ --add-section ".$section=$sectionfile" \
+ --change-section-vma ".$section=$lastoffset"
+ lastoffset=$((lastoffset + size))
+done
+set -- "$@" "$BOOTSTUB" "$OUTPUT"
+if test -n "$VERBOSE"; then
+ ls -lA "$TDIR"
+ printf "%s\n" "$*"
+fi
+"$@" || die "failed to construct UKI"