|
| 1 | +#!/usr/bin/env bats |
| 2 | +# A builder stage that compiles something for the image must run on the build |
| 3 | +# platform. `FROM --platform=$BUILDPLATFORM` keeps the toolchain native under a |
| 4 | +# multi-platform build; without it the compiler itself runs under QEMU, which |
| 5 | +# is slow and, for Go, unreliable (the migration-controller Dockerfile records |
| 6 | +# a "found bad pointer in Go heap" crash from exactly that). A native builder |
| 7 | +# must then be told which architecture to produce: a `go build` with no GOARCH |
| 8 | +# silently emits a binary for the build host, and nothing fails until a node |
| 9 | +# of the other architecture pulls it. |
| 10 | +# |
| 11 | +# A stage compiles when one of its RUN commands is go build/test/install, make, |
| 12 | +# gradle, dpkg-buildpackage, a node package manager or a *build.sh script, at |
| 13 | +# command position (after RUN and its --flag options, `&&`, `;` or `||`, past |
| 14 | +# any VAR=value prefixes). Such a stage must sit on a BUILDPLATFORM FROM and |
| 15 | +# use TARGETARCH or TARGETPLATFORM on a RUN or ENV line, unless its output |
| 16 | +# carries no architecture at all (ARCH_NEUTRAL below). A bare |
| 17 | +# `ARG TARGETARCH` does not count: it declares the variable, and a `go build` |
| 18 | +# that never reads it still builds for the host. The check is per stage, not |
| 19 | +# per command: a stage with `RUN echo $TARGETARCH` and a bare `go build` would |
| 20 | +# pass. |
| 21 | +# |
| 22 | +# Written for POSIX sh: hack/cozytest.sh sources this file into /bin/sh. |
| 23 | +# |
| 24 | +# Requires: awk, sed. |
| 25 | + |
| 26 | +REPO_ROOT="$(cd "$(dirname "${BATS_TEST_FILENAME:-$0}")/.." && pwd)" |
| 27 | + |
| 28 | +# Outputs with no architecture: the console builds static JS; piraeus-server |
| 29 | +# builds Java .debs that linstor-server's debian/control declares |
| 30 | +# `Architecture: all`. |
| 31 | +ARCH_NEUTRAL="packages/system/dashboard/images/console/Containerfile |
| 32 | +packages/system/linstor/images/piraeus-server/Dockerfile" |
| 33 | + |
| 34 | +# One line per stage: "<compiles 0|1> <targets 0|1> <FROM line>". |
| 35 | +# Continuation lines are joined first so a command after `&& \` is seen at |
| 36 | +# command position. |
| 37 | +stages() { |
| 38 | + sed -e ':a' -e '/\\$/N; s/\\\n//; ta' "$1" | awk ' |
| 39 | + function flush() { if (from != "") printf "%d %d %s\n", compiles, targets, from } |
| 40 | + /^FROM[[:space:]]/ { flush(); from = $0; compiles = 0; targets = 0; next } |
| 41 | + /^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[.]sh)([[:space:]]|$)/ { compiles = 1 } |
| 42 | + /^(RUN|ENV)[[:space:]]/ && /TARGETARCH|TARGETPLATFORM/ { targets = 1 } |
| 43 | + END { flush() } |
| 44 | + ' |
| 45 | +} |
| 46 | + |
| 47 | +@test "every compiling builder stage runs on BUILDPLATFORM and targets TARGETARCH" { |
| 48 | + failed="" |
| 49 | + checked=0 |
| 50 | + stagelist="$(mktemp)" |
| 51 | + # Package paths carry no whitespace (the same assumption hack/build-matrix.sh |
| 52 | + # makes), so word-splitting find's output is safe. |
| 53 | + for f in $(find "$REPO_ROOT"/packages -path '*/charts' -prune -o \( -name Dockerfile -o -name Containerfile \) -type f -print | sort); do |
| 54 | + rel="${f#"$REPO_ROOT"/}" |
| 55 | + neutral=0 |
| 56 | + case "$ARCH_NEUTRAL" in *"$rel"*) neutral=1 ;; esac |
| 57 | + stages "$f" >"$stagelist" |
| 58 | + while read -r compiles targets from; do |
| 59 | + [ "$compiles" = 1 ] || continue |
| 60 | + checked=$((checked + 1)) |
| 61 | + case "$from" in |
| 62 | + *'--platform=$BUILDPLATFORM'*|*'--platform=${BUILDPLATFORM}'*) ;; |
| 63 | + *) failed="$failed |
| 64 | + $rel: $from (not on BUILDPLATFORM)"; continue ;; |
| 65 | + esac |
| 66 | + if [ "$neutral" = 0 ] && [ "$targets" = 0 ]; then |
| 67 | + failed="$failed |
| 68 | + $rel: $from (compiles without TARGETARCH)" |
| 69 | + fi |
| 70 | + done <"$stagelist" |
| 71 | + done |
| 72 | + rm -f "$stagelist" |
| 73 | + |
| 74 | + [ "$checked" -gt 0 ] # the find must reach compiling stages at all |
| 75 | + if [ -n "$failed" ]; then |
| 76 | + echo "builder stages that would compile under emulation or for the wrong arch:$failed" >&2 |
| 77 | + exit 1 |
| 78 | + fi |
| 79 | +} |
| 80 | + |
| 81 | +@test "a compile after RUN options still counts as a compile" { |
| 82 | + fixture="$(mktemp)" |
| 83 | + printf '%s\n' 'FROM golang AS builder' 'RUN --mount=type=cache,target=/root/.cache go build .' >"$fixture" |
| 84 | + out="$(stages "$fixture")" |
| 85 | + rm -f "$fixture" |
| 86 | + [ "$out" = "1 0 FROM golang AS builder" ] |
| 87 | +} |
0 commit comments