Files
cozystack/hack/image-builders-cross-compile.bats
T
Myasnikov Daniil cc942fed68 test(tests): run unit suites with Bats and retain POSIX coverage
The shell translator rewrites test bodies and loses Bats reporting.
The unit lane needs complete TAP results and JUnit reports without
changing how live-cluster suites run.

Sourced production helpers still need a dash pass, and unit test bodies
need nounset without exporting shell options into subprocesses.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-10-07 11:04:00 +03:00

142 lines
6.6 KiB
Bash

#!/usr/bin/env bats
# A builder stage that compiles something for the image must run on the build
# platform. `FROM --platform=$BUILDPLATFORM` keeps the toolchain native under a
# multi-platform build; without it the compiler itself runs under QEMU, which
# is slow and, for Go, unreliable (the migration-controller Dockerfile records
# a "found bad pointer in Go heap" crash from exactly that). A native builder
# must then be told which architecture to produce: a `go build` with no GOARCH
# silently emits a binary for the build host, and nothing fails until a node
# of the other architecture pulls it.
#
# A stage compiles when one of its RUN commands is go build/test/install, make,
# gradle, dpkg-buildpackage, a node package manager or a script whose file name
# contains "build" and ends in .sh, at command position (after RUN and its
# --flag options, `&&`, `;` or `||`, past any VAR=value prefixes). Such a stage
# must run on the build platform (see stages() below) and use TARGETARCH or
# TARGETPLATFORM on a RUN or ENV line, unless its output carries no
# architecture at all (ARCH_NEUTRAL below). A bare
# `ARG TARGETARCH` does not count: it declares the variable, and a `go build`
# that never reads it still builds for the host. The check is per stage, not
# per command: a stage with `RUN echo $TARGETARCH` and a bare `go build` would
# pass.
#
# Requires: awk, sed.
load test_helper
REPO_ROOT="$(cd "$(dirname "${BATS_TEST_FILENAME:-$0}")/.." && pwd)"
# Outputs with no architecture, one entry per line: a whole file, or
# "<file>:<stage>" when only that stage is neutral and the file's other
# stages still have to target TARGETARCH. The console, the Harbor portal and
# shadowbox's webpack stage build static JS; piraeus-server builds Java .debs
# that linstor-server's debian/control declares `Architecture: all`.
ARCH_NEUTRAL="packages/apps/vpn/images/shadowbox/Dockerfile:app
packages/system/dashboard/images/console/Containerfile
packages/system/harbor/images/harbor-portal/Dockerfile
packages/system/linstor/images/piraeus-server/Dockerfile"
# is_neutral <file> <stage> <list>: 1 when the list names the file or that stage of it.
is_neutral() {
printf '%s\n' "$3" | awk -v f="$1" -v s="$1:$2" '$0 == f || $0 == s { hit = 1 } END { print hit + 0 }'
}
# One line per stage: "<compiles 0|1> <targets 0|1> <native 0|1> <name> <FROM line>".
# A stage is native when its FROM carries --platform=$BUILDPLATFORM, or when it
# builds FROM an earlier native stage: a stage inherits the platform of the
# stage it starts from. An unnamed stage is reported as "-".
# Continuation lines are joined first so a command after `&& \` is seen at
# command position.
stages() {
sed -e ':a' -e '/\\$/N; s/\\\n//; ta' "$1" | awk '
function flush() { if (from != "") printf "%d %d %d %s %s\n", compiles, targets, native, name, from }
/^FROM[[:space:]]/ {
flush(); from = $0; compiles = 0; targets = 0; native = 0; name = "-"; base = ""
for (i = 2; i <= NF; i++) {
if ($i ~ /^--platform=(\$BUILDPLATFORM|\$\{BUILDPLATFORM\})$/) native = 1
else if ($i !~ /^--/ && base == "") base = $i
else if (tolower($i) == "as" && i < NF) name = $(i + 1)
}
if (base in nativestage) native = 1
if (native && name != "-") nativestage[name] = 1
next
}
/^RUN[[:space:]]/ && /(^RUN([[:space:]]+--[^[:space:]]+)*|&&|;|[|][|])[[:space:]]+([A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+)*(go[[:space:]]+(build|test|install)|make|gradle|[.]\/gradlew|dpkg-buildpackage|pnpm|npm|yarn|[^[:space:]]*build[^[:space:]\/]*[.]sh)([[:space:]]|$)/ { compiles = 1 }
/^(RUN|ENV)[[:space:]]/ && /TARGETARCH|TARGETPLATFORM/ { targets = 1 }
END { flush() }
'
}
@test "every compiling builder stage runs on BUILDPLATFORM and targets TARGETARCH" {
failed=""
checked=0
stagelist="$(mktemp)"
# Package paths carry no whitespace (the same assumption hack/build-matrix.sh
# makes), so word-splitting find's output is safe.
for f in $(find "$REPO_ROOT"/packages -path '*/charts' -prune -o \( -name Dockerfile -o -name Containerfile \) -type f -print | sort); do
rel="${f#"$REPO_ROOT"/}"
stages "$f" >"$stagelist"
while read -r compiles targets native name from; do
[ "$compiles" = 1 ] || continue
checked=$((checked + 1))
if [ "$native" = 0 ]; then
failed="$failed
$rel: $from (not on BUILDPLATFORM)"
continue
fi
if [ "$targets" = 0 ] && [ "$(is_neutral "$rel" "$name" "$ARCH_NEUTRAL")" != 1 ]; then
failed="$failed
$rel: $from (compiles without TARGETARCH)"
fi
done <"$stagelist"
done
rm -f "$stagelist"
[ "$checked" -gt 0 ] # the find must reach compiling stages at all
if [ -n "$failed" ]; then
echo "builder stages that would compile under emulation or for the wrong arch:$failed" >&2
exit 1
fi
}
@test "a compile after RUN options still counts as a compile" {
fixture="$(mktemp)"
printf '%s\n' 'FROM golang AS builder' 'RUN --mount=type=cache,target=/root/.cache go build .' >"$fixture"
out="$(stages "$fixture")"
rm -f "$fixture"
[ "$out" = "1 0 0 builder FROM golang AS builder" ]
}
@test "a build script with a suffix after build still counts as a compile" {
fixture="$(mktemp)"
printf '%s\n' 'FROM --platform=$BUILDPLATFORM golang AS build' 'RUN ./hack/build-go.sh' >"$fixture"
out="$(stages "$fixture")"
rm -f "$fixture"
[ "$out" = '1 0 1 build FROM --platform=$BUILDPLATFORM golang AS build' ]
}
@test "a script under a build/ directory is not a build script" {
fixture="$(mktemp)"
printf '%s\n' 'FROM golang AS x' 'RUN ./build/install.sh' >"$fixture"
out="$(stages "$fixture")"
rm -f "$fixture"
[ "$out" = "0 0 0 x FROM golang AS x" ]
}
@test "a stage built FROM an earlier BUILDPLATFORM stage runs on the build platform too" {
fixture="$(mktemp)"
printf '%s\n' 'FROM --platform=$BUILDPLATFORM node AS source' 'RUN true' 'FROM source AS app' 'RUN npm ci' 'FROM node AS emulated' 'RUN npm ci' 'FROM emulated AS child' 'RUN go build .' >"$fixture"
out="$(stages "$fixture")"
rm -f "$fixture"
[ "$(printf '%s\n' "$out" | sed -n 2p)" = "1 0 1 app FROM source AS app" ]
[ "$(printf '%s\n' "$out" | sed -n 3p)" = "1 0 0 emulated FROM node AS emulated" ]
# A stage built from an emulated stage stays emulated.
[ "$(printf '%s\n' "$out" | sed -n 4p)" = "1 0 0 child FROM emulated AS child" ]
}
@test "an arch-neutral stage entry excuses only that stage" {
[ "$(is_neutral packages/x/Dockerfile app 'packages/x/Dockerfile:app')" = 1 ]
[ "$(is_neutral packages/x/Dockerfile ss 'packages/x/Dockerfile:app')" = 0 ]
[ "$(is_neutral packages/y/Dockerfile any 'packages/y/Dockerfile')" = 1 ]
}