Catch Windows/Linux/macOS-only defect classes before push: raw std::jthread, sqlite3_open* via bare path.c_str(), and raw getenv reads outside the config boundary. Wired into pre-commit and the unit/policy suite; 575 reviewed sites allowlisted.
607 lines
22 KiB
Bash
Executable file
607 lines
22 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
#
|
|
# Unified YAMS lint dispatcher.
|
|
# See: scripts/lint.sh --help
|
|
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
cd "$REPO_ROOT"
|
|
|
|
# Color helpers
|
|
if [[ -t 2 ]]; then
|
|
C_RED='\033[0;31m' C_GREEN='\033[0;32m' C_YELLOW='\033[1;33m'
|
|
C_BLUE='\033[0;34m' C_CYAN='\033[0;36m' C_DIM='\033[2m' C_OFF='\033[0m'
|
|
else
|
|
C_RED='' C_GREEN='' C_YELLOW='' C_BLUE='' C_CYAN='' C_DIM='' C_OFF=''
|
|
fi
|
|
say() { printf '%b%s%b\n' "$C_BLUE" "→ $*" "$C_OFF" >&2; }
|
|
ok() { printf '%b%s%b\n' "$C_GREEN" "✓ $*" "$C_OFF" >&2; }
|
|
warn() { printf '%b%s%b\n' "$C_YELLOW" "⚠ $*" "$C_OFF" >&2; }
|
|
err() { printf '%b%s%b\n' "$C_RED" "✗ $*" "$C_OFF" >&2; }
|
|
section(){ printf '\n%b== %s ==%b\n' "$C_CYAN" "$*" "$C_OFF" >&2; }
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: scripts/lint.sh [check ...] [options]
|
|
|
|
Unified YAMS lint dispatcher. Subsumes formatter, static analyzers,
|
|
metadata checks, and dependency audit.
|
|
|
|
CHECKS (positional; default = format cppcheck opengrep when none given)
|
|
format clang-format (delegates scripts/format-code.sh)
|
|
cppcheck cppcheck (delegates scripts/check-quality.sh)
|
|
tidy clang-tidy
|
|
opengrep opengrep audit profile (delegates scripts/dev/run_opengrep.sh)
|
|
windows-hdr Windows.h include order + WIN32_LEAN_AND_MEAN/NOMINMAX
|
|
license GPL-3.0-or-later header enforcement
|
|
commitlint commit-message lint (npx commitlint, requires npm)
|
|
dco DCO Signed-off-by check
|
|
actionlint GitHub workflow YAML lint
|
|
deps-conan Conan dep version drift
|
|
deps-cve Conan dep CVE scan (osv-scanner)
|
|
deps-subs git submodule freshness
|
|
deps-licenses Conan + submodule license allowlist
|
|
deps shorthand: deps-conan deps-cve deps-subs deps-licenses
|
|
all every check above
|
|
|
|
MODE
|
|
--check read-only (default for all but format)
|
|
--fix auto-fix where supported (format)
|
|
|
|
SCOPE
|
|
--git only files staged in git index
|
|
--diff REF only files changed vs REF (e.g. origin/main)
|
|
--paths PATH... explicit path list (consumes the rest of the line)
|
|
--all-files full repo (default)
|
|
|
|
BEHAVIOR
|
|
--baseline REF for opengrep + cppcheck: report only findings new vs REF
|
|
--strict exit nonzero on any finding (incl. warn-only)
|
|
--hook shorthand for the pre-commit set
|
|
--ci shorthand for the CI quality set
|
|
--help this message
|
|
|
|
EXAMPLES
|
|
scripts/lint.sh # default: format check + opengrep + cppcheck
|
|
scripts/lint.sh --hook # what .githooks/pre-commit calls
|
|
scripts/lint.sh --ci # what CI runs
|
|
scripts/lint.sh deps # full deps audit
|
|
scripts/lint.sh opengrep --baseline HEAD~1 # delta-only opengrep run
|
|
scripts/lint.sh format --fix --git # fix formatting on staged files
|
|
EOF
|
|
}
|
|
|
|
# ----- Argument parser ---------------------------------------------------
|
|
|
|
CHECKS=()
|
|
SCOPE="all-files"
|
|
DIFF_REF=""
|
|
PATHS=()
|
|
MODE_FIX=0
|
|
STRICT=0
|
|
BASELINE=""
|
|
HOOK=0
|
|
CI=0
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-h|--help) usage; exit 0 ;;
|
|
--check) MODE_FIX=0; shift ;;
|
|
--fix) MODE_FIX=1; shift ;;
|
|
--git) SCOPE="git"; shift ;;
|
|
--diff) SCOPE="diff"; DIFF_REF="${2:?--diff requires REF}"; shift 2 ;;
|
|
--paths) SCOPE="paths"; shift; while [[ $# -gt 0 && "$1" != --* ]]; do PATHS+=("$1"); shift; done ;;
|
|
--all-files) SCOPE="all-files"; shift ;;
|
|
--baseline) BASELINE="${2:?--baseline requires REF}"; shift 2 ;;
|
|
--strict) STRICT=1; shift ;;
|
|
--hook) HOOK=1; shift ;;
|
|
--ci) CI=1; shift ;;
|
|
--) shift; while [[ $# -gt 0 ]]; do PATHS+=("$1"); shift; done; SCOPE="paths" ;;
|
|
--*) err "unknown flag: $1"; usage; exit 2 ;;
|
|
*) CHECKS+=("$1"); shift ;;
|
|
esac
|
|
done
|
|
|
|
# Default check sets
|
|
if (( HOOK )); then
|
|
SCOPE="git"; STRICT=1; MODE_FIX=0
|
|
CHECKS=(format windows-hdr license portability opengrep)
|
|
elif (( CI )); then
|
|
SCOPE="all-files"; STRICT=1
|
|
CHECKS=(opengrep cppcheck tidy actionlint dco)
|
|
elif [[ ${#CHECKS[@]} -eq 0 ]]; then
|
|
CHECKS=(format cppcheck opengrep)
|
|
fi
|
|
|
|
# Expand "all" / "deps" shorthands
|
|
NEW=()
|
|
for c in "${CHECKS[@]}"; do
|
|
case "$c" in
|
|
all) NEW+=(format cppcheck tidy opengrep windows-hdr license commitlint dco actionlint deps-conan deps-cve deps-subs deps-licenses) ;;
|
|
deps) NEW+=(deps-conan deps-cve deps-subs deps-licenses) ;;
|
|
*) NEW+=("$c") ;;
|
|
esac
|
|
done
|
|
CHECKS=("${NEW[@]}")
|
|
|
|
# ----- Path resolver -----------------------------------------------------
|
|
|
|
# Emits NUL-delimited file list to stdout, filtered by extension regex in $1.
|
|
# Caller iterates with: while IFS= read -r -d '' f; do ...; done < <(resolve_paths '...')
|
|
resolve_paths() {
|
|
local ext_re="${1:-.*}"
|
|
case "$SCOPE" in
|
|
git)
|
|
git diff --cached --name-only --diff-filter=ACM -z 2>/dev/null \
|
|
| while IFS= read -r -d '' f; do
|
|
[[ -f "$f" && "$f" =~ $ext_re ]] && printf '%s\0' "$f"
|
|
done
|
|
;;
|
|
diff)
|
|
git diff --name-only --diff-filter=ACMR "$DIFF_REF" \
|
|
| while IFS= read -r f; do
|
|
[[ -f "$f" && "$f" =~ $ext_re ]] && printf '%s\0' "$f"
|
|
done
|
|
;;
|
|
paths)
|
|
for p in "${PATHS[@]}"; do
|
|
if [[ -d "$p" ]]; then
|
|
find "$p" -type f -print0 | while IFS= read -r -d '' f; do
|
|
[[ "$f" =~ $ext_re ]] && printf '%s\0' "$f"
|
|
done
|
|
elif [[ -f "$p" && "$p" =~ $ext_re ]]; then
|
|
printf '%s\0' "$p"
|
|
fi
|
|
done
|
|
;;
|
|
all-files)
|
|
git ls-files -z 2>/dev/null \
|
|
| while IFS= read -r -d '' f; do
|
|
[[ -f "$f" && "$f" =~ $ext_re ]] && printf '%s\0' "$f"
|
|
done
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# ----- Helpers: delegating wrappers --------------------------------------
|
|
|
|
CPP_RE='\.(c|cc|cpp|cxx|h|hh|hpp)$'
|
|
|
|
lint_format() {
|
|
section "format (clang-format)"
|
|
local files=()
|
|
while IFS= read -r -d '' f; do files+=("$f"); done < <(resolve_paths "$CPP_RE")
|
|
if [[ ${#files[@]} -eq 0 ]]; then ok "format: no files in scope"; return 0; fi
|
|
# Windows path: invoke format-code.ps1 if available; otherwise fall back to .sh.
|
|
local fmt_sh="$REPO_ROOT/scripts/format-code.sh"
|
|
local fmt_ps1="$REPO_ROOT/scripts/format-code.ps1"
|
|
local on_windows=0
|
|
case "${OSTYPE:-}" in msys*|cygwin*|win32*|mingw*) on_windows=1 ;; esac
|
|
run_fmt_check() {
|
|
if (( on_windows )) && command -v pwsh >/dev/null 2>&1; then
|
|
printf '%s\n' "${files[@]}" | pwsh -NoProfile -ExecutionPolicy Bypass -File "$fmt_ps1" -Check -Serial -StdinFiles
|
|
elif (( on_windows )) && command -v powershell >/dev/null 2>&1; then
|
|
printf '%s\n' "${files[@]}" | powershell -NoProfile -ExecutionPolicy Bypass -File "$fmt_ps1" -Check -Serial -StdinFiles
|
|
else
|
|
printf '%s\n' "${files[@]}" | "$fmt_sh" --check --serial --stdin-files
|
|
fi
|
|
}
|
|
run_fmt_fix() {
|
|
if (( on_windows )) && command -v pwsh >/dev/null 2>&1; then
|
|
printf '%s\n' "${files[@]}" | pwsh -NoProfile -ExecutionPolicy Bypass -File "$fmt_ps1" -Serial -StdinFiles
|
|
elif (( on_windows )) && command -v powershell >/dev/null 2>&1; then
|
|
printf '%s\n' "${files[@]}" | powershell -NoProfile -ExecutionPolicy Bypass -File "$fmt_ps1" -Serial -StdinFiles
|
|
else
|
|
printf '%s\n' "${files[@]}" | "$fmt_sh" --serial --stdin-files
|
|
fi
|
|
}
|
|
if (( MODE_FIX )); then
|
|
run_fmt_fix
|
|
# When operating on staged files, restage post-format.
|
|
if [[ "$SCOPE" == "git" ]]; then
|
|
git add -- "${files[@]}"
|
|
fi
|
|
fi
|
|
if run_fmt_check; then
|
|
ok "format: clean"; return 0
|
|
else
|
|
err "format: drift detected (run 'scripts/lint.sh format --fix --git' to repair)"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
lint_cppcheck() {
|
|
section "cppcheck"
|
|
local args=(--profile ci --format human --parallel)
|
|
case "$SCOPE" in
|
|
diff) args+=(--diff-base "$DIFF_REF") ;;
|
|
git) args+=(--git) ;;
|
|
esac
|
|
[[ -n "$BASELINE" ]] && args+=(--baseline "$BASELINE")
|
|
if "$REPO_ROOT/scripts/check-quality.sh" "${args[@]}"; then
|
|
ok "cppcheck: clean"; return 0
|
|
else
|
|
local rc=$?
|
|
err "cppcheck: $rc finding(s)"
|
|
return $rc
|
|
fi
|
|
}
|
|
|
|
lint_opengrep() {
|
|
local profile="audit"
|
|
if (( HOOK )); then
|
|
profile="default"
|
|
fi
|
|
section "opengrep ($profile profile)"
|
|
local env_pfx=()
|
|
env_pfx+=("OPENGREP_PROFILE=$profile")
|
|
env_pfx+=("OPENGREP_OUT_DIR=.artifacts/opengrep")
|
|
(( STRICT )) && env_pfx+=("OPENGREP_STRICT=1")
|
|
[[ -n "$BASELINE" ]] && env_pfx+=("OPENGREP_BASELINE_COMMIT=$BASELINE")
|
|
local target=()
|
|
case "$SCOPE" in
|
|
paths) target=("${PATHS[@]}") ;;
|
|
git) target=(); while IFS= read -r -d '' f; do target+=("$f"); done < <(resolve_paths "$CPP_RE") ;;
|
|
diff) target=(); while IFS= read -r -d '' f; do target+=("$f"); done < <(resolve_paths "$CPP_RE") ;;
|
|
all-files) target=(src include tests) ;;
|
|
esac
|
|
if [[ "$SCOPE" =~ ^(git|diff)$ && ${#target[@]} -eq 0 ]]; then
|
|
ok "opengrep: no files in scope"; return 0
|
|
fi
|
|
local json_out=".artifacts/opengrep/opengrep-yams-${profile//\//-}.json"
|
|
if env "${env_pfx[@]}" "$REPO_ROOT/scripts/dev/run_opengrep.sh" "${target[@]}"; then
|
|
local findings=0
|
|
if [[ -f "$json_out" ]]; then
|
|
findings=$(python3 - "$json_out" <<'PYCOUNT'
|
|
import json, sys
|
|
try:
|
|
with open(sys.argv[1], 'r', encoding='utf-8') as fh:
|
|
print(len(json.load(fh).get('results', [])))
|
|
except Exception:
|
|
print(0)
|
|
PYCOUNT
|
|
)
|
|
fi
|
|
if (( findings > 0 )); then
|
|
if (( STRICT )); then
|
|
err "opengrep: ${findings} finding(s) reported (see $json_out)"
|
|
return 1
|
|
fi
|
|
warn "opengrep: ${findings} finding(s) reported (non-strict; see $json_out)"
|
|
return 0
|
|
fi
|
|
ok "opengrep: clean"; return 0
|
|
else
|
|
err "opengrep: findings reported"; return 1
|
|
fi
|
|
}
|
|
|
|
lint_tidy() {
|
|
section "clang-tidy"
|
|
if ! command -v clang-tidy >/dev/null 2>&1; then
|
|
warn "clang-tidy not installed; skipping"; return 0
|
|
fi
|
|
local files=() rc=0
|
|
while IFS= read -r -d '' f; do files+=("$f"); done < <(resolve_paths "$CPP_RE")
|
|
if [[ ${#files[@]} -eq 0 ]]; then ok "tidy: no files in scope"; return 0; fi
|
|
for f in "${files[@]:-}"; do
|
|
[[ -z "$f" ]] && continue
|
|
clang-tidy "$f" --quiet 2>/dev/null || rc=1
|
|
done
|
|
if (( rc == 0 )); then ok "tidy: clean"; else err "tidy: findings reported"; fi
|
|
return $rc
|
|
}
|
|
|
|
# ----- Helpers: factored out from .githooks/pre-commit ------------------
|
|
|
|
lint_windows_hdr() {
|
|
section "windows-hdr (Windows.h include order + WIN32_LEAN_AND_MEAN/NOMINMAX)"
|
|
local files=() errors=()
|
|
while IFS= read -r -d '' f; do files+=("$f"); done < <(resolve_paths "$CPP_RE")
|
|
if [[ ${#files[@]} -eq 0 ]]; then ok "windows-hdr: no files in scope"; return 0; fi
|
|
for f in "${files[@]}"; do
|
|
[[ -f "$f" ]] || continue
|
|
if grep -qE '#include\s*<[^>]*[Pp]sapi\.h[^>]*>' "$f"; then
|
|
local psapi_line win_line lean_mean_line nominmax_line
|
|
psapi_line=$(grep -nE '#include\s*<[^>]*[Pp]sapi\.h[^>]*>' "$f" | head -1 | cut -d: -f1)
|
|
win_line=$(grep -nE '#include\s*<[^>]*[Ww]indows\.h[^>]*>' "$f" | head -1 | cut -d: -f1 || echo "")
|
|
if [[ -z "$win_line" ]]; then
|
|
errors+=("$f: Missing #include <Windows.h> before Psapi.h (Psapi.h depends on Windows types)")
|
|
elif (( psapi_line < win_line )); then
|
|
errors+=("$f: Psapi.h included at line $psapi_line, but Windows.h must come first (line $win_line)")
|
|
fi
|
|
lean_mean_line=$(grep -nE '#define\s+WIN32_LEAN_AND_MEAN' "$f" | head -1 | cut -d: -f1 || echo "")
|
|
if [[ -z "$lean_mean_line" ]]; then
|
|
errors+=("$f: Missing #define WIN32_LEAN_AND_MEAN before Windows headers")
|
|
elif [[ -n "$win_line" && "$lean_mean_line" -gt "$win_line" ]]; then
|
|
errors+=("$f: WIN32_LEAN_AND_MEAN must be defined before Windows.h (currently at line $lean_mean_line, Windows.h at $win_line)")
|
|
fi
|
|
nominmax_line=$(grep -nE '#define\s+NOMINMAX' "$f" | head -1 | cut -d: -f1 || echo "")
|
|
if [[ -z "$nominmax_line" ]]; then
|
|
errors+=("$f: Missing #define NOMINMAX before Windows headers")
|
|
elif [[ -n "$win_line" && "$nominmax_line" -gt "$win_line" ]]; then
|
|
errors+=("$f: NOMINMAX must be defined before Windows.h (currently at line $nominmax_line, Windows.h at $win_line)")
|
|
fi
|
|
fi
|
|
done
|
|
if (( ${#errors[@]} > 0 )); then
|
|
err "windows-hdr: ${#errors[@]} violation(s)"
|
|
for e in "${errors[@]}"; do printf ' - %s\n' "$e" >&2; done
|
|
cat >&2 <<'EOF'
|
|
Fix: Include Windows.h BEFORE Psapi.h, and define WIN32_LEAN_AND_MEAN and NOMINMAX first.
|
|
Example:
|
|
#ifdef _WIN32
|
|
#ifndef WIN32_LEAN_AND_MEAN
|
|
#define WIN32_LEAN_AND_MEAN 1
|
|
#endif
|
|
#ifndef NOMINMAX
|
|
#define NOMINMAX 1
|
|
#endif
|
|
#include <Windows.h>
|
|
#include <Psapi.h>
|
|
#endif
|
|
EOF
|
|
return 1
|
|
fi
|
|
ok "windows-hdr: clean"
|
|
}
|
|
|
|
lint_license() {
|
|
section "license (GPL-3.0-or-later only)"
|
|
local files=() errors=()
|
|
while IFS= read -r -d '' f; do files+=("$f"); done < <(resolve_paths "$CPP_RE")
|
|
if [[ ${#files[@]} -eq 0 ]]; then ok "license: no files in scope"; return 0; fi
|
|
for f in "${files[@]}"; do
|
|
[[ -f "$f" ]] || continue
|
|
[[ "$f" == *"third_party/"* || "$f" == *"subprojects/"* || "$f" == *"external/"* ]] && continue
|
|
if grep -qE "SPDX-License-Identifier:\s*Apache-2\.0" "$f"; then
|
|
errors+=("$f: SPDX-License-Identifier: Apache-2.0 (must be GPL-3.0-or-later)")
|
|
fi
|
|
if grep -qE "SPDX-License-Identifier:\s*MIT" "$f"; then
|
|
errors+=("$f: SPDX-License-Identifier: MIT (must be GPL-3.0-or-later)")
|
|
fi
|
|
if grep -qE "SPDX-License-Identifier:\s*BSD" "$f"; then
|
|
errors+=("$f: SPDX-License-Identifier: BSD-XX (must be GPL-3.0-or-later)")
|
|
fi
|
|
if grep -qE "Licensed under the Apache License, Version 2\.0" "$f"; then
|
|
errors+=("$f: Apache License text body (must be GPL-3.0-or-later)")
|
|
fi
|
|
if grep -qE "Permission is hereby granted, free of charge" "$f"; then
|
|
errors+=("$f: MIT License text body (must be GPL-3.0-or-later)")
|
|
fi
|
|
done
|
|
if (( ${#errors[@]} > 0 )); then
|
|
err "license: ${#errors[@]} non-GPL header(s)"
|
|
for e in "${errors[@]}"; do printf ' - %s\n' "$e" >&2; done
|
|
cat >&2 <<'EOF'
|
|
Fix: Update header to GPL-3.0-or-later:
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
Or run: python3 scripts/update-licenses.py
|
|
Note: third_party/, subprojects/, external/ are excluded from this check.
|
|
EOF
|
|
return 1
|
|
fi
|
|
ok "license: clean"
|
|
}
|
|
|
|
lint_portability() {
|
|
section "portability (std::jthread / path.c_str / raw getenv)"
|
|
local script="$REPO_ROOT/tests/scripts/check_portability.py"
|
|
local allowlist="$REPO_ROOT/tests/scripts/portability_allowlist.txt"
|
|
if python3 "$script" --root "$REPO_ROOT" --allowlist "$allowlist"; then
|
|
ok "portability: clean"; return 0
|
|
else
|
|
err "portability: unreviewed cross-platform occurrence(s)"; return 1
|
|
fi
|
|
}
|
|
|
|
# ----- Helpers: metadata checks -----------------------------------------
|
|
|
|
lint_commitlint() {
|
|
section "commitlint"
|
|
if ! command -v npx >/dev/null 2>&1; then
|
|
warn "npx not installed; skipping"; return 0
|
|
fi
|
|
local from="${COMMITLINT_FROM:-${GITHUB_BASE_SHA:-}}"
|
|
local to="${COMMITLINT_TO:-${GITHUB_HEAD_SHA:-HEAD}}"
|
|
if [[ -z "$from" ]]; then
|
|
from=$(git merge-base HEAD origin/main 2>/dev/null || git rev-list --max-parents=0 HEAD | head -1)
|
|
fi
|
|
if npx --no-install commitlint --from "$from" --to "$to" --verbose 2>&1; then
|
|
ok "commitlint: $(git rev-list --count "$from..$to") commit(s) clean"
|
|
else
|
|
err "commitlint: violations found"; return 1
|
|
fi
|
|
}
|
|
|
|
lint_dco() {
|
|
section "dco (Signed-off-by)"
|
|
local from="${DCO_BASE_SHA:-${GITHUB_BASE_SHA:-}}"
|
|
local to="${DCO_HEAD_SHA:-${GITHUB_HEAD_SHA:-HEAD}}"
|
|
if [[ -z "$from" ]]; then
|
|
from=$(git merge-base HEAD origin/main 2>/dev/null || echo "HEAD~1")
|
|
fi
|
|
local missing=0
|
|
while read -r commit; do
|
|
if ! git log -1 --format=%B "$commit" | grep -qE '^Signed-off-by: .+ <.+@.+>$'; then
|
|
err "Missing Signed-off-by in commit $commit"
|
|
git log -1 --format=%B "$commit" | sed 's/^/ /' >&2
|
|
missing=1
|
|
fi
|
|
done < <(git rev-list "$from..$to" 2>/dev/null)
|
|
if (( missing )); then
|
|
err "dco: one or more commits missing sign-off"; return 1
|
|
fi
|
|
ok "dco: all commits in $from..$to signed"
|
|
}
|
|
|
|
lint_actionlint() {
|
|
section "actionlint (workflow YAML)"
|
|
if ! command -v actionlint >/dev/null 2>&1; then
|
|
warn "actionlint not installed (https://github.com/rhysd/actionlint); skipping"; return 0
|
|
fi
|
|
if actionlint -color -shellcheck=; then
|
|
ok "actionlint: workflows clean"
|
|
else
|
|
err "actionlint: violations found"; return 1
|
|
fi
|
|
}
|
|
|
|
# ----- Helpers: deps audit ----------------------------------------------
|
|
|
|
lint_deps_conan() {
|
|
section "deps-conan (pinned package version drift)"
|
|
if ! command -v conan >/dev/null 2>&1; then
|
|
warn "conan not installed; skipping"; return 0
|
|
fi
|
|
local pinned drift=0 line name version
|
|
pinned=$(grep -nE "self\.requires\(\"[^\"]+/[^\"]+\"" conanfile.py | sed -E 's/.*self\.requires\("([^"]+)\/([^"]+)".*/\1\t\2/')
|
|
printf "%-24s %-18s %-18s %s\n" "package" "pinned" "latest" "status" >&2
|
|
while IFS=$'\t' read -r name version; do
|
|
[[ -z "$name" ]] && continue
|
|
local latest
|
|
latest=$(conan search "$name" --remote=conancenter --format=json 2>/dev/null \
|
|
| jq -r '.conancenter[][] | .reference // empty' 2>/dev/null \
|
|
| sed -E "s|^${name}/||" | sort -V | tail -1)
|
|
[[ -z "$latest" ]] && latest="?"
|
|
if [[ "$latest" == "?" ]] || [[ "$latest" == "$version" ]]; then
|
|
printf " %-22s %-18s %-18s %s\n" "$name" "$version" "$latest" "ok" >&2
|
|
else
|
|
printf " %-22s %-18s %-18s %s\n" "$name" "$version" "$latest" "DRIFT" >&2
|
|
drift=1
|
|
fi
|
|
done <<< "$pinned"
|
|
if (( drift && STRICT )); then
|
|
err "deps-conan: drift detected (--strict)"; return 1
|
|
fi
|
|
ok "deps-conan: report complete"
|
|
}
|
|
|
|
lint_deps_cve() {
|
|
section "deps-cve (OSV scan)"
|
|
if ! command -v osv-scanner >/dev/null 2>&1; then
|
|
warn "osv-scanner not installed (https://google.github.io/osv-scanner/); skipping"
|
|
warn " install: brew install osv-scanner | go install github.com/google/osv-scanner/cmd/osv-scanner@latest"
|
|
return 0
|
|
fi
|
|
if [[ ! -f conan.lock ]]; then
|
|
if command -v conan >/dev/null 2>&1; then
|
|
say "generating conan.lock for OSV scan"
|
|
conan lock create . --lockfile-out=conan.lock 2>/dev/null || warn "conan lock create failed; continuing"
|
|
fi
|
|
fi
|
|
if [[ ! -f conan.lock ]]; then
|
|
warn "no conan.lock available; skipping CVE scan"; return 0
|
|
fi
|
|
if osv-scanner --lockfile=conan.lock --format=table; then
|
|
ok "deps-cve: no known vulnerabilities"
|
|
else
|
|
err "deps-cve: CVE(s) found"; return 1
|
|
fi
|
|
}
|
|
|
|
lint_deps_subs() {
|
|
section "deps-subs (submodule freshness)"
|
|
local lag=0
|
|
while IFS= read -r path; do
|
|
[[ -z "$path" ]] && continue
|
|
if [[ ! -d "$path/.git" && ! -f "$path/.git" ]]; then
|
|
warn "submodule $path not initialized; skipping"; continue
|
|
fi
|
|
git -C "$path" fetch --quiet 2>/dev/null || true
|
|
local upstream behind
|
|
upstream=$(git -C "$path" rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null || echo "")
|
|
if [[ -z "$upstream" ]]; then
|
|
warn "$path: no upstream tracked; skipping"; continue
|
|
fi
|
|
behind=$(git -C "$path" rev-list --count "HEAD..$upstream" 2>/dev/null || echo "?")
|
|
if [[ "$behind" == "0" ]]; then
|
|
printf " %-40s up to date\n" "$path" >&2
|
|
else
|
|
printf " %-40s %s commit(s) behind %s\n" "$path" "$behind" "$upstream" >&2
|
|
(( behind > 0 )) && lag=1
|
|
fi
|
|
done < <(git config --file .gitmodules --get-regexp 'submodule\..*\.path' | awk '{print $2}')
|
|
if (( lag && STRICT )); then
|
|
err "deps-subs: submodule(s) behind upstream (--strict)"; return 1
|
|
fi
|
|
ok "deps-subs: report complete"
|
|
}
|
|
|
|
lint_deps_licenses() {
|
|
section "deps-licenses (Conan + submodule license allowlist)"
|
|
# Allowlist of licenses compatible with GPL-3.0-or-later.
|
|
local allow_re='^(GPL-3\.0|LGPL-3\.0|MPL-2\.0|Apache-2\.0|BSD|MIT|Zlib|ISC|Boost|BSL-1\.0|Unlicense|Public-Domain)'
|
|
local violations=0
|
|
if command -v conan >/dev/null 2>&1; then
|
|
local graph
|
|
graph=$(conan graph info . --format=json 2>/dev/null || echo '{}')
|
|
if [[ "$graph" != "{}" ]]; then
|
|
while IFS=$'\t' read -r ref license; do
|
|
[[ -z "$ref" || -z "$license" || "$license" == "null" ]] && continue
|
|
if ! [[ "$license" =~ $allow_re ]]; then
|
|
err "license: $ref → $license (not on GPL-3.0-or-later allowlist)"
|
|
violations=1
|
|
fi
|
|
done < <(echo "$graph" | jq -r '.graph.nodes // {} | to_entries[] | select(.value.ref != null and .value.ref != "conanfile") | "\(.value.ref)\t\(.value.license // "")"' 2>/dev/null)
|
|
else
|
|
warn "conan graph info empty; skipping Conan license check"
|
|
fi
|
|
else
|
|
warn "conan not installed; skipping Conan license check"
|
|
fi
|
|
while IFS= read -r path; do
|
|
[[ -z "$path" ]] && continue
|
|
[[ ! -d "$path" ]] && continue
|
|
local lf
|
|
lf=$(find "$path" -maxdepth 2 -iname 'LICENSE*' -o -iname 'COPYING*' 2>/dev/null | head -1)
|
|
if [[ -z "$lf" ]]; then
|
|
warn "submodule $path: no LICENSE/COPYING file detected"; continue
|
|
fi
|
|
done < <(git config --file .gitmodules --get-regexp 'submodule\..*\.path' | awk '{print $2}')
|
|
if (( violations )); then return 1; fi
|
|
ok "deps-licenses: all on GPL-3.0-or-later allowlist"
|
|
}
|
|
|
|
# ----- Dispatcher -------------------------------------------------------
|
|
|
|
resolve_fn() {
|
|
case "$1" in
|
|
format) echo lint_format ;;
|
|
cppcheck) echo lint_cppcheck ;;
|
|
tidy) echo lint_tidy ;;
|
|
opengrep) echo lint_opengrep ;;
|
|
windows-hdr) echo lint_windows_hdr ;;
|
|
license) echo lint_license ;;
|
|
portability) echo lint_portability ;;
|
|
commitlint) echo lint_commitlint ;;
|
|
dco) echo lint_dco ;;
|
|
actionlint) echo lint_actionlint ;;
|
|
deps-conan) echo lint_deps_conan ;;
|
|
deps-cve) echo lint_deps_cve ;;
|
|
deps-subs) echo lint_deps_subs ;;
|
|
deps-licenses) echo lint_deps_licenses ;;
|
|
*) echo "" ;;
|
|
esac
|
|
}
|
|
|
|
EXIT_CODE=0
|
|
for c in "${CHECKS[@]}"; do
|
|
fn=$(resolve_fn "$c")
|
|
if [[ -z "$fn" ]]; then
|
|
err "unknown check: $c"; EXIT_CODE=2; continue
|
|
fi
|
|
if ! "$fn"; then
|
|
EXIT_CODE=1
|
|
fi
|
|
done
|
|
|
|
if (( EXIT_CODE == 0 )); then
|
|
ok "lint: all checks passed (${#CHECKS[@]} run)"
|
|
else
|
|
err "lint: failed (exit $EXIT_CODE)"
|
|
fi
|
|
exit $EXIT_CODE
|