Files
Johnathan Corgan 308b68b595 Add an opt-in cross-target compile check to local CI
Local CI builds and lints only for the Linux host, so a change that breaks
the macOS, Windows or FreeBSD build, such as a Unix-only item used without
a cfg gate, passes it and the internal builder and first goes red on
GitHub's non-Linux jobs.

testing/cross-check/check.sh runs cargo check --workspace --all-targets for
x86_64-apple-darwin, x86_64-pc-windows-gnu and x86_64-unknown-freebsd, with
zig cc and zig ar as the C compiler and archiver for crates that build C.
All three share one target directory, target/cross-check/, reused between
runs, so the host-side build scripts and proc macros are built once. It can
be run on its own, or by testing/ci-local.sh with --cross-check, which adds
it to the build stage after clippy and records one result per target. It is
off by default because it needs zig.

A missing Rust target is installed with rustup target add. The targets are
deliberately not listed in rust-toolchain.toml, since rustup would then
fetch them for every cargo call in the repository, the package builds
included, and fail all of them when its download server is unreachable.
When the check is asked for, a missing zig or a target that cannot be
installed fails the run with a message naming it.

The check is local-only by design, since GitHub CI builds and tests on real
macOS, Windows and FreeBSD runners; the parity guard's header says so. The
shellcheck guard now lints the three new scripts.
2026-10-13 14:42:13 +00:00

147 lines
6.3 KiB
Bash
Executable File

#!/bin/bash
# ── Cross-target compile check ──────────────────────────────────────────────
# Compiles the crate, its tests, examples and benches for the non-Linux
# targets FIPS ships on, so a change that breaks the macOS, Windows or FreeBSD
# build fails here rather than first on GitHub. The usual shape of such a break
# is a Unix-only item (a std::os::unix import, a libc call, a field that exists
# only under cfg(unix)) used in code that is compiled on every target without
# a cfg gate.
#
# Usage: check.sh [--list | <target>...]
# With no argument, checks every target below in turn. With targets named,
# checks only those, each of which must be on the list. --list prints the
# targets, one per line, and exits; ci-local.sh reads it so that it records
# one result per target without keeping a second copy of the list.
#
# Each target runs
# cargo check --workspace --all-targets --target <target>
# which type-checks and runs build scripts but does not link, so no target
# linker or SDK is needed. Build scripts that compile C through the cc crate
# get zig as their C compiler and archiver, through the zig-cc and zig-ar
# wrappers beside this script. The Linux target is not checked here: the build
# and clippy stages of ci-local.sh compile it already.
#
# All targets build in one target directory,
# ${CARGO_TARGET_DIR:-<repo>/target}/cross-check
# where cargo keeps each target's output under its own <target>/ and builds the
# host-side build scripts and proc macros once for all three. It is kept
# between runs so incremental reuse works, and kept apart from the normal build
# so neither invalidates the other. Like any cargo target directory it grows
# as the source changes; `cargo clean` removes it.
#
# Requirements, each reported by name when missing:
# zig ${ZIG:-zig}, found on PATH unless ZIG names it (0.16.0 is the
# version this was written against).
# the Rust standard library for each target, for the toolchain
# rust-toolchain.toml pins. A missing one is installed with
# `rustup target add` when rustup is present. The targets are
# deliberately not listed in rust-toolchain.toml: rustup would
# then fetch them for every cargo call in the repository, on every
# host and in every container, and fail every one of those calls
# when the download server cannot be reached.
#
# Exit 0 = every target compiled. Exit 1 = at least one target failed to
# compile. Exit 2 = the check could not run (zig, cargo or a target's standard
# library missing and not installable, or an unknown target named); never
# treated as a pass.
# ─────────────────────────────────────────────────────────────────────────────
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
# Rust target triple, and the zig target its C code is compiled for.
TARGETS=(
"x86_64-apple-darwin x86_64-macos"
"x86_64-pc-windows-gnu x86_64-windows-gnu"
"x86_64-unknown-freebsd x86_64-freebsd"
)
cannot() { echo "cross-check: $*" >&2; exit 2; }
# The zig target for a Rust triple, or nothing if the triple is not listed.
zig_target() {
local entry
for entry in "${TARGETS[@]}"; do
if [[ "${entry%% *}" == "$1" ]]; then
echo "${entry#* }"
return 0
fi
done
return 1
}
list_targets() {
local entry
for entry in "${TARGETS[@]}"; do echo "${entry%% *}"; done
return 0
}
if [[ "${1:-}" == "--list" ]]; then
list_targets
exit 0
fi
selected=("$@")
if [[ ${#selected[@]} -eq 0 ]]; then
mapfile -t selected < <(list_targets)
fi
for t in "${selected[@]}"; do
zig_target "$t" >/dev/null || cannot "unknown target '$t'; known targets: $(list_targets | tr '\n' ' ')"
done
cd "$PROJECT_ROOT" || cannot "cannot cd to $PROJECT_ROOT"
command -v cargo >/dev/null 2>&1 || cannot "cargo not found on PATH"
command -v rustc >/dev/null 2>&1 || cannot "rustc not found on PATH"
zig_name="${ZIG:-zig}"
zig_path="$(command -v "$zig_name" 2>/dev/null)" \
|| cannot "zig not found (looked for '$zig_name' on PATH). Install zig 0.16.0 or set ZIG to its path, or run ci-local.sh without --cross-check."
zig_version="$("$zig_path" version 2>/dev/null)" \
|| cannot "'$zig_path version' failed; the zig install there is broken"
export ZIG="$zig_path"
# Resolved inside the repository, so rustup applies rust-toolchain.toml and
# `rustup target add` installs into the toolchain cargo will use.
sysroot="$(rustc --print sysroot 2>/dev/null)" || cannot "'rustc --print sysroot' failed"
for t in "${selected[@]}"; do
[[ -d "$sysroot/lib/rustlib/$t/lib" ]] && continue
command -v rustup >/dev/null 2>&1 \
|| cannot "Rust target $t is not installed for the toolchain at $sysroot, and there is no rustup to install it."
echo "cross-check: installing Rust target $t (rustup target add $t)"
rustup target add "$t" \
|| cannot "Rust target $t is not installed for the toolchain at $sysroot, and 'rustup target add $t' failed."
[[ -d "$sysroot/lib/rustlib/$t/lib" ]] \
|| cannot "'rustup target add $t' succeeded, but $sysroot/lib/rustlib/$t/lib is still missing."
done
echo "cross-check: zig $zig_version at $zig_path"
echo "cross-check: $(rustc --version)"
target_root="${CARGO_TARGET_DIR:-$PROJECT_ROOT/target}/cross-check"
failed=()
for t in "${selected[@]}"; do
z="$(zig_target "$t")"
v="${t//-/_}"
start=$SECONDS
echo "==> cargo check --workspace --all-targets --target $t (C via zig cc -target $z)"
rc=0
env "CC_$v=$SCRIPT_DIR/zig-cc" "AR_$v=$SCRIPT_DIR/zig-ar" ZTARGET="$z" \
CARGO_TARGET_DIR="$target_root" \
cargo check --workspace --all-targets --target "$t" 2>&1 || rc=$?
if [[ $rc -eq 0 ]]; then
echo " PASS $t ($(( SECONDS - start ))s)"
else
echo " FAIL $t (cargo exited $rc after $(( SECONDS - start ))s)"
failed+=("$t")
fi
done
if [[ ${#failed[@]} -ne 0 ]]; then
echo "cross-check FAILED: ${failed[*]}"
exit 1
fi
echo "cross-check PASS (${#selected[@]} targets: ${selected[*]})"
exit 0