diff --git a/.github/workflows/genesis-openembedded.yml b/.github/workflows/genesis-openembedded.yml index 5e9db94..f2ea8c0 100644 --- a/.github/workflows/genesis-openembedded.yml +++ b/.github/workflows/genesis-openembedded.yml @@ -24,25 +24,13 @@ jobs: apt-utils bats createrepo-c dpkg-dev gnupg \ libfile-slurper-perl \ libparallel-forkmanager-perl libperl-critic-perl rpm rpm2cpio cpio \ - grub-common curl ca-certificates + grub-common ca-certificates - name: Install the arm64-efi grub modules - # t/aarch64_target.t runs the spec's own grub2-mkimage command, and it skips when the - # arm64-efi modules are absent -- which is a skip exactly where the suite runs, so it - # measures nothing. grub-efi-arm64-bin is Architecture: arm64 and cannot be installed on - # the amd64 runner, but the modules inside it are target code, not host code, so - # extracting the deb puts them in place without enabling a foreign architecture. - run: | - set -euo pipefail - base=http://ports.ubuntu.com/ubuntu-ports/pool/main/g/grub2-unsigned - deb=$(curl -sS "$base/" | grep -oE 'grub-efi-arm64-bin_[^"]*_arm64\.deb' | sort -u | tail -1) - [ -n "$deb" ] || { echo "no grub-efi-arm64-bin deb found under $base" >&2; exit 1; } - curl -sSO "$base/$deb" - sudo dpkg -x "$deb" / - # The modules are what the test needs; say so on the branch that earned it. - n=$(ls /usr/lib/grub/arm64-efi 2>/dev/null | wc -l) - [ "$n" -gt 0 ] || { echo "the deb extracted no arm64-efi modules" >&2; exit 1; } - echo "installed $n arm64-efi grub modules from $deb" + # t/aarch64_target.t runs the spec's own grub2-mkimage command, and it skips without + # the arm64-efi modules -- a skip exactly where the suite runs. The script reads them + # from the runner's own suite through apt's signed metadata. + run: sudo ./install-grub-arm64-modules.sh - name: Run static checks run: | @@ -104,6 +92,7 @@ jobs: prove -v -Ilib t/openeuler_native_trust_dbpath.t prove -v -Ilib t/openeuler_native_cpio_scan.t bats t/verify-xcatsn-install.bats + bats t/install-grub-arm64-modules.bats # Ubuntu's AppArmor policy blocks these unprivileged test namespaces. - name: Run native packaging tests diff --git a/install-grub-arm64-modules.sh b/install-grub-arm64-modules.sh new file mode 100755 index 0000000..5ce1dd9 --- /dev/null +++ b/install-grub-arm64-modules.sh @@ -0,0 +1,154 @@ +#!/bin/bash +# Install the arm64-efi GRUB modules, so a host grub-mkimage can build the AArch64 +# network loader of grub2-xcat.spec. Ubuntu ships them only in grub-efi-arm64-bin, +# which is Architecture: arm64 and does not install on an amd64 host. The deb is read +# through apt's signed metadata, unpacked in a temporary directory, and only the +# module files are copied out. +# +# grub-mkimage embeds kernel.img and resolves moddep.lst from the module directory, +# so the modules must come from the GRUB version of the host tool. + +usage() { + cat <<'EOF' +Usage: install-grub-arm64-modules.sh [-d DESTDIR] [-s SUITE] [-m MIRROR] + + -d DESTDIR where the modules go (default /usr/lib/grub/arm64-efi) + -s SUITE suite to read (default: VERSION_CODENAME of /etc/os-release) + -m MIRROR apt mirror (default https://ports.ubuntu.com/ubuntu-ports) + +apt verifies the suite InRelease signature with the host keyring, and the deb hash +against that signed index. The deb is unpacked under ${TMPDIR:-/tmp} and removed on +exit. Nothing else from it is installed. +EOF +} + +PACKAGE=grub-efi-arm64-bin +MODULE_PATH=usr/lib/grub/arm64-efi +MIRROR_DEFAULT=https://ports.ubuntu.com/ubuntu-ports + +# Prints the GRUB upstream version of the host image builder, e.g. 2.12. +# EL names the tool grub2-mkimage and Debian names it grub-mkimage. +host_grub_version() { + local tool out + for tool in grub2-mkimage grub-mkimage; do + command -v "$tool" >/dev/null 2>&1 || continue + out=$("$tool" --version 2>/dev/null) || continue + # "grub-mkimage (GRUB) 2.12-1ubuntu7.3" + out=${out##* } + echo "${out%%-*}" + return 0 + done + echo "no grub2-mkimage and no grub-mkimage on PATH" >&2 + return 1 +} + +# The upstream version of a deb version: no epoch, no distribution revision. +upstream_version() { + local v=${1#*:} + echo "${v%%-*}" +} + +# apt-get against a private state tree, so the host dpkg architecture and the host +# sources stay as they are. An unsigned or stale index fails here. +apt_get() { + local root=$1 + shift + apt-get \ + -o Dir::State="$root/state" \ + -o Dir::State::status="$root/status" \ + -o Dir::Cache="$root/cache" \ + -o Dir::Etc::SourceList="$root/sources.list" \ + -o Dir::Etc::SourceParts="$root/none" \ + -o Dir::Etc::Preferences="$root/none/preferences" \ + -o Dir::Etc::PreferencesParts="$root/none" \ + -o APT::Architecture=arm64 \ + -o APT::Architectures=arm64 \ + -o APT::Sandbox::User=root \ + -o Acquire::AllowInsecureRepositories=false \ + -o APT::Get::AllowUnauthenticated=false \ + "$@" +} + +main() { + local dest=/usr/lib/grub/arm64-efi suite='' mirror=$MIRROR_DEFAULT opt + OPTIND=1 + while getopts 'd:s:m:h' opt; do + case $opt in + d) dest=$OPTARG ;; + s) suite=$OPTARG ;; + m) mirror=$OPTARG ;; + h) usage; return 0 ;; + *) usage >&2; return 2 ;; + esac + done + shift $((OPTIND - 1)) + if (( $# != 0 )); then + usage >&2 + return 2 + fi + if [[ -z $suite ]]; then + suite=$(. /etc/os-release 2>/dev/null && echo "$VERSION_CODENAME") + fi + if [[ -z $suite ]]; then + echo "no suite: /etc/os-release names no VERSION_CODENAME, give -s" >&2 + return 2 + fi + + local want + want=$(host_grub_version) || return 1 + + local root + root=$(mktemp -d "${TMPDIR:-/tmp}/grub-arm64.XXXXXX") || return 1 + # shellcheck disable=SC2064 + trap "rm -rf '$root'" EXIT + mkdir -p "$root/state/lists/partial" "$root/cache/archives/partial" "$root/none" "$root/deb" || return 1 + : > "$root/status" + printf 'deb [arch=arm64] %s %s main\n' "$mirror" "$suite" > "$root/sources.list" || return 1 + + if ! apt_get "$root" update; then + echo "FAIL: apt-get update of $mirror $suite failed" >&2 + return 1 + fi + if ! (cd "$root/deb" && apt_get "$root" download "$PACKAGE"); then + echo "FAIL: apt-get download $PACKAGE from $suite failed" >&2 + return 1 + fi + + local deb + deb=$(find "$root/deb" -maxdepth 1 -name "${PACKAGE}_*_arm64.deb" | sort | head -1) + if [[ -z $deb ]]; then + echo "FAIL: apt downloaded no $PACKAGE deb" >&2 + return 1 + fi + + local got + got=$(dpkg-deb -f "$deb" Version) || return 1 + got=$(upstream_version "$got") + if [[ $got != "$want" ]]; then + echo "FAIL: $PACKAGE is GRUB $got and the host image builder is GRUB $want" >&2 + return 1 + fi + + dpkg-deb -x "$deb" "$root/x" || return 1 + if [[ ! -d $root/x/$MODULE_PATH ]]; then + echo "FAIL: $PACKAGE carries no $MODULE_PATH" >&2 + return 1 + fi + + # The modules, moddep.lst and kernel.img, and none of the documentation or the + # prebuilt images under monolithic/ that the deb also carries. + install -d "$dest" || return 1 + find "$root/x/$MODULE_PATH" -maxdepth 1 -type f -exec install -m 0644 -t "$dest" {} + || return 1 + + local count + count=$(find "$dest" -maxdepth 1 -name '*.mod' | wc -l) + if (( count == 0 )) || [[ ! -f $dest/kernel.img || ! -f $dest/moddep.lst ]]; then + echo "FAIL: $dest has no modules, no kernel.img or no moddep.lst" >&2 + return 1 + fi + echo "PASS: $count arm64-efi modules of GRUB $got in $dest, from ${deb##*/}" +} + +if [[ ${BASH_SOURCE[0]} == "$0" ]]; then + main "$@" +fi diff --git a/t/install-grub-arm64-modules.bats b/t/install-grub-arm64-modules.bats new file mode 100644 index 0000000..2c5aa06 --- /dev/null +++ b/t/install-grub-arm64-modules.bats @@ -0,0 +1,162 @@ +#!/usr/bin/env bats +# Unit tests for install-grub-arm64-modules.sh. apt-get, dpkg-deb and the grub +# image builder are shell functions here, so no archive is read and the modules +# land only under BATS_TEST_TMPDIR. + +setup() { + source "$BATS_TEST_DIRNAME/../install-grub-arm64-modules.sh" + export TMPDIR=$BATS_TEST_TMPDIR + CALLS=$BATS_TEST_TMPDIR/calls + DEST=$BATS_TEST_TMPDIR/dest + DEB_VERSION=2.12-1ubuntu7 + TOOL_VERSION=2.12-1ubuntu7.3 + DEB_HAS_MODULES=1 +} + +# Both names, because the loop takes whichever the host has and a developer host +# may carry the real EL one. +grub2-mkimage() { + echo "grub2-mkimage (GRUB) $TOOL_VERSION" +} + +grub-mkimage() { + echo "grub-mkimage (GRUB) $TOOL_VERSION" +} + +apt-get() { + echo "apt-get $*" >>"$CALLS" + local list + list=$(printf '%s\n' "$@" | sed -nE 's/^Dir::Etc::SourceList=//p') + [ -z "$list" ] || cp "$list" "$BATS_TEST_TMPDIR/sources.list.seen" + case " $* " in + *" download "*) + # apt-get download writes the deb into the working directory. + : >"grub-efi-arm64-bin_${DEB_VERSION}_arm64.deb" + ;; + esac +} + +# A deb tree with the module directory, the documentation and the prebuilt images +# the real grub-efi-arm64-bin carries. +dpkg-deb() { + echo "dpkg-deb $*" >>"$CALLS" + case $1 in + -f) echo "$DEB_VERSION" ;; + -x) + local out=$3 mod=$3/usr/lib/grub/arm64-efi + if [[ $out != "$BATS_TEST_TMPDIR"/* ]]; then + echo "dpkg-deb -x outside the temporary tree: $out" >&2 + return 1 + fi + mkdir -p "$out/usr/share/doc/grub-efi-arm64-bin" + : >"$out/usr/share/doc/grub-efi-arm64-bin/copyright" + (( DEB_HAS_MODULES )) || return 0 + mkdir -p "$mod/monolithic" + local m + for m in normal linux tftp efinet; do : >"$mod/$m.mod"; done + : >"$mod/kernel.img" + : >"$mod/moddep.lst" + : >"$mod/monolithic/grubnetaa64.efi" + ;; + esac +} + +leftover_dirs() { + find "$BATS_TEST_TMPDIR" -maxdepth 1 -name 'grub-arm64.*' | wc -l +} + +@test "an operand is a usage error" { + run main noble + [ "$status" -eq 2 ] + [[ $output == Usage:* ]] +} + +@test "an unknown option is a usage error" { + run main -z + [ "$status" -eq 2 ] + [[ $output == *Usage:* ]] +} + +@test "the upstream version of a deb version drops the epoch and the revision" { + [ "$(upstream_version 1:2.12-1ubuntu7.3)" = 2.12 ] + [ "$(upstream_version 2.06-104)" = 2.06 ] +} + +@test "the host GRUB version comes from the image builder on PATH" { + [ "$(host_grub_version)" = 2.12 ] +} + +@test "no image builder on PATH stops the run before any download" { + host_grub_version() { echo "no grub2-mkimage and no grub-mkimage on PATH" >&2; return 1; } + run main -d "$DEST" -s noble + [ "$status" -eq 1 ] + [ ! -e "$CALLS" ] +} + +@test "apt reads the given suite and mirror, with authentication left on" { + run main -d "$DEST" -s jammy -m https://mirror.example/ubuntu-ports + [ "$status" -eq 0 ] + grep -q -- '-o APT::Get::AllowUnauthenticated=false' "$CALLS" + grep -q -- '-o Acquire::AllowInsecureRepositories=false' "$CALLS" + grep -q -- '-o APT::Architectures=arm64' "$CALLS" + grep -q 'apt-get .* update' "$CALLS" + grep -q 'download grub-efi-arm64-bin' "$CALLS" + # The suite and the mirror reach apt through its own sources.list, and the host + # sources and dpkg architectures are never read or changed. + local list + list=$(sed -nE 's/.*-o Dir::Etc::SourceList=([^ ]+).*/\1/p' "$CALLS" | head -1) + [ "$(cat "$BATS_TEST_TMPDIR/sources.list.seen")" = "deb [arch=arm64] https://mirror.example/ubuntu-ports jammy main" ] + [[ $list == "$BATS_TEST_TMPDIR"/grub-arm64.*/sources.list ]] +} + +@test "only the module files are installed, and the temporary tree is removed" { + run main -d "$DEST" -s noble + [ "$status" -eq 0 ] + [[ $output == "PASS: 4 arm64-efi modules of GRUB 2.12 in $DEST, from grub-efi-arm64-bin_2.12-1ubuntu7_arm64.deb" ]] + [ -f "$DEST/normal.mod" ] + [ -f "$DEST/kernel.img" ] + [ -f "$DEST/moddep.lst" ] + # Not the documentation, and not the prebuilt images of monolithic/. + [ ! -e "$DEST/usr" ] + [ ! -e "$DEST/monolithic" ] + [ ! -e "$DEST/copyright" ] + [ "$(find "$DEST" -mindepth 1 | wc -l)" -eq 6 ] + [ "$(leftover_dirs)" -eq 0 ] +} + +@test "a deb of another GRUB version than the host tool installs nothing" { + DEB_VERSION=2.06-1ubuntu1 + run main -d "$DEST" -s noble + [ "$status" -eq 1 ] + [[ $output == *"grub-efi-arm64-bin is GRUB 2.06 and the host image builder is GRUB 2.12"* ]] + [ ! -e "$DEST" ] + [ "$(leftover_dirs)" -eq 0 ] +} + +@test "a deb without the module directory installs nothing" { + DEB_HAS_MODULES=0 + run main -d "$DEST" -s noble + [ "$status" -eq 1 ] + [[ $output == *"carries no usr/lib/grub/arm64-efi"* ]] + [ ! -e "$DEST" ] +} + +@test "a failed apt-get update stops the run before the download" { + apt-get() { + echo "apt-get $*" >>"$CALLS" + case " $* " in *" update "*) return 100 ;; esac + } + run main -d "$DEST" -s noble + [ "$status" -eq 1 ] + [[ $output == *"apt-get update of https://ports.ubuntu.com/ubuntu-ports noble failed"* ]] + ! grep -q 'download' "$CALLS" + [ ! -e "$DEST" ] +} + +@test "a download that yields no deb is not reported as a success" { + apt-get() { echo "apt-get $*" >>"$CALLS"; } + run main -d "$DEST" -s noble + [ "$status" -eq 1 ] + [[ $output == *"apt downloaded no grub-efi-arm64-bin deb"* ]] + [ ! -e "$DEST" ] +}