271 lines
11 KiB
Bash
Executable file
271 lines
11 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# preflight.sh — gates that must pass before pushing.
|
|
#
|
|
# Runs:
|
|
# 1. actionlint across GitHub Actions workflows
|
|
# 2. clang-format --dry-run -Werror across src/ include/ tests/ benchmarks/ fuzz/
|
|
# 3. clang-tidy on the dev compile_commands.json (if available)
|
|
# 4. asan preset: configure, build, ctest
|
|
# 5. tsan preset: configure, build, ctest
|
|
#
|
|
# Exits non-zero on the first failure. Re-run with --skip-tidy to bypass tidy
|
|
# locally if it is not installed.
|
|
|
|
set -euo pipefail
|
|
|
|
ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
cd "${ROOT}"
|
|
|
|
skip_tidy=0
|
|
required_actionlint_version="1.7.12"
|
|
required_clang_format_version="22.1.8"
|
|
for arg in "$@"; do
|
|
case "${arg}" in
|
|
--skip-tidy) skip_tidy=1 ;;
|
|
*) echo "unknown arg: ${arg}" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
bold() { printf '\033[1m%s\033[0m\n' "$*"; }
|
|
fail() { printf '\033[1;31m✗ %s\033[0m\n' "$*" >&2; exit 1; }
|
|
ok() { printf '\033[1;32m✓ %s\033[0m\n' "$*"; }
|
|
|
|
prepare_linux_userns_tests() {
|
|
if [[ "${GLOVE_PREPARE_LINUX_USERNS:-0}" == "1" ]]; then
|
|
if [[ "$(uname -s)" != "Linux" ]]; then
|
|
fail "GLOVE_PREPARE_LINUX_USERNS is supported only on Linux"
|
|
fi
|
|
if ! command -v sudo >/dev/null; then
|
|
fail "GLOVE_PREPARE_LINUX_USERNS requires sudo"
|
|
fi
|
|
if [[ ! -e /proc/sys/kernel/apparmor_restrict_unprivileged_userns ]]; then
|
|
fail "AppArmor unprivileged-userns control is unavailable"
|
|
fi
|
|
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
|
|
fi
|
|
}
|
|
|
|
find_fuzzer_compiler() {
|
|
local candidate=""
|
|
if [[ "$(uname -s)" == "Darwin" ]] && command -v brew >/dev/null; then
|
|
local formula=""
|
|
local prefix=""
|
|
# LLVM 19+ can emit libFuzzer Mach-O relocations rejected by Apple's
|
|
# linker. Prefer the CI-pinned compatible toolchain when available.
|
|
for formula in llvm@18 llvm; do
|
|
if ! prefix="$(brew --prefix "${formula}" 2>/dev/null)"; then
|
|
continue
|
|
fi
|
|
candidate="${prefix}/bin/clang++"
|
|
if [[ -x "${candidate}" ]]; then
|
|
printf '%s\n' "${candidate}"
|
|
return
|
|
fi
|
|
done
|
|
fi
|
|
for candidate in clang++-22 clang++-21 clang++-20 clang++-19 clang++-18 clang++; do
|
|
if command -v "${candidate}" >/dev/null; then
|
|
command -v "${candidate}"
|
|
return
|
|
fi
|
|
done
|
|
fail "a Clang compiler with libFuzzer support is required"
|
|
}
|
|
|
|
# 1. GitHub Actions ---------------------------------------------------------
|
|
bold "[1/5] actionlint"
|
|
if ! command -v actionlint >/dev/null; then
|
|
fail "actionlint not on PATH"
|
|
fi
|
|
actionlint_version="$(actionlint -version | sed -n '1{s/^v//;p;}')"
|
|
if [[ "${actionlint_version}" != "${required_actionlint_version}" ]]; then
|
|
fail "actionlint ${required_actionlint_version} required; found ${actionlint_version}"
|
|
fi
|
|
actionlint
|
|
sh -n setup.sh
|
|
ok "GitHub Actions workflows clean"
|
|
|
|
# 2. format -----------------------------------------------------------------
|
|
bold "[2/5] clang-format --dry-run"
|
|
if ! command -v clang-format >/dev/null; then
|
|
fail "clang-format not on PATH"
|
|
fi
|
|
clang_format_version="$(clang-format --version)"
|
|
if [[ "${clang_format_version}" != *"version ${required_clang_format_version}"* ]]; then
|
|
fail "clang-format ${required_clang_format_version} required; found ${clang_format_version}"
|
|
fi
|
|
fmt_files=()
|
|
while IFS= read -r -d '' f; do
|
|
fmt_files+=("${f}")
|
|
done < <(
|
|
find src include tests benchmarks fuzz -type f \( -name '*.cpp' -o -name '*.hpp' \) -print0 \
|
|
2>/dev/null
|
|
)
|
|
if [[ ${#fmt_files[@]} -eq 0 ]]; then
|
|
ok "no source files yet"
|
|
else
|
|
clang-format --dry-run -Werror "${fmt_files[@]}"
|
|
ok "format clean (${#fmt_files[@]} files)"
|
|
fi
|
|
|
|
# 3. tidy -------------------------------------------------------------------
|
|
bold "[3/5] clang-tidy"
|
|
tidy_bin=""
|
|
if [[ ${skip_tidy} -eq 1 ]]; then
|
|
echo " (skipped via --skip-tidy)"
|
|
else
|
|
if command -v clang-tidy >/dev/null; then
|
|
tidy_bin="$(command -v clang-tidy)"
|
|
elif command -v brew >/dev/null && [[ -x "$(brew --prefix llvm 2>/dev/null)/bin/clang-tidy" ]]; then
|
|
tidy_bin="$(brew --prefix llvm)/bin/clang-tidy"
|
|
fi
|
|
|
|
if [[ -z "${tidy_bin}" ]]; then
|
|
echo " clang-tidy not installed; skipping (install via 'brew install llvm' for the gate)"
|
|
else
|
|
# Benchmarks are opt-in for ordinary builds, but their source remains
|
|
# part of the style gate and must appear in the compilation database.
|
|
cmake --preset dev -DGLOVE_BUILD_BENCHMARKS=ON -DGLOVE_BUILD_FUZZERS=OFF >/dev/null
|
|
# Ninja's C++ dependency scanner writes module-map response files that
|
|
# appear in compile_commands.json. A fresh checkout must build once
|
|
# before clang-tidy can consume those generated arguments.
|
|
cmake --build --preset dev
|
|
# Apple clang's compile_commands omits -isysroot because the driver
|
|
# implicitly knows the SDK; homebrew clang-tidy doesn't, so tell it.
|
|
tidy_extra=()
|
|
if [[ "$(uname -s)" == "Darwin" ]] && command -v xcrun >/dev/null; then
|
|
sdk="$(xcrun --show-sdk-path)"
|
|
tidy_extra=(-extra-arg-before="-isysroot${sdk}")
|
|
fi
|
|
# Lint only translation units represented by the active CMake compile
|
|
# database. Headers are still analyzed through their owning sources,
|
|
# while platform-specific files absent from this host build are not
|
|
# parsed with an unrelated fallback command.
|
|
tidy_files=()
|
|
while IFS= read -r -d '' f; do
|
|
tidy_files+=("${f}")
|
|
done < <(
|
|
python3 - "${ROOT}" <<'PY'
|
|
import json
|
|
import pathlib
|
|
import sys
|
|
|
|
root = pathlib.Path(sys.argv[1]).resolve()
|
|
database = json.loads((root / "build/dev/compile_commands.json").read_text())
|
|
seen = set()
|
|
for entry in database:
|
|
source = pathlib.Path(entry["file"])
|
|
if not source.is_absolute():
|
|
source = pathlib.Path(entry["directory"]) / source
|
|
source = source.resolve()
|
|
try:
|
|
source.relative_to(root)
|
|
except ValueError:
|
|
continue
|
|
if source.suffix != ".cpp" or source in seen:
|
|
continue
|
|
seen.add(source)
|
|
sys.stdout.buffer.write(str(source).encode() + b"\0")
|
|
PY
|
|
)
|
|
if [[ ${#tidy_files[@]} -eq 0 ]]; then
|
|
fail "compile database contains no project translation units"
|
|
fi
|
|
tidy_runner="$(dirname "${tidy_bin}")/run-clang-tidy"
|
|
if [[ ! -x "${tidy_runner}" ]]; then
|
|
tidy_runner="$(dirname "${tidy_bin}")/run-clang-tidy.py"
|
|
fi
|
|
if [[ ! -x "${tidy_runner}" ]]; then
|
|
fail "run-clang-tidy not installed alongside ${tidy_bin}"
|
|
fi
|
|
# Two workers keep memory bounded on hosted runners while reducing the
|
|
# full Linux translation-unit pass from timeout-scale wall-clock time.
|
|
"${tidy_runner}" -j 2 -quiet -p build/dev -clang-tidy-binary "${tidy_bin}" \
|
|
"${tidy_extra[@]}" "${tidy_files[@]}"
|
|
ok "tidy clean (${tidy_bin})"
|
|
fi
|
|
fi
|
|
|
|
# 4. asan -------------------------------------------------------------------
|
|
bold "[4/5] asan preset"
|
|
prepare_linux_userns_tests
|
|
cmake --preset asan -DGLOVE_BUILD_FUZZERS=OFF
|
|
cmake --build --preset asan
|
|
asan_opts="halt_on_error=1:abort_on_error=1"
|
|
if [[ "$(uname -s)" != "Darwin" ]]; then
|
|
# LSan ships with ASan on Linux but not on Apple platforms.
|
|
asan_opts="${asan_opts}:detect_leaks=1"
|
|
fi
|
|
ASAN_OPTIONS="${asan_opts}" \
|
|
UBSAN_OPTIONS="halt_on_error=1:print_stacktrace=1" \
|
|
ctest --preset asan
|
|
fuzzer_cxx="$(find_fuzzer_compiler)"
|
|
fuzzer_symbolizer="$(dirname "${fuzzer_cxx}")/llvm-symbolizer"
|
|
if [[ -x "${fuzzer_symbolizer}" ]]; then
|
|
export ASAN_SYMBOLIZER_PATH="${fuzzer_symbolizer}"
|
|
fi
|
|
cmake --preset fuzz \
|
|
-DCMAKE_CXX_COMPILER="${fuzzer_cxx}" \
|
|
-DFETCHCONTENT_SOURCE_DIR_GLAZE="${ROOT}/build/dev/_deps/glaze-src"
|
|
cmake --build --preset fuzz
|
|
fuzz_workspace="$(mktemp -d "${TMPDIR:-/tmp}/glove-fuzz-corpora.XXXXXX")"
|
|
trap 'rm -rf -- "${fuzz_workspace}"' EXIT
|
|
mcp_fuzz_corpus="${fuzz_workspace}/mcp_codec"
|
|
policy_fuzz_corpus="${fuzz_workspace}/policy_jsonpath"
|
|
manifest_fuzz_corpus="${fuzz_workspace}/change_manifest"
|
|
session_plan_fuzz_corpus="${fuzz_workspace}/session_plan"
|
|
journal_fuzz_corpus="${fuzz_workspace}/change_apply_journal"
|
|
bundle_fuzz_corpus="${fuzz_workspace}/library_bundle"
|
|
cp -R fuzz/corpus/mcp_codec "${mcp_fuzz_corpus}"
|
|
cp -R fuzz/corpus/policy_jsonpath "${policy_fuzz_corpus}"
|
|
cp -R fuzz/corpus/change_manifest "${manifest_fuzz_corpus}"
|
|
cp -R fuzz/corpus/session_plan "${session_plan_fuzz_corpus}"
|
|
cp -R fuzz/corpus/change_apply_journal "${journal_fuzz_corpus}"
|
|
cp -R fuzz/corpus/library_bundle "${bundle_fuzz_corpus}"
|
|
ASAN_OPTIONS="${asan_opts}" \
|
|
UBSAN_OPTIONS="halt_on_error=1:print_stacktrace=1" \
|
|
build/fuzz/fuzz/glove_mcp_codec_fuzzer \
|
|
-runs=10000 -max_len=65536 -timeout=5 -rss_limit_mb=2048 -verbosity=0 \
|
|
-artifact_prefix="${mcp_fuzz_corpus}/" \
|
|
"${mcp_fuzz_corpus}"
|
|
ASAN_OPTIONS="${asan_opts}" \
|
|
UBSAN_OPTIONS="halt_on_error=1:print_stacktrace=1" \
|
|
build/fuzz/fuzz/glove_policy_jsonpath_fuzzer \
|
|
-runs=10000 -max_len=65536 -timeout=5 -rss_limit_mb=2048 -verbosity=0 \
|
|
-artifact_prefix="${policy_fuzz_corpus}/" \
|
|
"${policy_fuzz_corpus}"
|
|
ASAN_OPTIONS="${asan_opts}" \
|
|
UBSAN_OPTIONS="halt_on_error=1:print_stacktrace=1" \
|
|
build/fuzz/fuzz/glove_change_manifest_fuzzer \
|
|
-runs=10000 -max_len=65536 -timeout=5 -rss_limit_mb=2048 -verbosity=0 \
|
|
-artifact_prefix="${manifest_fuzz_corpus}/" \
|
|
"${manifest_fuzz_corpus}"
|
|
ASAN_OPTIONS="${asan_opts}" \
|
|
UBSAN_OPTIONS="halt_on_error=1:print_stacktrace=1" \
|
|
build/fuzz/fuzz/glove_session_plan_fuzzer \
|
|
-runs=10000 -max_len=65536 -timeout=5 -rss_limit_mb=2048 -verbosity=0 \
|
|
-artifact_prefix="${session_plan_fuzz_corpus}/" \
|
|
"${session_plan_fuzz_corpus}"
|
|
ASAN_OPTIONS="${asan_opts}" \
|
|
UBSAN_OPTIONS="halt_on_error=1:print_stacktrace=1" \
|
|
build/fuzz/fuzz/glove_change_apply_journal_fuzzer \
|
|
-runs=10000 -max_len=65536 -timeout=5 -rss_limit_mb=2048 -verbosity=0 \
|
|
-artifact_prefix="${journal_fuzz_corpus}/" \
|
|
"${journal_fuzz_corpus}"
|
|
ASAN_OPTIONS="${asan_opts}" \
|
|
UBSAN_OPTIONS="halt_on_error=1:print_stacktrace=1" \
|
|
build/fuzz/fuzz/glove_library_bundle_fuzzer \
|
|
-runs=10000 -max_len=65536 -timeout=5 -rss_limit_mb=2048 -verbosity=0 \
|
|
-artifact_prefix="${bundle_fuzz_corpus}/" \
|
|
"${bundle_fuzz_corpus}"
|
|
ok "asan ok"
|
|
|
|
# 5. tsan -------------------------------------------------------------------
|
|
bold "[5/5] tsan preset"
|
|
cmake --preset tsan -DGLOVE_BUILD_FUZZERS=OFF
|
|
cmake --build --preset tsan
|
|
TSAN_OPTIONS="halt_on_error=1:second_deadlock_stack=1" \
|
|
ctest --preset tsan
|
|
ok "tsan ok"
|
|
|
|
bold "all gates passed"
|