diff --git a/.github/workflows/genesis-openembedded.yml b/.github/workflows/genesis-openembedded.yml index d78b75c..32baaf7 100644 --- a/.github/workflows/genesis-openembedded.yml +++ b/.github/workflows/genesis-openembedded.yml @@ -23,7 +23,14 @@ jobs: sudo apt-get install -y --no-install-recommends \ apt-utils bats createrepo-c dpkg-dev gnupg \ libfile-slurper-perl \ - libparallel-forkmanager-perl libperl-critic-perl rpm rpm2cpio cpio + libparallel-forkmanager-perl libperl-critic-perl rpm rpm2cpio cpio \ + 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 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: | @@ -75,6 +82,8 @@ jobs: prove -v -It/lib t/genesis_openembedded_release.t sudo -E prove -v -It/lib t/genesis_openembedded_consumer.t prove -v t/riscv64_perl_cell.t + prove -v t/aarch64_target.t + prove -v t/aarch64_cross_build.t sudo -E prove -v -It/lib t/common-repo-gate.t prove -v t/openeuler.t prove -v t/openeuler_mock_template_include.t @@ -85,6 +94,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/BUILD.md b/BUILD.md index 553ad3b..93b1113 100644 --- a/BUILD.md +++ b/BUILD.md @@ -395,6 +395,17 @@ tracked prebuilt EFI payload and `syslinux-xcat` installs the prebuilt bootloade release tarball, so the noarch rpms carry the same payload as the ppc64le build. aarch64 is not part of the `--finalize-xcat-dep` cross-arch genesis copy. +`aarch64` also builds on an x86_64 host. mock applies `--forcearch=aarch64` to the +`alma+epel-10-aarch64` config when the host is not aarch64, so the chroot runs aarch64 binaries +through `qemu-aarch64-static` and produces aarch64 rpms. The host needs that emulator and its +`binfmt_misc` handler; without them mock warns at the start of the build and the chroot cannot run +its own rpm scriptlets. EL packages no `qemu-user-static`, so take the static binary from a Fedora +container, as the riscv64 host prerequisites do. + +A cross-build is slower than a native one and is otherwise the same build: the rpms and the +repository cell carry the target architecture, not the host one, and the step timeout is the +emulated bound. + # riscv64 (EL10): cross-building on an x86_64 host There is no riscv64 build host in the xCAT build farm and no EPEL for riscv64, so the diff --git a/MockBuildUtils.pm b/MockBuildUtils.pm index 7ac9de2..603aaa1 100644 --- a/MockBuildUtils.pm +++ b/MockBuildUtils.pm @@ -25,7 +25,7 @@ our @EXPORT_OK = qw( cross_copy_genesis finalize_xcat_dep bump_dep_release_suffix createrepo_c_cmd sign_and_index_repo build_mock_uniqueext rpm_in_cell resolve_mock_cfg mock_chroot_uid mock_result_dirs - make_traversable_path + make_traversable_path epel_target_profile epel_cell_path cell_lock_path native_owner_command NATIVE_BUILD_GID openeuler_build_target openeuler_repo_subdir recover_common_repository @@ -896,6 +896,39 @@ sub recover_common_repository { return 1; } +# epel_target_profile($target, $host_arch): the EL release, the rpm architecture, and whether +# mock cross-builds, for one EPEL target name on one build host. Returns undef for a name that is +# not an EPEL target, so the caller keeps its own error. +# +# The target name carries the architecture of the rpms it produces, and that architecture decides +# the repository cell the build deploys to. A host of another architecture cross-builds the target +# through mock's own --forcearch and qemu-user; the rpms are still the target's architecture. +sub epel_target_profile { + my ($target, $host_arch) = @_; + return undef unless defined $target && defined $host_arch; + my ($rel, $arch) = $target =~ /epel-(\d+)-(.+)\z/; + return undef unless defined $rel && defined $arch; + return { rel => $rel, arch => $arch, forcearch => ($arch eq $host_arch ? 0 : 1) }; +} + +# epel_cell_path($repo_dep, $profile): the repository cell an EPEL target deploys, +# /rh/, from the profile epel_target_profile returns. +# +# The deploy, the carry-over and the cell lock all take the path from here, so the lock covers the +# directory the deploy writes. Two targets share a lock exactly when they share a cell. +sub epel_cell_path { + my ($repo_dep, $profile) = @_; + return undef unless defined $repo_dep && $profile; + return "$repo_dep/rh$profile->{rel}/$profile->{arch}"; +} + +# cell_lock_path($cell): the lock of a repository cell / sits beside it, as +# /..lock, because the deploy replaces the cell directory itself. +sub cell_lock_path { + my ($cell) = @_; + return dirname($cell) . '/.' . basename($cell) . '.lock'; +} + # resolve_mock_cfg($os_id, $rel, $arch[, $cfg_dir]): the mock config for EL release $rel on this # host, +epel--. /etc/os-release says 'almalinux' where mock-core-configs names the # file 'alma', so the short form is tried too. $cfg_dir defaults to /etc/mock. 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/mockbuild-all.pl b/mockbuild-all.pl index b2a0420..b7fb707 100755 --- a/mockbuild-all.pl +++ b/mockbuild-all.pl @@ -19,7 +19,7 @@ use FindBin qw($RealBin); use lib $RealBin, "$RealBin/lib"; use XCAT::NFSLock (); use MockBuildUtils qw(sh_quote print_step version_matches required_pkgs rpm_in_cell resolve_mock_cfg - NATIVE_BUILD_GID + NATIVE_BUILD_GID epel_target_profile epel_cell_path cell_lock_path carry_over_rpms rpm_name rpm_arch rpm_source_rpm rpm_digests_ok install_deps_packages install_deps_command missing_perl_modules read_manifest derive_target_from_repo_path @@ -1179,13 +1179,16 @@ sub target_profile { epel => 0, }; } - my ($rel) = $target =~ /epel-(\d+)-/; - die "Could not parse EL release from target '$target'\n" unless defined $rel; + # The target name, not the host, decides the architecture of the rpms and so the repository + # cell they deploy to. A host of another architecture cross-builds the target through mock's + # own --forcearch, which is what gives the emulated build its own step timeout below. + my $epel = epel_target_profile($target, $host_arch); + die "Could not parse EL release from target '$target'\n" unless $epel; return { - rel => $rel, - arch => $host_arch, + rel => $epel->{rel}, + arch => $epel->{arch}, noarch_cfg => $target, - forcearch => 0, + forcearch => $epel->{forcearch}, epel => 1, dep_builders => [qw(elilo-xcat grub2-xcat ipmitool-xcat syslinux-xcat goconserver conserver-xcat xnba-undi ipxe-xcat)], # xCAT Requires all of these on every arch, and every one of them builds natively on @@ -2490,15 +2493,7 @@ sub target_cell { my ($target) = @_; my $native = openeuler_repo_subdir($target); return "$repo_dep/$native" if defined $native; - my $profile = target_profile($target); - return "$repo_dep/rh$profile->{rel}/$profile->{arch}"; -} - -# The lock of a repository cell / sits beside it, as /..lock: the deploy -# replaces the cell directory itself. -sub cell_lock_path { - my ($cell) = @_; - return dirname($cell) . '/.' . basename($cell) . '.lock'; + return epel_cell_path($repo_dep, target_profile($target)); } # Release locks on every exit path, but only from the process that acquired them. diff --git a/t/aarch64_cross_build.t b/t/aarch64_cross_build.t new file mode 100644 index 0000000..2b38f56 --- /dev/null +++ b/t/aarch64_cross_build.t @@ -0,0 +1,73 @@ +#!/usr/bin/env perl +# An EPEL target names the architecture of the rpms it produces, and that architecture decides +# the repository cell the build deploys to. A build host whose own architecture differs +# cross-builds the target through mock's --forcearch and qemu-user; the rpms are still the +# target's architecture. +# +# alma+epel-10-aarch64 built on an x86_64 host reported arch=x86_64, so target_cell resolved to +# rh10/x86_64 and the aarch64 build would have replaced the x86_64 cell of the published +# repository with aarch64 rpms. It also reported forcearch=0, which left the emulated build with +# the native step timeout. +use strict; +use warnings; + +use FindBin qw($RealBin); +use Test::More; + +use lib $RealBin, "$RealBin/.."; +use MockBuildUtils qw(epel_target_profile epel_cell_path cell_lock_path); + +# A cross-built target: the rpms and the cell are the target's architecture, not the host's. +{ + my $p = epel_target_profile('alma+epel-10-aarch64', 'x86_64'); + ok($p, 'alma+epel-10-aarch64 is an EPEL target') or done_testing, exit; + is($p->{rel}, '10', 'EL release 10'); + is($p->{arch}, 'aarch64', 'the rpm architecture is the target architecture, not the host one'); + is($p->{forcearch}, 1, 'an x86_64 host cross-builds it'); +} + +# The same target on its own architecture is a native build. +{ + my $p = epel_target_profile('alma+epel-10-aarch64', 'aarch64'); + is($p->{arch}, 'aarch64', 'a native aarch64 build still produces aarch64 rpms'); + is($p->{forcearch}, 0, 'and does not cross-build'); +} + +# Every target that builds today keeps the answer it has today: same arch, no cross-build. +for my $case (['alma+epel-10-x86_64', 'x86_64', '10'], + ['alma+epel-9-x86_64', 'x86_64', '9'], + ['alma+epel-8-x86_64', 'x86_64', '8'], + ['alma+epel-10-ppc64le', 'ppc64le', '10'], + ['alma+epel-9-ppc64le', 'ppc64le', '9'], + ['alma+epel-8-ppc64le', 'ppc64le', '8']) +{ + my ($target, $host, $rel) = @$case; + my $p = epel_target_profile($target, $host); + is($p->{rel}, $rel, "$target on $host: EL release $rel"); + is($p->{arch}, $host, "$target on $host: builds natively for $host"); + is($p->{forcearch}, 0, "$target on $host: no cross-build"); +} + +# The two EL10 targets deploy to different cells, so they take different cell locks. With the +# architecture taken from the host, both resolved to rh10/x86_64 and to one lock: the aarch64 build +# on xcat-master held .x86_64.lock, and the x86_64 build of the same run waited out its 100 retries +# and exited before it built anything (xcat-dep-build-el10-x86_64 builds 92 and 93). +{ + my $repo = '/repo/yum/xcat-dep'; + my %cell = map { $_ => epel_cell_path($repo, epel_target_profile("alma+epel-10-$_", 'x86_64')) } + qw(x86_64 aarch64); + is($cell{aarch64}, "$repo/rh10/aarch64", 'the aarch64 target deploys to the aarch64 cell'); + is($cell{x86_64}, "$repo/rh10/x86_64", 'the x86_64 target deploys to the x86_64 cell'); + isnt(cell_lock_path($cell{aarch64}), cell_lock_path($cell{x86_64}), + 'the two EL10 targets of one host take different cell locks'); + is(cell_lock_path($cell{aarch64}), "$repo/rh10/.aarch64.lock", + 'a cell lock sits beside its cell'); +} + +# A name that is not an EPEL target is refused, so the caller can keep its own error. The +# forcearch and openEuler targets are resolved before this sub is reached. +for my $not (qw(rocky-10-riscv64-xcat openeuler-24.03sp4-x86_64 noble)) { + is(epel_target_profile($not, 'x86_64'), undef, "'$not' is not an EPEL target"); +} + +done_testing; diff --git a/t/aarch64_target.t b/t/aarch64_target.t index b5d89ab..f88c92c 100644 --- a/t/aarch64_target.t +++ b/t/aarch64_target.t @@ -130,11 +130,17 @@ my $grub_spec = slurp($grub_spec_file); # build does not carry, or efi_uga, makes grub2-mkimage fail. SKIP: { my $moddir = '/usr/lib/grub/arm64-efi'; - skip 'grub2-mkimage or the arm64-efi grub modules are not installed', 4 - unless -d $moddir && system('command -v grub2-mkimage >/dev/null 2>&1') == 0; + # Debian and Ubuntu name the tool grub-mkimage and EL names it grub2-mkimage. The CI + # runner is Ubuntu, so looking only for the EL name skips the checks exactly where the + # suite runs, which measures nothing. + my ($mkimage) = grep { system("command -v $_ >/dev/null 2>&1") == 0 } + qw(grub2-mkimage grub-mkimage); + skip 'no grub2-mkimage or grub-mkimage, or the arm64-efi grub modules are not installed', 4 + unless -d $moddir && $mkimage; skip 'no grub2-mkimage command in the spec', 4 if $cmd eq ''; my $tmp = tempdir(CLEANUP => 1); my $run = $cmd; + $run =~ s/\Agrub2-mkimage\b/$mkimage/; $run =~ s/(?<=\s-o\s)\S+/$tmp\/grubaa64.efi/; is(system("$run >/dev/null 2>&1"), 0, "the spec's grub2-mkimage command succeeds"); my $image = ''; 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" ] +}