Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
377 changes: 310 additions & 67 deletions packaging/checks/smoke-arch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,83 +4,326 @@
# Usage: smoke-arch.sh <path/to/pkg.pkg.tar.zst> <package-name> node|gui [pkg-arch]
#
# The optional fourth argument is the architecture the package was built for
# (defaults to the container architecture). For a foreign-architecture package
# (the cross-target aarch64 leg reuses the amd64-only Arch image), pacman's
# architecture check is bypassed via IgnoreArch and the binaries are executed
# through the host's qemu binfmt handlers — the same emulation the deb/rpm
# legs use for their arm64 smoke tests.
# (defaults to the container architecture). Docker Hub publishes no arm64
# Arch image (library/archlinux and archlinux/archlinux are amd64-only), so a
# foreign-architecture package (the cross-target aarch64 leg reuses the
# amd64-only image) is smoke-tested in a chroot of the Arch Linux ARM rootfs:
# the package and its dependencies are installed by the chroot's pacman and
# its binaries execute through the host's qemu binfmt handlers — the same
# emulation the deb/rpm legs use for their arm64 smoke tests.
set -euo pipefail

# Shared binary list (NODE_BINARIES)
. "$(cd "$(dirname "${BASH_SOURCE[0]}")/../common" && pwd)/lib.sh"
usage() {
echo "Usage: $0 <path/to/pkg.pkg.tar.zst> <package-name> node|gui [pkg-arch]" >&2
echo " $0 --chroot <pkg-file-in-chroot> <package-name> node|gui (internal, called by the foreign-arch branch)" >&2
exit 64
}

CHROOT_MODE=0
case "${1:-}" in
--chroot)
CHROOT_MODE=1
shift
[ "$#" -eq 3 ] || usage
;;
*)
{ [ "$#" -eq 3 ] || [ "$#" -eq 4 ]; } || usage
;;
esac

# Fail fast on an invalid kind, before any (potentially foreign-arch) setup
# work; main() repeats the check as defense in depth.
case "${3:-}" in
node|gui) ;;
*) usage ;;
esac

CHROOT_DIR=/alarm
# Primary + fallback download locations for the Arch Linux ARM rootfs; the
# env var override is for local testing (must be an https:// URL, since the
# download loop only permits https, and it must serve the pinned build unless
# ALARM_ROOTFS_SHA256 is overridden alongside it).
alarm_rootfs_urls() {
if [ -n "${ALARM_ROOTFS_URL:-}" ]; then
printf '%s\n' "$ALARM_ROOTFS_URL"
else
printf '%s\n' \
https://fl.us.mirror.archlinuxarm.org/os/ArchLinuxARM-aarch64-latest.tar.gz \
https://mirror.archlinuxarm.org/os/ArchLinuxARM-aarch64-latest.tar.gz
fi
}

# Shared binary list (NODE_BINARIES). Inside the chroot the repo tree is not
# visible; the foreign-arch branch copies lib.sh in next to the script.
if [ "$CHROOT_MODE" = 1 ]; then
. /smoke-lib.sh
else
. "$(cd "$(dirname "${BASH_SOURCE[0]}")/../common" && pwd)/lib.sh"
fi

# Inside the ARM chroot pacman must not drop to its download sandbox user:
# the (emulated) setuid switch fails under the container's seccomp profile.
pacman_cmd() {
if [ -n "${SMOKE_CHROOT:-}" ]; then
pacman --disable-sandbox "$@"
else
pacman "$@"
fi
}

main() {
case "$KIND" in
node|gui) ;;
*)
echo "ERROR: kind must be 'node' or 'gui', got '$KIND'" >&2
exit 64
;;
esac

# Sync the databases so the package dependencies resolve from the repos.
pacman_cmd -Sy --noconfirm >/dev/null
pacman_cmd -U --noconfirm "$PKG_FILE" >/dev/null

echo "== pacman query =="
pacman_cmd -Qi "$PKG_NAME"

if [ "$KIND" = node ]; then
echo "== system user =="
id mintlayer

echo "== binaries =="
for bin in "${NODE_BINARIES[@]}"; do
test -x "/usr/bin/mintlayer-$bin"
"/usr/bin/mintlayer-$bin" --help >/dev/null 2>&1
echo " ok: mintlayer-$bin --help"
done

