From 506da9cb9f408d0ab2a8957843be9a9c7eb5e317 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Wed, 7 Oct 2026 17:48:38 -0300 Subject: [PATCH 1/4] fix(xcat-dep): an aarch64 build on an x86_64 host deploys to the x86_64 cell target_profile() took the rpm architecture of an EPEL target from the build host, so alma+epel-10-aarch64 built on an x86_64 host reported arch=x86_64. target_cell() then resolved to rh10/x86_64, and the deploy would have replaced the x86_64 cell of the published repository with aarch64 rpms. The same answer reported forcearch=0, which left the emulated build with the native step timeout. The architecture now comes from the target name, which is what names the rpms, and a target whose architecture differs from the host is a cross-build. mock already applies --forcearch to such a target, so nothing else has to change for the build itself to work. epel_target_profile() in MockBuildUtils carries the decision, and t/aarch64_cross_build.t calls it. The test pins the answer for every target that builds today, so the change cannot move one of them, and its two aarch64 assertions fail against the previous behaviour. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- BUILD.md | 11 ++++++++ MockBuildUtils.pm | 17 +++++++++++- mockbuild-all.pl | 15 ++++++----- t/aarch64_cross_build.t | 57 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 t/aarch64_cross_build.t 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 4280d23..a5b2698 100644 --- a/MockBuildUtils.pm +++ b/MockBuildUtils.pm @@ -24,7 +24,7 @@ our @EXPORT_OK = qw( rpm_version rpm_release rpm_sigmd5 rpm_is_signed restamp_release_line 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 + build_mock_uniqueext rpm_in_cell resolve_mock_cfg epel_target_profile native_owner_command NATIVE_BUILD_GID openeuler_build_target openeuler_repo_subdir recover_common_repository @@ -895,6 +895,21 @@ 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) }; +} + # 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/mockbuild-all.pl b/mockbuild-all.pl index b2a0420..25b0e9a 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 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 diff --git a/t/aarch64_cross_build.t b/t/aarch64_cross_build.t new file mode 100644 index 0000000..5900fe8 --- /dev/null +++ b/t/aarch64_cross_build.t @@ -0,0 +1,57 @@ +#!/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); + +# 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"); +} + +# 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; From 7b450d7c08d540caf6b45d28c79f2e69b5ca8427 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:12:58 -0300 Subject: [PATCH 2/4] test(xcat-dep): the aarch64 regression skips its image checks in CI t/aarch64_target.t was added with the aarch64 build target and no workflow step ran it. Adding the step alone is not enough: the four checks that run the spec's own grub2-mkimage command skip unless the arm64-efi grub modules are installed and the tool is on PATH, and both were missing on the runner. Debian and Ubuntu name the tool grub-mkimage, so looking only for the EL name skips exactly where the suite runs. The test now resolves either name and substitutes it into the spec's command. grub-efi-arm64-bin is Architecture: arm64 and cannot be installed on the amd64 runner, but the modules in it are target code, so the workflow extracts the deb from ports.ubuntu.com instead of enabling a foreign architecture, and fails when the extraction yields no modules. Measured in an ubuntu:24.04 container with the workflow's own steps: the upstream test reports 4 skips, `# skip grub2-mkimage or the arm64-efi grub modules are not installed`, and this one reports 0 with all four image checks passing. The workflow also runs t/aarch64_cross_build.t. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- .github/workflows/genesis-openembedded.yml | 23 +++++++++++++++++++++- t/aarch64_target.t | 10 ++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/.github/workflows/genesis-openembedded.yml b/.github/workflows/genesis-openembedded.yml index d419823..5e9db94 100644 --- a/.github/workflows/genesis-openembedded.yml +++ b/.github/workflows/genesis-openembedded.yml @@ -23,7 +23,26 @@ 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 curl 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" - name: Run static checks run: | @@ -75,6 +94,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 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 = ''; From 380fccd625e7cf2b734483ea9e2d543467eca298 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Thu, 8 Oct 2026 12:21:58 -0300 Subject: [PATCH 3/4] fix(xcat-dep): the CI arm64 grub modules come from an unverified archive The workflow step that puts the arm64-efi GRUB modules on the runner read a directory listing over plain HTTP, took the last name in lexical order, and unpacked the whole deb into / as root. Nothing authenticated the artifact, and the name chosen was neither tied to the runner's suite nor to the installed grub-mkimage. install-grub-arm64-modules.sh now reads grub-efi-arm64-bin through apt in a private state tree: apt verifies the InRelease signature of the suite that /etc/os-release names, and the deb hash against that signed index. The host dpkg architecture and the host sources are not changed. The script stops when the deb and the host grub-mkimage report different GRUB upstream versions, unpacks under TMPDIR, and copies only the files of usr/lib/grub/arm64-efi. The documentation and the prebuilt images under monolithic/ stay out. t/install-grub-arm64-modules.bats drives the script with apt-get, dpkg-deb and the image builder as shell functions. Four removals were each caught by the assertion that names them: the version gate, the file-only copy, the apt authentication options, and the missing-module-directory check. Measured in an ubuntu:24.04 container with the workflow's own steps: t/aarch64_target.t reports 4 skips before the step and 0 after it, with all 38 checks passing, and t/aarch64_cross_build.t passes 27 with 0 skips. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- .github/workflows/genesis-openembedded.yml | 23 +-- install-grub-arm64-modules.sh | 154 ++++++++++++++++++++ t/install-grub-arm64-modules.bats | 162 +++++++++++++++++++++ 3 files changed, 322 insertions(+), 17 deletions(-) create mode 100755 install-grub-arm64-modules.sh create mode 100644 t/install-grub-arm64-modules.bats 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" ] +} From 20110fc17d7136cab62d06e905175c874c4ab9e8 Mon Sep 17 00:00:00 2001 From: Daniel Hilst <392820+dhilst@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:14:37 -0300 Subject: [PATCH 4/4] test(xcat-dep): nothing asserts which repository cell lock a target takes The reported failure is a lock, not an architecture: xcat-dep-build-el10-aarch64 took /repos/yum/xcat-dep/rh10/.x86_64.lock on xcat-master and held it, and xcat-dep-build-el10-x86_64 of the same run waited out 100 retries and exited before it built anything. Builds 92 and 93 failed that way, and build 94 passed only because it won the race. No test covers the path that is locked. target_cell() built the cell inline from target_profile(), and cell_lock_path() lived in mockbuild-all.pl, so neither could be called from a test. The cell and the lock are now epel_cell_path() and cell_lock_path() in MockBuildUtils, and target_cell() calls them. The behaviour does not change. t/aarch64_cross_build.t resolves both EL10 targets on an x86_64 host and asserts that their cells and their locks differ. With the architecture taken from the host again, three of its assertions fail, among them "the two EL10 targets of one host take different cell locks". Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com> --- MockBuildUtils.pm | 19 +++++++++++++++++++ mockbuild-all.pl | 12 ++---------- t/aarch64_cross_build.t | 18 +++++++++++++++++- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/MockBuildUtils.pm b/MockBuildUtils.pm index a5b2698..257c189 100644 --- a/MockBuildUtils.pm +++ b/MockBuildUtils.pm @@ -25,6 +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 epel_target_profile + epel_cell_path cell_lock_path native_owner_command NATIVE_BUILD_GID openeuler_build_target openeuler_repo_subdir recover_common_repository @@ -910,6 +911,24 @@ sub epel_target_profile { 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/mockbuild-all.pl b/mockbuild-all.pl index 25b0e9a..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 epel_target_profile + 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 @@ -2493,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 index 5900fe8..2b38f56 100644 --- a/t/aarch64_cross_build.t +++ b/t/aarch64_cross_build.t @@ -15,7 +15,7 @@ use FindBin qw($RealBin); use Test::More; use lib $RealBin, "$RealBin/.."; -use MockBuildUtils qw(epel_target_profile); +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. { @@ -48,6 +48,22 @@ for my $case (['alma+epel-10-x86_64', 'x86_64', '10'], 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)) {