Files
RedBear-OS/local/scripts/verify-fork-functions.sh
T
vasilito a9dfca6633 fix: verify-fork-functions compares bare function names
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.
2026-08-03 09:09:56 +03:00

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