--- /dev/null
+#!/bin/bash
+#
+# Build and unit-test bees with toolchain and flag combinations that the
+# default build does not cover.
+#
+# The default build is whatever `makeflags` and a developer's `localconf` say,
+# which in practice is one compiler and no -flto. Combinations outside that
+# still have to work: distributions build with LTO, and bees has been broken by
+# LTO before (see the -Wmaybe-uninitialized false positive in BEESTRACE that
+# used to break GCC 12 and 14). Nothing catches a regression there unless it is
+# built on purpose.
+#
+# Each combination is built in its own scratch tree, so the working tree's
+# incremental build and its object files are left alone. Only source files are
+# copied -- git decides which those are -- so no object built by one compiler
+# can be picked up by another.
+#
+# Usage:
+# scripts/build-check [combination ...]
+#
+# With no arguments, every combination in DEFAULT_COMBINATIONS is built. A
+# combination is named "<cc>:<cxx>:<extra flags>", for example:
+#
+# scripts/build-check clang:clang++:-flto
+# scripts/build-check gcc:g++:'-flto -D_GLIBCXX_ASSERTIONS'
+#
+# A combination that passes has its scratch tree deleted; one that fails keeps
+# it, so the failure can be reproduced by hand in the tree that produced it.
+# Build logs are kept either way, next to the trees.
+#
+# Environment:
+# BUILD_CHECK_DIR scratch directory to build in (default .build-check)
+# MAKEJOBS -j value for the inner builds (default: nproc)
+
+set -u -o pipefail
+
+# Both LTO combinations, because the two toolchains fail differently: GCC's
+# historical breakage was a false-positive warning at compile time, while a
+# mismatched CC/CXX pair fails at link time with "plugin needed to handle lto
+# object". The latter is easy to hit by accident, because localconf overriding
+# only CXX leaves CC as the system default cc.
+DEFAULT_COMBINATIONS=(
+ 'clang:clang++:-flto'
+ 'gcc:g++:-flto'
+)
+
+readonly SOURCE_DIR="$(cd "$(dirname "$0")/.." && pwd)"
+readonly SCRATCH_DIR="${BUILD_CHECK_DIR:-$SOURCE_DIR/.build-check}"
+readonly JOBS="${MAKEJOBS:-$(nproc)}"
+readonly VERSION="$("$SOURCE_DIR/scripts/bees-version" "$SOURCE_DIR")"
+
+# Copy the sources git knows about: tracked files plus untracked ones that are
+# not ignored. That is exactly the working tree minus build artifacts, so a
+# new source file that has not been committed yet is still tested, while no
+# *.o, *.dep, localconf or bin/bees comes along.
+populate_tree() {
+ local dest="$1"
+
+ rm -rf -- "$dest"
+ mkdir -p -- "$dest"
+ (
+ cd "$SOURCE_DIR" || exit 1
+ git ls-files -z --cached --others --exclude-standard
+ ) | (cd "$SOURCE_DIR" && cpio -0pdmu --quiet "$dest") || return 1
+}
+
+# Run one combination. Returns the make exit status.
+run_combination() {
+ local cc="$1" cxx="$2" flags="$3" dest="$4" log="$5"
+
+ populate_tree "$dest" || return 1
+
+ # The Makefiles -include ../localconf after ../makeflags, so this is the
+ # supported override point. Set CC and CXX together: -flto embeds
+ # compiler-specific IR in the object files, and GNU ld cannot read GCC's
+ # without the matching plugin, so a half-overridden toolchain fails at
+ # link time rather than falling back to a working build.
+ cat > "$dest/localconf" <<-EOF
+ CC=$cc
+ CXX=$cxx
+ CCFLAGS += $flags
+ EOF
+
+ # The scratch tree has no .git of its own, and it sits inside the source
+ # tree's, so scripts/bees-version would describe the enclosing repository
+ # by accident. Pass the version the source tree reports instead.
+ #
+ # `test' builds lib and src as prerequisites, so this covers compiling,
+ # linking and running the unit tests.
+ BEES_VERSION="$VERSION" make -C "$dest" -j"$JOBS" test > "$log" 2>&1
+}
+
+main() {
+ local -a combinations
+ if [ "$#" -gt 0 ]; then
+ combinations=("$@")
+ else
+ combinations=("${DEFAULT_COMBINATIONS[@]}")
+ fi
+
+ # Fail early on a combination naming a compiler that is not installed,
+ # rather than reporting it as a bees build failure.
+ local combination cc cxx
+ for combination in "${combinations[@]}"; do
+ IFS=: read -r cc cxx _ <<<"$combination"
+ if [ -z "$cc" ] || [ -z "$cxx" ]; then
+ echo "build-check: malformed combination '$combination', want <cc>:<cxx>:<flags>" >&2
+ return 2
+ fi
+ if ! command -v "$cc" >/dev/null || ! command -v "$cxx" >/dev/null; then
+ echo "build-check: $combination: $cc or $cxx not installed" >&2
+ return 2
+ fi
+ done
+
+ mkdir -p -- "$SCRATCH_DIR" || return 1
+
+ local flags name dest log rc
+ local -a failed=()
+ for combination in "${combinations[@]}"; do
+ IFS=: read -r cc cxx flags <<<"$combination"
+ name="$(echo "$cc$flags" | tr -c 'A-Za-z0-9_.-' '_')"
+ dest="$SCRATCH_DIR/$name"
+ log="$SCRATCH_DIR/$name.log"
+
+ echo "build-check: $cc/$cxx $flags"
+ run_combination "$cc" "$cxx" "$flags" "$dest" "$log"
+ rc="$?"
+ if [ "$rc" -eq 0 ]; then
+ # A successful tree is ~30-60MB of objects nobody will
+ # read again. A failed one is the only place to look at
+ # what went wrong, so keep that. Logs are kept either way.
+ rm -rf -- "$dest"
+ echo "build-check: $cc/$cxx $flags: ok"
+ else
+ echo "build-check: $cc/$cxx $flags: FAILED (rc $rc), last 20 lines of $log:" >&2
+ tail -20 "$log" >&2
+ failed+=("$combination")
+ fi
+ done
+
+ if [ "${#failed[@]}" -gt 0 ]; then
+ echo "build-check: ${#failed[@]} of ${#combinations[@]} combinations failed: ${failed[*]}" >&2
+ return 1
+ fi
+ echo "build-check: all ${#combinations[@]} combinations ok"
+}
+
+main "$@"