UltrafastSecp256k1/scripts/local-ci.sh

786 lines
31 KiB
Bash

#!/usr/bin/env bash
# =============================================================================
# local-ci.sh -- Run full CI jobs locally (inside Docker container)
# =============================================================================
# Reproduces GitHub Actions workflows locally:
# security-audit.yml + ci.yml + audit-report.yml + clang-tidy.yml + cppcheck.yml
#
# Usage:
# bash scripts/local-ci.sh --quick # Fast gate: werror + ci (Release) ~5 min
# bash scripts/local-ci.sh --all # Standard: 7 jobs ~20-25 min
# bash scripts/local-ci.sh --full # Everything: 10 jobs ~45-60 min
# bash scripts/local-ci.sh --job werror # Only -Werror build
# bash scripts/local-ci.sh --job asan # Only ASan+UBSan
# bash scripts/local-ci.sh --job tsan # Only TSan
# bash scripts/local-ci.sh --job valgrind # Only Valgrind memcheck
# bash scripts/local-ci.sh --job dudect # Only dudect smoke
# bash scripts/local-ci.sh --job audit # Only unified_audit_runner (641K checks)
# bash scripts/local-ci.sh --job coverage # Only code coverage (HTML report)
# bash scripts/local-ci.sh --job clang-tidy # Only clang-tidy static analysis
# bash scripts/local-ci.sh --job cppcheck # Only cppcheck static analysis
# bash scripts/local-ci.sh --job ci # CI matrix (GCC+Clang x Debug+Release)
# bash scripts/local-ci.sh --job valgrind-ct # Valgrind CT taint analysis
# bash scripts/local-ci.sh --job bench # Quick benchmark snapshot (no regression check)
# bash scripts/local-ci.sh --job qemu-smoke # ARM64/RISC-V cross-build + QEMU smoke
# bash scripts/local-ci.sh --job trust # Trust signals summary (coverage + links)
# bash scripts/local-ci.sh --list # List all available jobs
#
# Build dirs use /tmp/build-local-ci-* (not /src) to avoid Windows NTFS overhead.
# Exit codes: 0 = all passed, 1 = at least one job failed
# =============================================================================
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
BOLD='\033[1m'
# Auto-detect source dir: /src in Docker, otherwise derive from script location
if [ -d "/src/cpu" ]; then
SRC="/src"
else
SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
fi
NPROC=$(nproc)
RESULTS=()
FAILED=0
# Build dirs go to /tmp to avoid Windows NTFS write overhead.
# Second+ builds reuse ccache — rebuild of unchanged code takes seconds.
BUILD_BASE="/tmp/build-local-ci"
# -- Auto-detect compilers ----------------------------------------------------
# GCC: prefer gcc-13, fall back to gcc
if command -v gcc-13 &>/dev/null; then
GCC_CC=gcc-13; GCC_CXX=g++-13
elif command -v gcc &>/dev/null; then
GCC_CC=gcc; GCC_CXX=g++
echo -e "${YELLOW}warning: gcc-13 not found, using $(gcc --version | head -1)${NC}"
else
GCC_CC=""; GCC_CXX=""
echo -e "${YELLOW}warning: no gcc found — GCC jobs will be skipped${NC}"
fi
# Clang: prefer 17, then 18, 19, 20, 21, then unversioned
CLANG_CC="" CLANG_CXX=""
for v in 17 18 19 20 21; do
if command -v "clang-$v" &>/dev/null; then
CLANG_CC="clang-$v"; CLANG_CXX="clang++-$v"; break
fi
done
if [ -z "$CLANG_CC" ] && command -v clang &>/dev/null; then
CLANG_CC=clang; CLANG_CXX=clang++
fi
if [ -z "$CLANG_CC" ]; then
echo -e "${YELLOW}warning: no clang found — Clang jobs will be skipped${NC}"
else
echo -e "${BOLD}Clang:${NC} $CLANG_CC ($($CLANG_CC --version 2>/dev/null | head -1))"
fi
# LLVM tools: derive version suffix from detected clang
LLVM_SUFFIX=""
if [ -n "$CLANG_CC" ]; then
_ver="${CLANG_CC#clang-}" # e.g. "18" from "clang-18", or "clang" if unversioned
if [ "$_ver" != "$CLANG_CC" ] && [ -n "$_ver" ]; then
LLVM_SUFFIX="-$_ver"
fi
fi
LLVM_PROFDATA="llvm-profdata${LLVM_SUFFIX}"
LLVM_COV="llvm-cov${LLVM_SUFFIX}"
CLANG_TIDY="clang-tidy${LLVM_SUFFIX}"
# -- ccache stats (if available) ----------------------------------------------
if command -v ccache &>/dev/null && [ -d "${CCACHE_DIR:-/ccache}" ]; then
echo -e "${BOLD}ccache:${NC} ${CCACHE_DIR:-/ccache} ($(ccache -s 2>/dev/null | grep 'cache size' || echo 'empty'))"
ccache --zero-stats &>/dev/null || true
CCACHE_ENABLED=1
else
CCACHE_ENABLED=0
fi
banner() {
echo ""
echo -e "${CYAN}+==============================================================+${NC}"
echo -e "${CYAN}|${NC} ${BOLD}$1${NC}"
echo -e "${CYAN}+==============================================================+${NC}"
echo ""
}
pass() {
RESULTS+=("${GREEN}OK PASS${NC}: $1")
echo -e "\n${GREEN}OK PASS${NC}: $1\n"
}
fail() {
RESULTS+=("${RED}X FAIL${NC}: $1")
FAILED=1
echo -e "\n${RED}X FAIL${NC}: $1\n"
}
# -----------------------------------------------------------------------------
# Job: werror — Build with -Werror (GCC-13, Release)
# Mirrors: security-audit.yml / compiler-warnings
# -----------------------------------------------------------------------------
job_werror() {
if [ -z "$GCC_CXX" ]; then fail "werror: gcc not found"; return; fi
banner "werror: Build -Werror -Wall -Wextra ($GCC_CXX, Release)"
local build_dir="${BUILD_BASE}-werror"
cmake -S "$SRC" -B "$build_dir" -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_COMPILER="$GCC_CXX" \
-DCMAKE_CXX_FLAGS="-Werror -Wall -Wextra -Wpedantic -Wconversion -Wshadow" \
-DSECP256K1_BUILD_TESTS=ON
if cmake --build "$build_dir" -j"$NPROC"; then
pass "werror: Build with -Werror"
else
fail "werror: Build with -Werror"
fi
}
# -----------------------------------------------------------------------------
# Job: asan — ASan + UBSan (Clang-17, Debug)
# Mirrors: ci.yml/sanitizers (asan) + security-audit.yml/sanitizers
# -----------------------------------------------------------------------------
job_asan() {
if [ -z "$CLANG_CC" ]; then fail "asan: clang not found"; return; fi
banner "asan: ASan + UBSan ($CLANG_CC, Debug)"
local build_dir="${BUILD_BASE}-asan"
cmake -S "$SRC" -B "$build_dir" -G Ninja \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_C_COMPILER="$CLANG_CC" \
-DCMAKE_CXX_COMPILER="$CLANG_CXX" \
"-DCMAKE_C_FLAGS=-fsanitize=address,undefined -fno-sanitize-recover=all -fno-omit-frame-pointer" \
"-DCMAKE_CXX_FLAGS=-fsanitize=address,undefined -fno-sanitize-recover=all -fno-omit-frame-pointer" \
"-DCMAKE_EXE_LINKER_FLAGS=-fsanitize=address,undefined" \
-DSECP256K1_BUILD_TESTS=ON \
-DSECP256K1_BUILD_FUZZ_TESTS=ON \
-DSECP256K1_BUILD_PROTOCOL_TESTS=ON \
-DSECP256K1_USE_ASM=OFF
cmake --build "$build_dir" -j"$NPROC"
if ASAN_OPTIONS="detect_leaks=1:halt_on_error=1" \
UBSAN_OPTIONS="halt_on_error=1:print_stacktrace=1" \
ctest --test-dir "$build_dir" --output-on-failure -j"$NPROC" \
-E "^(ct_sidechannel|unified_audit|selftest)" --timeout 900; then
pass "asan: ASan + UBSan"
else
fail "asan: ASan + UBSan"
fi
}
# -----------------------------------------------------------------------------
# Job: tsan — TSan (Clang-17, Debug)
# Mirrors: ci.yml/sanitizers (tsan)
# -----------------------------------------------------------------------------
job_tsan() {
if [ -z "$CLANG_CC" ]; then fail "tsan: clang not found"; return; fi
banner "tsan: TSan ($CLANG_CC, Debug)"
local build_dir="${BUILD_BASE}-tsan"
cmake -S "$SRC" -B "$build_dir" -G Ninja \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_C_COMPILER="$CLANG_CC" \
-DCMAKE_CXX_COMPILER="$CLANG_CXX" \
"-DCMAKE_C_FLAGS=-fsanitize=thread -fno-omit-frame-pointer" \
"-DCMAKE_CXX_FLAGS=-fsanitize=thread -fno-omit-frame-pointer" \
"-DCMAKE_EXE_LINKER_FLAGS=-fsanitize=thread" \
-DSECP256K1_BUILD_TESTS=ON \
-DSECP256K1_BUILD_FUZZ_TESTS=ON \
-DSECP256K1_BUILD_PROTOCOL_TESTS=ON \
-DSECP256K1_USE_ASM=OFF
cmake --build "$build_dir" -j"$NPROC"
if ctest --test-dir "$build_dir" --output-on-failure -j"$NPROC" \
-E "^(ct_sidechannel|unified_audit|selftest)" --timeout 900; then
pass "tsan: TSan"
else
fail "tsan: TSan"
fi
}
# -----------------------------------------------------------------------------
# Job: valgrind — Valgrind Memcheck (GCC-13, Debug)
# Mirrors: security-audit.yml / valgrind
# -----------------------------------------------------------------------------
job_valgrind() {
if [ -z "$GCC_CXX" ]; then fail "valgrind: gcc not found"; return; fi
banner "valgrind: Valgrind Memcheck ($GCC_CXX, Debug)"
local build_dir="${BUILD_BASE}-valgrind"
cmake -S "$SRC" -B "$build_dir" -G Ninja \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_CXX_COMPILER="$GCC_CXX" \
-DSECP256K1_BUILD_TESTS=ON
cmake --build "$build_dir" -j"$NPROC"
# Build suppression flag only if file exists
local supp_flag=""
if [ -f "$SRC/valgrind.supp" ]; then
supp_flag="--suppressions=$SRC/valgrind.supp"
fi
ctest --test-dir "$build_dir" --output-on-failure -j"$NPROC" \
-E "^ct_sidechannel" \
--overwrite MemoryCheckCommand=/usr/bin/valgrind \
--overwrite "MemoryCheckCommandOptions=--leak-check=full --error-exitcode=1 --show-leak-kinds=definite,indirect,possible --errors-for-leak-kinds=definite,indirect,possible ${supp_flag}" \
-T MemCheck || true
local valgrind_fail=0
if grep -q 'ERROR SUMMARY: [1-9]' "$build_dir"/Testing/Temporary/MemoryChecker.*.log 2>/dev/null; then
echo -e "${RED}Valgrind found memory errors${NC}"
grep 'ERROR SUMMARY:' "$build_dir"/Testing/Temporary/MemoryChecker.*.log
valgrind_fail=1
fi
if grep -q 'definitely lost: [1-9]' "$build_dir"/Testing/Temporary/MemoryChecker.*.log 2>/dev/null; then
echo -e "${RED}Valgrind found definite memory leaks${NC}"
grep 'definitely lost:' "$build_dir"/Testing/Temporary/MemoryChecker.*.log
valgrind_fail=1
fi
if [ "$valgrind_fail" -eq 0 ]; then
pass "valgrind: Memcheck clean"
else
fail "valgrind: Memcheck found errors"
fi
}
# -----------------------------------------------------------------------------
# Job: dudect — dudect Timing Analysis (GCC-13, Release, 60s local / 300s CI)
# Mirrors: security-audit.yml / dudect
# -----------------------------------------------------------------------------
job_dudect() {
if [ -z "$GCC_CXX" ]; then fail "dudect: gcc not found"; return; fi
banner "dudect: Timing Analysis smoke test ($GCC_CXX, Release)"
local build_dir="${BUILD_BASE}-dudect"
cmake -S "$SRC" -B "$build_dir" -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_COMPILER="$GCC_CXX" \
-DSECP256K1_BUILD_TESTS=ON
cmake --build "$build_dir" --target test_ct_sidechannel_standalone -j"$NPROC"
# 60s local timeout (shorter than CI's 300s)
local exit_code=0
timeout 60 "$build_dir/cpu/test_ct_sidechannel_standalone" 2>&1 || exit_code=$?
if [ "$exit_code" -eq 124 ]; then
echo -e "${YELLOW}dudect timed out after 60s (expected — no significant leakage in window)${NC}"
pass "dudect: Timing Analysis (timeout — OK)"
elif [ "$exit_code" -ne 0 ]; then
echo -e "${YELLOW}dudect reported timing variance (common on VMs — verify on bare metal)${NC}"
pass "dudect: Timing Analysis (variance — acceptable)"
else
pass "dudect: Timing Analysis passed"
fi
}
# -----------------------------------------------------------------------------
# Job: coverage — LLVM source-based coverage → HTML + lcov
# Mirrors: ci.yml / coverage
# -----------------------------------------------------------------------------
job_coverage() {
if [ -z "$CLANG_CC" ]; then fail "coverage: clang not found"; return; fi
banner "coverage: LLVM coverage → HTML ($CLANG_CC, Debug)"
local build_dir="${BUILD_BASE}-coverage"
cmake -S "$SRC" -B "$build_dir" -G Ninja \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_C_COMPILER="$CLANG_CC" \
-DCMAKE_CXX_COMPILER="$CLANG_CXX" \
-DSECP256K1_BUILD_TESTS=ON \
-DSECP256K1_BUILD_BENCH=OFF \
-DSECP256K1_BUILD_FUZZ_TESTS=ON \
-DSECP256K1_BUILD_PROTOCOL_TESTS=ON \
-DSECP256K1_USE_ASM=OFF \
"-DCMAKE_C_FLAGS=-fprofile-instr-generate -fcoverage-mapping" \
"-DCMAKE_CXX_FLAGS=-fprofile-instr-generate -fcoverage-mapping" \
"-DCMAKE_EXE_LINKER_FLAGS=-fprofile-instr-generate"
cmake --build "$build_dir" -j"$NPROC"
LLVM_PROFILE_FILE="$build_dir/%p-%m.profraw" \
ctest --test-dir "$build_dir" --output-on-failure -j"$NPROC" \
-E "^ct_sidechannel" || true
# Merge profiles
find "$build_dir" -name '*.profraw' -print0 \
| xargs -0 "$LLVM_PROFDATA" merge -sparse -o "$build_dir/merged.profdata"
# Find all instrumented objects
local objects=""
for bin in $(find "$build_dir" -type f -executable); do
if "$LLVM_COV" show --instr-profile="$build_dir/merged.profdata" "$bin" >/dev/null 2>&1; then
objects="$objects -object=$bin"
fi
done
if [ -z "$objects" ]; then
fail "Code Coverage (no instrumented objects found)"
return
fi
# Generate lcov + HTML report
# shellcheck disable=SC2086
"$LLVM_COV" export \
--format=lcov \
--instr-profile="$build_dir/merged.profdata" \
$objects \
--ignore-filename-regex='(tests/|bench/|examples/|/usr/)' \
> "$build_dir/coverage.lcov"
# shellcheck disable=SC2086
"$LLVM_COV" show \
--format=html \
--instr-profile="$build_dir/merged.profdata" \
$objects \
--ignore-filename-regex='(tests/|bench/|examples/|/usr/)' \
--output-dir="$build_dir/html"
# Summary
echo ""
echo -e "${BOLD}Coverage summary:${NC}"
# shellcheck disable=SC2086
"$LLVM_COV" report \
--instr-profile="$build_dir/merged.profdata" \
$objects \
--ignore-filename-regex='(tests/|bench/|examples/|/usr/)' \
| tail -10
local html_index="$build_dir/html/index.html"
if [ -f "$html_index" ]; then
# Copy HTML to /src so it's accessible from Windows host
local out_dir="$SRC/local-ci-output/coverage-html"
rm -rf "$out_dir"
cp -r "$build_dir/html" "$out_dir"
echo -e "\n${GREEN}HTML report:${NC} local-ci-output/coverage-html/index.html"
pass "coverage: HTML report generated"
else
fail "coverage: HTML report not generated"
fi
}
# -----------------------------------------------------------------------------
# Job: clang-tidy — Static analysis (Clang-17)
# Mirrors: clang-tidy.yml
# -----------------------------------------------------------------------------
job_clang_tidy() {
if [ -z "$CLANG_CC" ]; then fail "clang-tidy: clang not found"; return; fi
banner "clang-tidy: Static analysis ($CLANG_CC)"
local build_dir="${BUILD_BASE}-tidy"
cmake -S "$SRC" -B "$build_dir" -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER="$CLANG_CC" \
-DCMAKE_CXX_COMPILER="$CLANG_CXX" \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DSECP256K1_BUILD_TESTS=ON \
-DSECP256K1_BUILD_BENCH=ON \
-DSECP256K1_BUILD_EXAMPLES=ON
cmake --build "$build_dir" -j"$NPROC"
# Only analyse C++ source files that CMake actually compiles
# Filter out .S assembly files (clang-tidy cannot process them)
local output_file="$build_dir/clang-tidy-output.txt"
jq -r '.[].file' "$build_dir/compile_commands.json" \
| grep -E '\.(cpp|cc|cxx)$' \
| sort -u \
| xargs -P "$NPROC" -n 4 \
"$CLANG_TIDY" -p "$build_dir" --warnings-as-errors='' --quiet 2>&1 \
| tee "$output_file"
local count=0
if grep -qE '^.+:[0-9]+:[0-9]+: (warning|error):' "$output_file" 2>/dev/null; then
count=$(grep -cE '^.+:[0-9]+:[0-9]+: (warning|error):' "$output_file")
fi
# Copy report to /src for host access
cp "$output_file" "$SRC/local-ci-output/clang-tidy-output.txt" 2>/dev/null || true
if [ "$count" -gt 0 ]; then
echo -e "${YELLOW}clang-tidy found $count diagnostic(s) — see local-ci-output/clang-tidy-output.txt${NC}"
# Non-blocking (matches GitHub CI behaviour: warning only)
pass "clang-tidy ($count diagnostics — non-blocking)"
else
pass "clang-tidy: clean"
fi
}
# -----------------------------------------------------------------------------
# Job: ci — CI matrix (GCC-13 + Clang-17, Release + Debug)
# Mirrors: ci.yml / linux
# -----------------------------------------------------------------------------
job_ci() {
local all_pass=1
local compilers=()
if [ -n "$GCC_CC" ]; then compilers+=(gcc); fi
if [ -n "$CLANG_CC" ]; then compilers+=(clang); fi
if [ ${#compilers[@]} -eq 0 ]; then fail "ci: no compilers found"; return; fi
for compiler in "${compilers[@]}"; do
for build_type in Release Debug; do
local cc cxx label
if [ "$compiler" = "gcc" ]; then
cc="$GCC_CC"; cxx="$GCC_CXX"; label="$GCC_CC"
else
cc="$CLANG_CC"; cxx="$CLANG_CXX"; label="$CLANG_CC"
fi
banner "ci: $label / $build_type"
local build_dir="${BUILD_BASE}-ci-${label}-${build_type}"
cmake -S "$SRC" -B "$build_dir" -G Ninja \
-DCMAKE_BUILD_TYPE="$build_type" \
-DCMAKE_C_COMPILER="$cc" \
-DCMAKE_CXX_COMPILER="$cxx" \
-DSECP256K1_BUILD_TESTS=ON \
-DSECP256K1_BUILD_BENCH=ON \
-DSECP256K1_BUILD_EXAMPLES=ON \
-DSECP256K1_BUILD_FUZZ_TESTS=ON \
-DSECP256K1_BUILD_PROTOCOL_TESTS=ON
cmake --build "$build_dir" -j"$NPROC"
if ctest --test-dir "$build_dir" --output-on-failure -j"$NPROC" \
-E "^ct_sidechannel"; then
echo -e "${GREEN}OK${NC} $compiler / $build_type"
else
echo -e "${RED}FAIL${NC} $compiler / $build_type"
all_pass=0
fi
done
done
if [ "$all_pass" -eq 1 ]; then
pass "ci: Matrix (4 configs)"
else
fail "ci: Matrix (4 configs) — some failed"
fi
}
# -----------------------------------------------------------------------------
# Job: audit — unified_audit_runner (641,194 checks)
# Mirrors: audit-report.yml / linux-gcc
# -----------------------------------------------------------------------------
job_audit() {
if [ -z "$GCC_CXX" ]; then fail "audit: gcc not found"; return; fi
banner "audit: unified_audit_runner — 641,194 checks ($GCC_CXX, Release)"
local build_dir="${BUILD_BASE}-audit"
cmake -S "$SRC" -B "$build_dir" -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_COMPILER="$GCC_CXX" \
-DBUILD_TESTING=ON \
-DSECP256K1_BUILD_PROTOCOL_TESTS=ON \
-DSECP256K1_BUILD_FUZZ_TESTS=ON
cmake --build "$build_dir" -j"$NPROC"
local out_dir="$SRC/local-ci-output/audit"
mkdir -p "$out_dir"
"$build_dir/audit/unified_audit_runner" --report-dir "$out_dir" || true
if [ -f "$out_dir/audit_report.json" ]; then
echo ""
tail -20 "$out_dir/audit_report.txt" 2>/dev/null || true
local verdict
verdict=$(grep -o '"audit_verdict": *"[^"]*"' "$out_dir/audit_report.json" \
| head -1 | cut -d'"' -f4)
echo -e "\n${BOLD}Verdict: $verdict${NC}"
if [ "$verdict" = "AUDIT-READY" ] || [ "$verdict" = "PASS" ]; then
pass "audit: $verdict — report in local-ci-output/audit/"
else
fail "audit: $verdict — see local-ci-output/audit/audit_report.txt"
fi
else
fail "audit: report not generated"
fi
}
# -----------------------------------------------------------------------------
# Job: cppcheck — Cppcheck static analysis
# Mirrors: cppcheck.yml
# -----------------------------------------------------------------------------
job_cppcheck() {
if ! command -v cppcheck &>/dev/null; then
echo -e "${YELLOW}cppcheck not installed — rebuild image: docker build -f Dockerfile.local-ci -t uf-local-ci .${NC}"
pass "cppcheck: skipped (not installed)"
return
fi
banner "cppcheck: Static analysis"
local build_dir="${BUILD_BASE}-cppcheck"
local out_dir="$SRC/local-ci-output"
mkdir -p "$out_dir"
cmake -S "$SRC" -B "$build_dir" -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DSECP256K1_BUILD_TESTS=ON \
-DSECP256K1_BUILD_BENCH=ON
local supp_flag=""
if [ -f "$SRC/.cppcheck-suppressions" ]; then
supp_flag="--suppressions-list=$SRC/.cppcheck-suppressions"
fi
cppcheck \
--project="$build_dir/compile_commands.json" \
--enable=warning,performance,portability \
--suppress=missingIncludeSystem \
--suppress=unmatchedSuppression \
--suppress=unusedFunction \
${supp_flag} \
--inline-suppr \
--error-exitcode=0 \
--std=c++20 \
--xml \
2> "$out_dir/cppcheck-results.xml" || true
local errors
errors=$(grep -c '<error ' "$out_dir/cppcheck-results.xml" 2>/dev/null || echo 0)
echo -e "cppcheck: ${BOLD}$errors finding(s)${NC} — see local-ci-output/cppcheck-results.xml"
pass "cppcheck: $errors finding(s) — non-blocking"
}
# -----------------------------------------------------------------------------
# Job: bench — Quick benchmark run (output only, no regression comparison)
# Mirrors: bench-regression.yml / benchmark.yml (local: no baseline comparison)
# Note: Docker adds noise; use GH CI for regression detection against baseline.
# -----------------------------------------------------------------------------
job_bench() {
if [ -z "$GCC_CXX" ]; then fail "bench: gcc not found"; return; fi
banner "bench: Performance snapshot ($GCC_CXX, Release)"
local build_dir="${BUILD_BASE}-bench"
cmake -S "$SRC" -B "$build_dir" -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_COMPILER="$GCC_CXX" \
-DBUILD_TESTING=ON \
-DSECP256K1_USE_ASM=ON
cmake --build "$build_dir" --target bench_unified bench_atomic_operations -j"$NPROC"
local out_dir="$SRC/local-ci-output"
mkdir -p "$out_dir"
echo -e "\n${BOLD}=== bench_unified ===${NC}"
"$build_dir/cpu/bench_unified" --quick 2>&1 | tee "$out_dir/bench_unified.txt"
echo -e "\n${BOLD}=== bench_atomic_operations ===${NC}"
"$build_dir/cpu/bench_atomic_operations" 2>&1 | tee "$out_dir/bench_atomic_operations.txt"
echo -e "\n${YELLOW}NOTE: Docker/VM benchmarks are noisy (shared CPU, no frequency pinning).${NC}"
echo -e "${YELLOW} Use GitHub CI bench-regression.yml for authoritative regression detection.${NC}"
echo -e "${GREEN}Results saved to: local-ci-output/bench_unified.txt${NC}"
pass "bench: Snapshot complete (see local-ci-output/)"
}
# -----------------------------------------------------------------------------
# Job: qemu-smoke - ARM64/RISC-V cross-build + QEMU smoke
# Mirrors: ci.yml / linux-arm64 + linux-riscv64
# Optional env: SECP256K1_QEMU_SMOKE_TARGET=arm64|riscv64|all (default: all)
# -----------------------------------------------------------------------------
job_qemu_smoke() {
banner "qemu-smoke: ARM64/RISC-V cross-build + QEMU smoke"
local script="$SRC/scripts/run-qemu-smoke.sh"
local target="${SECP256K1_QEMU_SMOKE_TARGET:-all}"
local out_dir="$SRC/local-ci-output"
mkdir -p "$out_dir"
if [ ! -f "$script" ]; then
fail "qemu-smoke: scripts/run-qemu-smoke.sh not found"
return
fi
echo -e "${BOLD}Target:${NC} $target"
echo -e "${BOLD}Hint:${NC} export SECP256K1_QEMU_SMOKE_TARGET=arm64|riscv64|all to override"
if bash "$script" "$target" 2>&1 | tee "$out_dir/qemu-smoke.txt"; then
pass "qemu-smoke: $target"
else
fail "qemu-smoke: $target"
fi
}
# -----------------------------------------------------------------------------
# Job: trust - Trust signals summary (local)
# Mirrors: codecov + SonarCloud + scorecard (local = evidence + links)
# -----------------------------------------------------------------------------
job_trust() {
banner "trust: Public Trust Signals (local summary)"
job_coverage
local out_dir="$SRC/local-ci-output"
mkdir -p "$out_dir"
cat > "$out_dir/trust_summary.txt" <<'EOF'
Public Trust Signals (local):
- Codecov: ensure coverage artifacts exist (local-ci-output/coverage/)
- SonarCloud: run remote analysis in CI (requires token)
- OSSF Scorecard: GitHub-hosted scan (public)
EOF
echo -e "${GREEN}Trust summary saved to: local-ci-output/trust_summary.txt${NC}"
echo -e "${YELLOW}NOTE:${NC} SonarCloud/Scorecard run in GitHub Actions (not local)."
pass "trust: Summary complete"
}
# -----------------------------------------------------------------------------
# Job: valgrind-ct — Valgrind CT taint analysis
# Mirrors: valgrind-ct.yml
# -----------------------------------------------------------------------------
job_valgrind_ct() {
local script="$SRC/scripts/valgrind_ct_check.sh"
if [ ! -f "$script" ]; then
echo -e "${YELLOW}scripts/valgrind_ct_check.sh not found — skipping${NC}"
pass "valgrind-ct: skipped (script not found)"
return
fi
banner "valgrind-ct: CT taint analysis"
chmod +x "$script"
local build_dir="${BUILD_BASE}-valgrind-ct"
if "$script" "$build_dir"; then
local report="$build_dir/valgrind_reports/valgrind_ct_report.json"
if [ -f "$report" ]; then
mkdir -p "$SRC/local-ci-output/valgrind-ct"
cp -r "$build_dir/valgrind_reports/." "$SRC/local-ci-output/valgrind-ct/"
echo ""
cat "$report"
fi
pass "valgrind-ct: CT taint clean"
else
fail "valgrind-ct: CT taint violations detected"
fi
}
# -----------------------------------------------------------------------------
# Summary
# -----------------------------------------------------------------------------
print_summary() {
echo ""
echo -e "${BOLD}===============================================================${NC}"
echo -e "${BOLD} LOCAL CI SUMMARY${NC}"
echo -e "${BOLD}===============================================================${NC}"
for r in "${RESULTS[@]}"; do
echo -e " $r"
done
echo -e "${BOLD}===============================================================${NC}"
if [ "$FAILED" -eq 0 ]; then
echo -e " ${GREEN}${BOLD}ALL PASSED${NC}"
else
echo -e " ${RED}${BOLD}SOME JOBS FAILED${NC}"
fi
echo ""
}
# -----------------------------------------------------------------------------
# Main
# -----------------------------------------------------------------------------
main() {
local jobs=()
# -- Presets -----------------------------------------------------------
# --quick Fast pre-commit gate (~5 min): catches build breakage + test regressions
local PRESET_QUICK=(werror ci)
# --all Standard check (~20-25 min): everything except slow analysis jobs
local PRESET_ALL=(werror ci asan tsan audit clang-tidy cppcheck)
# --full Release-quality check (~45-60 min): mirrors entire GitHub CI suite
local PRESET_FULL=(werror ci asan tsan audit clang-tidy cppcheck coverage valgrind dudect valgrind-ct)
while [[ $# -gt 0 ]]; do
case "$1" in
--quick) jobs=("${PRESET_QUICK[@]}"); shift ;;
--all) jobs=("${PRESET_ALL[@]}"); shift ;;
--full) jobs=("${PRESET_FULL[@]}"); shift ;;
--job) IFS=',' read -ra _j <<< "$2"; jobs+=("${_j[@]}"); shift 2 ;;
--list)
echo "Available jobs:"
echo " werror Build -Werror -Wall -Wextra (security-audit.yml)"
echo " ci GCC+Clang Release+Debug (ci.yml)"
echo " asan ASan + UBSan, Clang (ci.yml + security-audit.yml)"
echo " tsan TSan, Clang (ci.yml)"
echo " audit unified_audit_runner 641K chk (audit-report.yml)"
echo " clang-tidy clang-tidy static analysis (clang-tidy.yml)"
echo " cppcheck Cppcheck static analysis (cppcheck.yml)"
echo " coverage LLVM coverage → HTML (ci.yml/coverage)"
echo " valgrind Valgrind memcheck (security-audit.yml)"
echo " dudect dudect 60s timing smoke test (security-audit.yml)"
echo " valgrind-ct Valgrind CT taint analysis (valgrind-ct.yml)"
echo " bench Benchmark snapshot (output only) (benchmark.yml)"
echo " qemu-smoke ARM64/RISC-V cross-build + QEMU smoke (ci.yml)"
echo " trust Trust signals summary (coverage + links)"
echo ""
echo "Presets:"
echo " --quick ${PRESET_QUICK[*]} (~5 min)"
echo " --all ${PRESET_ALL[*]} (~20-25 min)"
echo " --full ${PRESET_FULL[*]} (~45-60 min)"
exit 0 ;;
--help|-h)
sed -n '2,27p' "$0" | sed 's/^# \?//'
exit 0 ;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done
# Default = --all
if [ ${#jobs[@]} -eq 0 ]; then
jobs=("${PRESET_ALL[@]}")
fi
# -- Setup output dir --------------------------------------------------
mkdir -p "$SRC/local-ci-output"
echo ""
echo -e "${CYAN}+==============================================================+${NC}"
echo -e "${CYAN}|${NC} ${BOLD}UltrafastSecp256k1 — Local CI Runner${NC}"
echo -e "${CYAN}|${NC} Mirrors GitHub Actions ubuntu-24.04 environment"
echo -e "${CYAN}|${NC} Jobs: ${BOLD}${jobs[*]}${NC}"
echo -e "${CYAN}|${NC} CPUs: ${BOLD}${NPROC}${NC} Output: local-ci-output/"
echo -e "${CYAN}+==============================================================+${NC}"
echo ""
for job in "${jobs[@]}"; do
case "$job" in
werror) job_werror ;;
asan) job_asan ;;
tsan) job_tsan ;;
valgrind) job_valgrind ;;
dudect) job_dudect ;;
coverage) job_coverage ;;
clang-tidy) job_clang_tidy ;;
cppcheck) job_cppcheck ;;
ci) job_ci ;;
audit) job_audit ;;
valgrind-ct) job_valgrind_ct ;;
bench) job_bench ;;
qemu-smoke) job_qemu_smoke ;;
trust) job_trust ;;
*) echo -e "${RED}Unknown job: $job${NC}"; exit 1 ;;
esac
done
# -- ccache summary ----------------------------------------------------
if [ "$CCACHE_ENABLED" -eq 1 ]; then
echo ""
echo -e "${BOLD}ccache hit rate:${NC}"
ccache -s 2>/dev/null | grep -E 'hit|miss|size' || true
fi
print_summary
exit "$FAILED"
}
main "$@"