]> git.hungrycats.org Git - bees/commitdiff
scripts: add build-check for toolchains the default build skips
authorZygo Blaxell <bees@furryterror.org>
Mon, 24 Aug 2026 19:20:07 +0000 (15:20 -0400)
committerZygo Blaxell <bees@furryterror.org>
Sat, 5 Sep 2026 04:03:58 +0000 (00:03 -0400)
The default build is whatever makeflags and a developer's localconf say,
which is one compiler and no -flto.  Distributions build with LTO, and
bees has been broken by it before: BEESTRACE used to construct its
std::function from a lambda that captured nothing, which GCC 12 and 14
read as an uninitialized _Any_data under -flto, and -Werror turned the
false positive into a build failure.  Nothing catches a regression there
unless something builds it on purpose.

Add scripts/build-check, which builds and runs the unit tests for a list
of toolchain and flag combinations, and a `make build-check' target to
invoke it.  The default list is clang and gcc, both with -flto.  Both are
worth covering because they fail in different places: GCC's breakage was
a compile-time diagnostic, while a half-overridden toolchain fails at
link time with "plugin needed to handle lto object" -- easy to hit by
accident, since a localconf that sets only CXX leaves CC as the system cc.

Each combination is built in its own scratch tree under .build-check,
populated from git ls-files so only sources are copied.  Object files
record nothing about the compiler that produced them, so building two
toolchains in one tree would silently mix them; a separate tree also
leaves the working tree's incremental build alone.  BEES_VERSION is
passed in explicitly because the scratch tree has no .git of its own but
sits inside one, so scripts/bees-version would otherwise describe the
enclosing repository.

A combination that passes has its tree deleted, since it is tens of
megabytes of objects nobody will read again.  A failing one is kept, so
the failure can be reproduced by hand where it happened.  Logs are kept
either way.

Verified that both default combinations pass, that a deliberately
mismatched cc/clang++ combination fails and is reported by name with a
non-zero exit, and that successful trees are cleaned up.

Assisted-by: Claude-Code:claude-opus-5
Signed-off-by: Zygo Blaxell <bees@furryterror.org>
.gitignore
Makefile
scripts/build-check [new file with mode: 0755]

index 22057e8720491d81eee9d1b59d0152131734c396..899d8c344d3f567e73d5d3b80f7a3e2ae3986f03 100644 (file)
@@ -13,6 +13,7 @@ latex/
 make.log
 make.log.new
 localconf
+.build-check/
 lib/configure.h
 scripts/beesd
 scripts/beesd@.service
index 668161e93a53921c670c12f15dcdd5848918f6ce..8b52dadaa4a9ca44846a87b3600de403a6baf408 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -30,7 +30,7 @@ reallyall: all doc test
 clean: ## Cleanup
        git clean -dfx -e localconf
 
-.PHONY: lib src test doc
+.PHONY: lib src test doc build-check
 
 lib: ## Build libs
        +$(MAKE) TAG="$(BEES_VERSION)" -C lib
@@ -46,6 +46,9 @@ test: lib src
 doc: ## Build docs
        +$(MAKE) -C docs
 
+build-check: ## Build+test toolchains the default build does not cover (LTO)
+       scripts/build-check
+
 scripts/%: scripts/%.in
        $(TEMPLATE_COMPILER)
 
diff --git a/scripts/build-check b/scripts/build-check
new file mode 100755 (executable)
index 0000000..bc46d8b
--- /dev/null
@@ -0,0 +1,149 @@
+#!/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 "$@"