echo "== systemd units =="
shopt -s nullglob
units=(/usr/lib/systemd/system/mintlayer-*.service)
shopt -u nullglob
[ ${#units[@]} -gt 0 ] || { echo "ERROR: no mintlayer units installed" >&2; exit 1; }
for unit in "${units[@]}"; do
systemd-analyze verify "$unit"
echo " ok: $(basename "$unit")"
done
test -f /usr/lib/systemd/system-preset/90-mintlayer.preset
grep -q "enable mintlayer-node@mainnet.service" \
/usr/lib/systemd/system-preset/90-mintlayer.preset
test -f /usr/lib/sysusers.d/mintlayer.conf
test -f /usr/lib/udev/rules.d/51-mintlayer.rules
test -f /etc/mintlayer/mainnet/node.env
test -f /etc/mintlayer/testnet/node.env
echo " ok: preset/sysusers/udev/conffiles present"

echo "== man pages =="
# Capturing first avoids the pipefail SIGPIPE race: grep -q exits at the
# first match and can kill a still-writing pacman with SIGPIPE, which
# pipefail turns into a spurious script failure.
package_file_list="$(pacman_cmd -Ql "$PKG_NAME")"
grep -q "usr/share/man/man1/mintlayer-.*\.1\.gz$" <<<"$package_file_list"
echo " ok"
else
echo "== gui files =="
test -x /usr/bin/mintlayer-node-gui
"/usr/bin/mintlayer-node-gui" --help >/dev/null 2>&1
echo " ok: mintlayer-node-gui --help"

pacman_cmd -S --noconfirm --needed desktop-file-utils >/dev/null
desktop-file-validate /usr/share/applications/mintlayer-node-gui.desktop

echo "== icons =="
for size in 64x64 128x128 256x256 512x512; do
test -f "/usr/share/icons/hicolor/${size}/apps/mintlayer-node-gui.png"
echo " ok: hicolor ${size}"
done

echo "== man pages =="
package_file_list="$(pacman_cmd -Ql "$PKG_NAME")"
grep -q "usr/share/man/man1/mintlayer-node-gui\.1\.gz$" <<<"$package_file_list"
echo " ok"
fi

echo "smoke test passed for $PKG_NAME"
}

# Re-entry mode: the foreign-arch branch below invokes this script inside the
# ARM chroot with `--chroot <pkg-in-chroot> <package-name> node|gui`.
if [ "$CHROOT_MODE" = 1 ]; then
# Enforce the pacman >= 6.1 assumption the rootfs pin documents: fail fast
# with a clear message instead of an opaque pacman usage error. A
# functional probe beats grepping help text (help wording is not a stable
# upstream interface): on pacman >= 6.1 the flag parses and -Sh short-
# circuits to the sync help with exit 0, needing no database or network.
probe_out=""
if ! probe_out="$(pacman --disable-sandbox -Sh 2>&1)"; then
echo "ERROR: chroot pacman probe failed; if it rejected --disable-sandbox, pacman >= 6.1 is required. Probe output: $probe_out" >&2
exit 1
fi
Comment on lines +159 to +162

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
The probe treats any non-zero exit as 'pacman rejected --disable-sandbox' but the same failure signature occurs for unrelated causes: no network for the -Sh sync-database path in an offline chroot, a corrupted rootfs pacman.conf, or a missing downloader binary. In those cases the operator is sent chasing a pacman-version problem that does not exist. Since the rootfs is sha256-pinned this is a narrow window, but including the probe output in the message (or a distinct hint) would save a debugging cycle.

Suggestion:

Suggested change
if ! probe_out="$(pacman --disable-sandbox -Sh 2>&1)"; then
echo "ERROR: chroot pacman probe failed; if it rejected --disable-sandbox, pacman >= 6.1 is required. Probe output: $probe_out" >&2
exit 1
fi
if ! probe_out="$(pacman --disable-sandbox -Sh 2>&1)"; then
echo "ERROR: chroot pacman probe failed (pacman >= 6.1 required for --disable-sandbox; network/config problems produce the same signature). Probe output: $probe_out" >&2
exit 1
fi

SMOKE_CHROOT=1
PKG_FILE="$1"
PKG_NAME="$2"
KIND="$3"
main
exit 0
fi

PKG_FILE="$(readlink -f "$1")"
PKG_NAME="$2"
KIND="$3"
PKG_ARCH="${4:-$(uname -m)}"

if [ "$PKG_ARCH" != "$(uname -m)" ]; then
echo "foreign-architecture package ($PKG_ARCH on $(uname -m)): enabling IgnoreArch"
sed -i "/^\[options\]$/a IgnoreArch = $PKG_ARCH" /etc/pacman.conf
if [ "$PKG_ARCH" = "$(uname -m)" ]; then
main
exit 0
fi

if [ "$PKG_ARCH" != aarch64 ]; then
echo "ERROR: foreign-architecture smoke tests only support aarch64, got '$PKG_ARCH'" >&2
exit 64
fi
echo "foreign-architecture package ($PKG_ARCH on $(uname -m)): smoke-testing in an Arch Linux ARM chroot"

# Sync the databases so the package dependencies resolve from the repos.
pacman -Sy --noconfirm >/dev/null
pacman -U --noconfirm "$PKG_FILE" >/dev/null

echo "== pacman query =="
pacman -Qi "$PKG_NAME"

if [ "$KIND" = node ]; then
echo "== system user =="
id mintlayer

echo "== binaries =="
for bin in "${NODE_BINARIES[@]}"; do
test -x "/usr/bin/mintlayer-$bin"
"/usr/bin/mintlayer-$bin" --help >/dev/null 2>&1
echo " ok: mintlayer-$bin --help"
done

echo "== systemd units =="
shopt -s nullglob
units=(/usr/lib/systemd/system/mintlayer-*.service)
shopt -u nullglob
[ ${#units[@]} -gt 0 ] || { echo "ERROR: no mintlayer units installed" >&2; exit 1; }
for unit in "${units[@]}"; do
systemd-analyze verify "$unit"
echo " ok: $(basename "$unit")"
done
test -f /usr/lib/systemd/system-preset/90-mintlayer.preset
grep -q "enable mintlayer-node@mainnet.service" \
/usr/lib/systemd/system-preset/90-mintlayer.preset
test -f /usr/lib/sysusers.d/mintlayer.conf
test -f /usr/lib/udev/rules.d/51-mintlayer.rules
test -f /etc/mintlayer/mainnet/node.env
test -f /etc/mintlayer/testnet/node.env
echo " ok: preset/sysusers/udev/conffiles present"

echo "== man pages =="
pacman -Ql "$PKG_NAME" | grep -q "usr/share/man/man1/mintlayer-.*\.1\.gz$"
echo " ok"
# Assemble a native $PKG_ARCH userland. Requires qemu binfmt handlers for the
# architecture to be registered on the runner's kernel (release_linux.yml
# sets them up via docker/setup-qemu-action; the F-flag keeps them working
# inside the chroot).
# Best-effort pre-flight: with binfmt_misc mounted, an absent qemu-aarch64
# handler means the chroot's first emulated exec would fail later anyway.
if [ -f /proc/sys/fs/binfmt_misc/status ]; then
if [ "$(cat /proc/sys/fs/binfmt_misc/status 2>/dev/null || true)" != enabled ]; then
echo "ERROR: binfmt_misc is mounted but disabled; emulation would fail at the first exec (enable with: echo 1 > /proc/sys/fs/binfmt_misc/status)" >&2
exit 1
fi
binfmt_entries="$(ls /proc/sys/fs/binfmt_misc 2>/dev/null || true)"
if ! grep -qx qemu-aarch64 <<<"$binfmt_entries"; then
echo "ERROR: no qemu-aarch64 binfmt handler on this kernel (registered by docker/setup-qemu-action in release_linux.yml)" >&2
exit 1
fi
if [ "$(head -n 1 /proc/sys/fs/binfmt_misc/qemu-aarch64 2>/dev/null || true)" != enabled ] ||
! grep -q '^flags:.*F' /proc/sys/fs/binfmt_misc/qemu-aarch64; then
echo "ERROR: the qemu-aarch64 binfmt handler is disabled or lacks the F (fix-binary) flag; the chroot's first emulated exec would fail" >&2
exit 1
fi
else
echo "== gui files =="
test -x /usr/bin/mintlayer-node-gui
/usr/bin/mintlayer-node-gui --help >/dev/null 2>&1
echo " ok: mintlayer-node-gui --help"

pacman -S --noconfirm --needed desktop-file-utils >/dev/null
desktop-file-validate /usr/share/applications/mintlayer-node-gui.desktop

echo "== icons =="
for size in 64x64 128x128 256x256 512x512; do
test -f "/usr/share/icons/hicolor/${size}/apps/mintlayer-node-gui.png"
echo " ok: hicolor ${size}"
done

echo "== man pages =="
pacman -Ql "$PKG_NAME" | grep -q "usr/share/man/man1/mintlayer-node-gui\.1\.gz$"
echo " ok"
echo "WARNING: /proc/sys/fs/binfmt_misc not visible in this container; skipping the qemu-aarch64 pre-flight" >&2
fi

echo "smoke test passed for $PKG_NAME"
rootfs_tar=/tmp/alarm.tar.gz
# Reclaim the ~1 GB chroot and tarball on every exit path, and remove any
# stale chroot from a previous attempt so results are reproducible.
cleanup() {
rm -rf -- "$CHROOT_DIR" || true
[ -z "${rootfs_tar:-}" ] || rm -f -- "$rootfs_tar"
}
trap 'exit 130' INT
trap 'exit 143' TERM
trap cleanup EXIT
rm -rf "$CHROOT_DIR"
mkdir -p "$CHROOT_DIR"
# Pinned to a known-good rootfs build (cross-checked against the mirror's
# published .md5); 'latest' is a mutable rolling artifact fetched over
# transport security only, and its contents are executed as root inside the
# chroot. Bump the hash deliberately (same policy as packaging/images.env)
# whenever upstream rebuilds the rootfs, and re-check that the chroot's
# pacman still supports --disable-sandbox (pacman >= 6.1), which main()
# relies on. These pins live here rather than in packaging/images.env
# because this script is their only consumer; move them there if that
# changes. ALARM_ROOTFS_SHA256 overrides the pin for local testing together
# with ALARM_ROOTFS_URL.
expected_rootfs_sha256="${ALARM_ROOTFS_SHA256:-42a4eeaa038994ffd31fa173256ef2f0ef511358eeb41b9ea1f8626391b9b319}"
# The overrides are for local testing; make a CI-level bypass visible in the
# job log.
if [ -n "${ALARM_ROOTFS_SHA256:-}" ]; then
echo "WARNING: rootfs pin overridden via ALARM_ROOTFS_SHA256; the sha256 check compares against the overridden value, not the known-good pin" >&2
fi
if [ -n "${ALARM_ROOTFS_URL:-}" ]; then
echo "WARNING: rootfs source overridden via ALARM_ROOTFS_URL; the sha256 is still checked against the known-good pin" >&2
fi
rc=1
while IFS= read -r url; do
[ -n "$url" ] || continue
echo "downloading ARM rootfs from $url"
# A mirror that fails or serves a build other than the pinned one (the
# mirrors of the rolling 'latest' artifact sync at different times) falls
# through to the next URL; only a verified download sets rc=0.
# --retry-max-time bounds the cumulative retry window per URL (--max-time
# alone applies per attempt), so a throttling mirror cannot eat hours of
# the CI job budget before the loop falls through to the next URL.
if curl --proto '=https' --tlsv1.2 --retry 3 --retry-delay 5 \
--connect-timeout 30 --max-time 1800 --retry-max-time 1800 \
-sSfL -o "$rootfs_tar" "$url"; then
computed_rootfs_sha256="$(sha256sum "$rootfs_tar" | awk '{print $1}')"
if [ "$computed_rootfs_sha256" = "$expected_rootfs_sha256" ]; then
rc=0
break
fi
# curl exiting 0 means the transfer completed, so this is a complete
# file that simply is not the pinned build (a truncated transfer makes
# curl fail and lands in the branch below).
echo "WARNING: $url served rootfs sha256 $computed_rootfs_sha256, expected $expected_rootfs_sha256 (upstream rebuild?); trying the next mirror" >&2
else
echo "WARNING: $url download failed; trying the next mirror" >&2
fi
rm -f -- "$rootfs_tar"
done < <(alarm_rootfs_urls)
[ "$rc" -eq 0 ] || { echo "ERROR: could not download an Arch Linux ARM rootfs matching the pinned sha256; if upstream rebuilt ArchLinuxARM-aarch64-latest.tar.gz, update the pin deliberately" >&2; exit 1; }
# The rootfs ships device nodes we cannot recreate inside an unprivileged
# container; skip them. --exclude never causes tar errors, so any non-zero
# exit is a real extraction failure (truncated archive, full disk; GNU tar
# reports per-member extraction failures with exit 2, and its exit 1 is only
# used for --diff/--create divergence), which must fail the smoke test.
tar_rc=0
tar -xzf "$rootfs_tar" -C "$CHROOT_DIR" --exclude='./dev/*' || tar_rc=$?
if [ "$tar_rc" -ne 0 ]; then
echo "ERROR: failed to extract the Arch Linux ARM rootfs (tar exit code $tar_rc)" >&2
exit 1
fi

# Recreate the essential device nodes the tar exclusion skipped: main()
# redirects to /dev/null, and pacman hooks and emulated binaries need the
# others. CAP_MKNOD is in Docker's default capability set, so plain mknod
# works even though the container is unprivileged.
mkdir -p "$CHROOT_DIR/dev"
mknod_in_chroot() {
mknod "$@" || {
echo "ERROR: could not create chroot device nodes (CAP_MKNOD required; rootless container runtime?)" >&2
exit 1
}
}
mknod_in_chroot -m 666 "$CHROOT_DIR/dev/null" c 1 3
mknod_in_chroot -m 666 "$CHROOT_DIR/dev/zero" c 1 5
mknod_in_chroot -m 444 "$CHROOT_DIR/dev/random" c 1 8
mknod_in_chroot -m 444 "$CHROOT_DIR/dev/urandom" c 1 9

# chroot plumbing the rootfs does not ship: DNS resolution and a mount table
# (pacman reads /etc/mtab for its free-space check; /proc is not mounted here,
# so a static copy of the container's table is used).
rm -f "$CHROOT_DIR/etc/resolv.conf"
cp -L /etc/resolv.conf "$CHROOT_DIR/etc/resolv.conf"
rm -f "$CHROOT_DIR/etc/mtab"
cp /proc/self/mounts "$CHROOT_DIR/etc/mtab"
Comment on lines +304 to +305

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · medium
This static copy of /proc/self/mounts is the host container's mount table (overlay root, /work bind mount, docker virtual filesystems), none of which exist inside the chroot. Modern pacman computes its free-space check with statvfs() on the target directory, so a wrong mtab is mostly harmless today, but if pacman (or any hook/script run during -U, e.g. ldconfig under emulation) ever consults it, the bogus entries could mislead diagnostics or trigger a spurious disk-space failure that is very hard to debug in CI. Consider documenting this explicitly here (or minimizing the table), since the comment currently implies the copy is a faithful stand-in.

Suggestion:

Suggested change
rm -f "$CHROOT_DIR/etc/mtab"
cp /proc/self/mounts "$CHROOT_DIR/etc/mtab"
rm -f "$CHROOT_DIR/etc/mtab"
# Host mount table; modern pacman uses statvfs() on the target dir, so this
# is a best-effort stand-in only — entries reference host paths absent from
# the chroot.
cp /proc/self/mounts "$CHROOT_DIR/etc/mtab"


# Initialize the chroot's pacman keyring from the container: gpg inside the
# chroot would run under emulation, and pacman-key's --populate needs
# process substitution (/dev/fd), which requires a mounted /proc the chroot
# does not have. The host container's pacman-key can act on the chroot's
# keyring via --gpgdir once the archlinuxarm keyring is visible to it.
# The -trusted/-revoked companions are required alongside the keyring:
# --populate applies ownertrust only from <keyring>-trusted; without it
# every imported key lands at unknown trust and pacman refuses packages.
for f in archlinuxarm.gpg archlinuxarm-trusted archlinuxarm-revoked; do
cp "$CHROOT_DIR/usr/share/pacman/keyrings/$f" /usr/share/pacman/keyrings/
done
pacman-key --gpgdir "$CHROOT_DIR/etc/pacman.d/gnupg" --init >/dev/null
pacman-key --gpgdir "$CHROOT_DIR/etc/pacman.d/gnupg" --populate archlinuxarm

# /work is not visible inside the chroot: copy the script, its lib.sh and the
# package in.
cp "$PKG_FILE" "$CHROOT_DIR/$(basename "$PKG_FILE")"
cp "$0" "$CHROOT_DIR/smoke-arch.sh"
cp "$(cd "$(dirname "$0")/../common" && pwd)/lib.sh" "$CHROOT_DIR/smoke-lib.sh"

chroot_rc=0
chroot "$CHROOT_DIR" /smoke-arch.sh --chroot "/$(basename "$PKG_FILE")" "$PKG_NAME" "$KIND" || chroot_rc=$?
[ "$chroot_rc" -eq 0 ] || exit "$chroot_rc"
Loading