a9dfca6633
The extractor matched '(?:pub )?(?:async )?(?:unsafe )?fn \w+' and then
stripped only the 'fn ', leaving the visibility modifier inside the key.
So 'fn foo' and 'pub fn foo' were different keys, and 'pub(crate) fn' was
not matched at all. Merely changing a function's visibility made it look
deleted: base was reported as missing remove_dentry, which the fork
demonstrably still has, and the report printed nonsense like 'fn pub
socket'. The exclude files had accumulated duplicate entries ('foo' and
'pub foo') to work around this.
Compare bare names on both sides, and normalize the function half of each
exclude entry so existing files keep matching.
232 lines
9.9 KiB
Bash
Executable File
232 lines
9.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# verify-fork-functions.sh — Verify that local forks retain ALL upstream functions.
|
|
#
|
|
# This is the FUNCTION-LEVEL verification that prevents the silent code-loss bug
|
|
# that occurred when git merge conflict resolution dropped upstream functions
|
|
# (kfdwrite, bulk_insert_files, resize, read_with_timeout, etc.) from the kernel fork.
|
|
#
|
|
# For each fork with an upstream remote, this script:
|
|
# 1. Extracts all function definitions (fn <name>) from upstream source files
|
|
# 2. Checks that each function exists in the local fork
|
|
# 3. Reports any missing functions with file paths and line numbers
|
|
#
|
|
# This catches the class of bugs that file-level verification CANNOT detect:
|
|
# when a file is touched by RB patches, file-level diff allows ANY difference,
|
|
# including silently dropped upstream functions.
|
|
#
|
|
# Usage:
|
|
# ./local/scripts/verify-fork-functions.sh # Check all forks
|
|
# ./local/scripts/verify-fork-functions.sh kernel relibc # Check specific forks
|
|
# ./local/scripts/verify-fork-functions.sh --no-fetch # Skip network fetch
|
|
# ./local/scripts/verify-fork-functions.sh --quiet # Only print violations
|
|
#
|
|
# Exit codes:
|
|
# 0 = All upstream functions present in all forks
|
|
# 1 = One or more upstream functions are missing from forks
|
|
|
|
set -uo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
|
|
NO_FETCH=0
|
|
QUIET=0
|
|
declare -a TARGET_FORKS=()
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--no-fetch) NO_FETCH=1 ;;
|
|
--quiet) QUIET=1 ;;
|
|
-h|--help)
|
|
echo "Usage: $0 [--no-fetch] [--quiet] [fork1 fork2 ...]"
|
|
echo ""
|
|
echo "Verifies that local forks retain ALL upstream functions."
|
|
echo "Catches silent code loss from bad merge conflict resolution."
|
|
exit 0
|
|
;;
|
|
--*) echo "Unknown: $1" >&2; exit 1 ;;
|
|
*) TARGET_FORKS+=("$1") ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
cd "$PROJECT_ROOT"
|
|
|
|
# Discover forks if not specified
|
|
if [[ ${#TARGET_FORKS[@]} -eq 0 ]]; then
|
|
while IFS= read -r line; do
|
|
TARGET_FORKS+=("$line")
|
|
done < <(for d in local/sources/*/; do
|
|
[[ -d "$d/.git" ]] || continue
|
|
name=$(basename "$d")
|
|
git -C "$d" remote get-url upstream >/dev/null 2>&1 && echo "$name"
|
|
done | sort)
|
|
fi
|
|
|
|
VIOLATIONS=0
|
|
TOTAL_MISSING=0
|
|
|
|
for fork in "${TARGET_FORKS[@]}"; do
|
|
fork_dir="local/sources/${fork}"
|
|
|
|
if [[ ! -d "$fork_dir/.git" ]]; then
|
|
continue
|
|
fi
|
|
|
|
upstream_url=$(cd "$fork_dir" && git remote get-url upstream 2>/dev/null || true)
|
|
if [[ -z "$upstream_url" ]]; then
|
|
continue
|
|
fi
|
|
|
|
# Determine upstream branch
|
|
upstream_branch="master"
|
|
if ! (cd "$fork_dir" && git rev-parse --verify upstream/master >/dev/null 2>&1); then
|
|
if (cd "$fork_dir" && git rev-parse --verify upstream/main >/dev/null 2>&1); then
|
|
upstream_branch="main"
|
|
else
|
|
if [[ "$NO_FETCH" -eq 0 ]]; then
|
|
(cd "$fork_dir" && git fetch upstream --depth=200 --quiet 2>/dev/null) || true
|
|
fi
|
|
if (cd "$fork_dir" && git rev-parse --verify upstream/master >/dev/null 2>&1); then
|
|
upstream_branch="master"
|
|
elif (cd "$fork_dir" && git rev-parse --verify upstream/main >/dev/null 2>&1); then
|
|
upstream_branch="main"
|
|
else
|
|
continue
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Fetch if needed
|
|
if [[ "$NO_FETCH" -eq 0 ]]; then
|
|
(cd "$fork_dir" && git fetch upstream --depth=200 --quiet 2>/dev/null) || true
|
|
fi
|
|
|
|
upstream_ref="upstream/$upstream_branch"
|
|
|
|
# Get list of source files that exist in BOTH upstream and our fork
|
|
upstream_files=$(cd "$fork_dir" && git ls-tree -r --name-only "$upstream_ref" 2>/dev/null | grep '\.rs$' | sort)
|
|
local_files=$(cd "$fork_dir" && find . -name '*.rs' -not -path './.git/*' -not -path './target/*' -not -path './stage/*' | sed 's|^\./||' | sort)
|
|
mapfile -t shared_files < <(comm -12 <(echo "$upstream_files") <(echo "$local_files"))
|
|
|
|
fork_missing=0
|
|
declare -a missing_details=()
|
|
|
|
for f in "${shared_files[@]}"; do
|
|
# Extract BARE function names (no visibility/async/unsafe/const
|
|
# modifiers) from the upstream version of the file.
|
|
#
|
|
# The key must be the bare name. The previous pattern kept whatever
|
|
# modifier it happened to match as part of the key ('pub foo' vs
|
|
# 'foo'), so merely changing a function's visibility in the fork —
|
|
# or upstream using 'pub(crate)', which the old pattern did not
|
|
# match at all — made an existing function look deleted. That is
|
|
# what produced report lines like "fn pub socket" and flagged
|
|
# functions the fork demonstrably still has. Visibility changes are
|
|
# not dropped code; only the name's presence is what this gate is
|
|
# meant to assert.
|
|
upstream_fns=$(cd "$fork_dir" && git show "${upstream_ref}:${f}" 2>/dev/null | \
|
|
grep -oP '\bfn\s+\w+' | sed -E 's/^fn[[:space:]]+//' | sort -u)
|
|
|
|
[[ -z "$upstream_fns" ]] && continue
|
|
|
|
# Extract function names from our version of the file
|
|
local_fns=$(cd "$fork_dir" && grep -oP '\bfn\s+\w+' "$f" 2>/dev/null | \
|
|
sed -E 's/^fn[[:space:]]+//' | sort -u)
|
|
|
|
# Load per-fork exclusion list for intentionally removed/replaced functions
|
|
exclude_file="${fork_dir}/.verify-fork-functions.exclude"
|
|
declare -A excluded=()
|
|
if [[ -f "$exclude_file" ]]; then
|
|
while IFS= read -r line; do
|
|
[[ "$line" =~ ^# ]] && continue
|
|
[[ -z "$line" ]] && continue
|
|
# Existing exclude files were written against the old
|
|
# modifier-carrying key format (and often list the same
|
|
# function twice, e.g. 'foo' and 'pub foo', to work around
|
|
# it). Normalize the function half to the bare name so both
|
|
# spellings keep matching now that keys are bare.
|
|
ex_path="${line%%:*}"
|
|
ex_fn="${line#*:}"
|
|
ex_fn="$(printf '%s' "$ex_fn" | \
|
|
sed -E 's/^(pub(\([^)]*\))?[[:space:]]+|async[[:space:]]+|unsafe[[:space:]]+|const[[:space:]]+)+//')"
|
|
excluded["${ex_path}:${ex_fn}"]=1
|
|
done < "$exclude_file"
|
|
fi
|
|
|
|
# Find functions in upstream but not in our fork
|
|
missing=$(comm -23 <(echo "$upstream_fns") <(echo "$local_fns") 2>/dev/null)
|
|
|
|
if [[ -n "$missing" ]]; then
|
|
while IFS= read -r fn; do
|
|
[[ -z "$fn" ]] && continue
|
|
# Check exclusion list: "$f:$fn"
|
|
if [[ -n "${excluded["$f:$fn"]:-}" ]]; then
|
|
[[ "$QUIET" -eq 0 ]] && missing_details+=(" $f: fn $fn → EXCLUDED (RB intentional)")
|
|
continue
|
|
fi
|
|
# Common function names (constructors, trait impls, patterns) appear
|
|
# in many unrelated files — cross-file search produces false MOVED matches.
|
|
bare_fn=$(echo "$fn" | sed -E 's/^(pub |async |unsafe )+//')
|
|
# Only fmt and eq are blanket-exempt: derivable, no semantic cost.
|
|
# drop/deref/hash/clone must go through the fork-specific exclude file.
|
|
case "$bare_fn" in
|
|
fmt|eq)
|
|
missing_details+=(" $f: fn $fn → WARN (derivable; compiler-caught, not gated)")
|
|
continue
|
|
;;
|
|
esac
|
|
case "$bare_fn" in
|
|
new|default|read|write|parse|open|close|run|init|main|\
|
|
get|set|remove|insert|delete|start|stop|send|recv|\
|
|
connect|bind|listen|accept|clone|drop|eq|hash|fmt|\
|
|
from|into|next|count|len|is_empty|clear|contains|\
|
|
daemon|allocate|index|try_from|from_psf|into_object|\
|
|
get_unique|set_plane)
|
|
# Too ambiguous — treat as truly missing (operator must decide)
|
|
;;
|
|
*)
|
|
found_elsewhere=$(cd "$fork_dir" && \
|
|
grep -rlP "(?:pub )?(?:async )?(?:unsafe )?fn ${bare_fn}\b" \
|
|
--include='*.rs' --exclude-dir='.git' --exclude-dir='target' --exclude-dir='stage' . 2>/dev/null | \
|
|
grep -v "^\./${f}$" | head -1 | sed 's|^\./||')
|
|
if [[ -n "$found_elsewhere" ]]; then
|
|
[[ "$QUIET" -eq 0 ]] && missing_details+=(" $f: fn $fn → MOVED to $found_elsewhere")
|
|
continue
|
|
fi
|
|
;;
|
|
esac
|
|
missing_details+=(" $f: fn $fn")
|
|
fork_missing=$((fork_missing + 1))
|
|
TOTAL_MISSING=$((TOTAL_MISSING + 1))
|
|
done <<< "$missing"
|
|
fi
|
|
done
|
|
|
|
if [[ "$fork_missing" -gt 0 ]]; then
|
|
if [[ "$QUIET" -eq 0 ]] || [[ "$fork_missing" -gt 0 ]]; then
|
|
echo ""
|
|
echo -e "\033[1;31mFAIL: $fork is missing $fork_missing upstream function(s):\033[0m"
|
|
for detail in "${missing_details[@]}"; do
|
|
echo "$detail"
|
|
done
|
|
echo ""
|
|
echo " These functions exist in upstream $upstream_ref but are MISSING from the local fork."
|
|
echo " This likely means a bad merge or cherry-pick dropped them silently."
|
|
echo " Fix: ./local/scripts/upgrade-forks.sh $fork"
|
|
fi
|
|
VIOLATIONS=$((VIOLATIONS + 1))
|
|
elif [[ "$QUIET" -eq 0 ]]; then
|
|
echo -e "\033[1;32mOK: $fork — all upstream functions present\033[0m"
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
if [[ "$VIOLATIONS" -gt 0 ]]; then
|
|
echo -e "\033[1;31mFUNCTION VERIFICATION FAILED: $VIOLATIONS fork(s) missing $TOTAL_MISSING upstream function(s).\033[0m"
|
|
exit 1
|
|
fi
|
|
|
|
echo -e "\033[1;32mAll forks retain all upstream functions.\033[0m"
|
|
exit 0
|