#!/usr/bin/env bash # verify-fork-versions.sh — Enforce the "no fake version label" rule. # # For each local Cat 2 fork under local/sources// that has a # version field of the form `-rb`, verify that: # 1. The fork's source content is a real rebase onto the matching # upstream `` release (with the Red Bear patches applied). # 2. The `version` field in the fork's Cargo.toml starts with that # upstream release tag. # # This script is invoked by build-preflight.sh and apply-rb-suffix.sh. # It returns exit code 1 if any fork fails the check, with a clear # error message identifying the fork and the specific mismatch. set -euo pipefail ROOT="$(cd "$(dirname "$0")/../.." && pwd)" cd "$ROOT" MAP_FILE="$ROOT/local/fork-upstream-map.toml" if [ ! -f "$MAP_FILE" ]; then echo "ERROR: $MAP_FILE not found." >&2 echo " Run local/scripts/refresh-fork-upstream-map.sh to generate it." >&2 exit 1 fi violations=0 for fork_dir in local/sources/*/; do [ -d "$fork_dir" ] || continue fork_name=$(basename "$fork_dir") toml="$fork_dir/Cargo.toml" [ -f "$toml" ] || continue version=$(grep -E '^version\s*=' "$toml" | head -1 | sed -E 's/.*"([^"]+)".*/\1/') || true [ -n "$version" ] || continue # Only check Cat 2 forks (those with -rb suffix) if [[ "$version" != *"-rb"* ]]; then continue fi # Extract the base version (before -rb) and the rb counter base_version=$(echo "$version" | sed -E 's/-rb[0-9]+$//') || true rb_n=$(echo "$version" | sed -E 's/^.*-rb([0-9]+)$/\1/') || true if [ -z "$base_version" ] || [ -z "$rb_n" ]; then # If the base_version came out empty (e.g. version field is # blank), we already detected & skipped in the empty-version # case above. Reaching this branch with empty outputs means # the version string is malformed (e.g. ends in `-rb` with # no number). That is a fake-label failure. echo "ERROR: $toml has malformed version '$version' (must be -rb)" >&2 violations=$((violations + 1)) continue fi # Look up the fork in the upstream map map_line=$(grep "^$fork_name\b" "$MAP_FILE" || true) if [ -z "$map_line" ]; then echo "ERROR: $fork_name is not in $MAP_FILE. Run refresh-fork-upstream-map.sh." >&2 violations=$((violations + 1)) continue fi upstream_url=$(echo "$map_line" | awk '{print $2}') upstream_tag=$(echo "$map_line" | awk '{print $3}') # If the upstream tag is `PENDING_REBASE` (used to mark a fork # whose rebase is in progress), the build cannot proceed. Refuse # with a clear error pointing the user to the rebase work. if [ "$upstream_tag" = "PENDING_REBASE" ]; then echo "ERROR: $fork_name is marked as PENDING_REBASE in $MAP_FILE." >&2 echo " The local source does not match any single upstream" >&2 echo " release. A real rebase onto a chosen upstream tag is" >&2 echo " required. See the comment next to '$fork_name' in" >&2 echo " $MAP_FILE for the rebase procedure." >&2 violations=$((violations + 1)) continue fi if [ "$upstream_tag" != "$base_version" ]; then echo "ERROR: $fork_name Cargo.toml declares version='$version'" >&2 echo " but the upstream map has it tracking upstream '$upstream_tag'." >&2 echo " Either update the version field or update the map." >&2 violations=$((violations + 1)) continue fi # Fetch the upstream tag's tree hash upstream_hash=$(cd /tmp && git ls-remote --tags "$upstream_url" "refs/tags/$upstream_tag" 2>/dev/null | awk '{print $1}' | head -1) if [ -z "$upstream_hash" ]; then echo "WARN: $fork_name: couldn't ls-remote $upstream_url tag $upstream_tag, skipping content check" >&2 continue fi # Compare file lists: the local fork should have the same files as # upstream at $upstream_hash, plus any Red Bear patch files in # local/patches/$fork_name/. cd "$fork_dir" local_files=$(find . -type f -not -path './.git/*' -not -path './target/*' -not -path './Cargo.lock' | sort | sed 's|^\./||') cd /tmp rm -rf "verify-$fork_name" 2>/dev/null # Shallow-clone the specific tag directly. `--depth 1 --branch ` # makes the tag the initial HEAD, so we have the file tree without # having to fetch a specific commit (which `git clone --depth 1` # can't reach on shallow clones because the commit is past the # shallow boundary). upstream_dir="verify-$fork_name-upstream" rm -rf "$upstream_dir" 2>/dev/null if ! timeout 60 git clone --depth 1 --branch "$upstream_tag" --quiet "$upstream_url" "$upstream_dir" 2>/dev/null; then echo "WARN: $fork_name: couldn't clone $upstream_url branch $upstream_tag, skipping content check" >&2 cd "$ROOT" continue fi cd "$upstream_dir" upstream_files=$(find . -type f -not -path './.git/*' -not -path './target/*' -not -path './Cargo.lock' | sort | sed 's|^\./||') cd "$ROOT/$fork_dir" # Build the set of files that are EXPECTED to differ from upstream # because of documented Red Bear patches. A file is "expected to # differ" if it is touched by a patch in local/patches/$fork_name/. # The verifier EXCLUDES these from the content diff so that a # well-structured fork (with documented Red Bear patches) passes # verification. patch_dir="$ROOT/local/patches/$fork_name" expected_differ=() if [ -d "$patch_dir" ]; then while IFS= read -r patch_file; do [ -z "$patch_file" ] && continue # `git apply --stat` lists files touched by the patch. # Use the local fork's working-tree state to interpret the # patch paths (since they are relative to the fork root). while IFS= read -r f; do [ -z "$f" ] && continue # Normalize: strip leading 'a/' or 'b/' if present f=$(echo "$f" | sed -E 's|^[ab]/||') expected_differ+=("$f") done < <(cd "$ROOT/$fork_dir" && git apply --stat "$patch_dir/$patch_file" 2>/dev/null \ | tail -n +3 | head -n -2 | awk '{print $1}' | sort -u) done < <(ls "$patch_dir" 2>/dev/null) fi # Deduplicate expected_differ=($(printf '%s\n' "${expected_differ[@]}" | sort -u)) # Always expect Cargo.toml and Cargo.toml.orig to differ (version field # bump and auto-generated Cargo.toml are Red Bear bookkeeping). expected_differ+=("Cargo.toml") expected_differ+=("Cargo.toml.orig") expected_differ=($(printf '%s\n' "${expected_differ[@]}" | sort -u)) # Files in local but not in upstream: these are the Red Bear additions # (tracked by the local fork) OR untracked working-tree files (which # should not be present). Files in upstream but not in local: these # are missing patches (unacceptable). only_local=$(comm -23 <(echo "$local_files") <(echo "$upstream_files")) only_upstream=$(comm -13 <(echo "$local_files") <(echo "$upstream_files")) if [ -n "$only_upstream" ]; then # Filter out files that are EXPECTED to be absent (e.g. deleted # by a Red Bear patch). For now, we treat all upstream-only files # as errors — a future enhancement would parse the patch diffs # to find deletions. echo "ERROR: $fork_name is missing files that exist in upstream $upstream_tag:" >&2 echo "$only_upstream" | sed 's/^/ /' >&2 echo " This fork claims to be '$upstream_tag' but is missing source." >&2 violations=$((violations + 1)) fi if [ -n "$only_local" ]; then # Filter out files in local/patches/$fork_name/ (patches # themselves, README, etc.) — those are Red Bear bookkeeping, # not source files. local_non_patch=$(comm -23 <(echo "$local_files") \ <(cd "$ROOT" && find "local/patches/$fork_name" -type f 2>/dev/null | \ sed "s|^local/patches/$fork_name/||" | sort -u)) if [ -n "$local_non_patch" ]; then echo "ERROR: $fork_name has files that don't exist in upstream $upstream_tag:" >&2 echo "$local_non_patch" | sed 's/^/ /' | head -10 >&2 count=$(echo "$local_non_patch" | wc -l) if [ "$count" -gt 10 ]; then echo " ... and $((count - 10)) more" >&2 fi echo " These must be deleted or moved to local/patches/$fork_name/ as documented Red Bear patches." >&2 violations=$((violations + 1)) fi fi # Verify content of shared files. Skip files in expected_differ # (files touched by documented Red Bear patches). diff_count=0 while IFS= read -r f; do [ -z "$f" ] && continue # Skip if this file is in expected_differ skip=0 for ed in "${expected_differ[@]}"; do if [ "$f" = "$ed" ]; then skip=1 break fi done [ "$skip" = "1" ] && continue if ! diff -q "$f" "$upstream_dir/$f" >/dev/null 2>&1; then if [ "$diff_count" -eq 0 ]; then echo "ERROR: $fork_name has files that diverge from upstream $upstream_tag:" >&2 fi echo " $f" >&2 diff_count=$((diff_count + 1)) fi done <<< "$(comm -12 <(echo "$local_files") <(echo "$upstream_files"))" if [ "$diff_count" -gt 0 ]; then echo " These must either be re-rebased onto $upstream_tag OR" >&2 echo " moved to local/patches/$fork_name/ as documented Red Bear patches." >&2 violations=$((violations + 1)) fi rm -rf "/tmp/verify-$fork_name" done if [ "$violations" -gt 0 ]; then echo "" >&2 echo "FAIL: $violations fork version violations found." >&2 echo " Run local/scripts/refresh-fork-upstream-map.sh and" >&2 echo " local/scripts/apply-rb-suffix.sh to fix the offending forks." >&2 exit 1 fi echo "All Cat 2 forks pass the no-fake-version-label check." exit 0