Ctrl K
ComunidadGuías…

hack/image-builders-cross-compile.bats

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/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 ]
}