#!/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 `` part matches the current Red Bear OS git branch. # 3. The `version` field in the fork's Cargo.toml starts with that # upstream release tag. # # The `+rb` suffix is Cargo build metadata, NOT a pre-release identifier. # Using `-rb` is a policy violation because Cargo treats it as a pre-release # and transitive deps requiring `^X.Y.Z` will not resolve to the fork. # # This script is invoked by build-preflight.sh and apply-rb-suffix.sh. # It returns exit code 1 if any fork fails the check. 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 BRANCH="$(git branch --show-current 2>/dev/null || echo "")" BRANCH_VERSION="$(echo "$BRANCH" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || true)" violations=0 # Per-fork status tracking for post-loop summary (Phase 13.1) declare -A FORK_STATUS=() declare -A FORK_DETAIL=() for fork_dir in local/sources/*/; do [ -d "$fork_dir" ] || continue fork_name=$(basename "$fork_dir") toml="$fork_dir/Cargo.toml" [ -f "$toml" ] || { FORK_STATUS[$fork_name]="SKIP"; FORK_DETAIL[$fork_name]="no Cargo.toml (not a tracked fork)"; continue; } version=$(grep -E '^version\s*=' "$toml" | head -1 | sed -E 's/.*"([^"]+)".*/\1/') || true if [ -z "$version" ]; then # Workspace root: walk member list, pick first +rb version found. for member in $(grep -oP '^\s*"\K[^"]+(?=/")' "$toml"); do member_toml="$fork_dir$member/Cargo.toml" [ -f "$member_toml" ] || continue mv=$(grep -E '^version\s*=' "$member_toml" | head -1 | sed -E 's/.*"([^"]+)".*/\1/') if [ -n "$mv" ] && [[ "$mv" == *"+rb"* ]]; then version="$mv" break fi done fi [ -n "$version" ] || continue # Only check Cat 2 forks (those with +rb build-metadata suffix) if [[ "$version" != *"+rb"* ]]; then continue fi # Extract the upstream base version (before +rb) and the branch version (after +rb) base_version=$(echo "$version" | sed -E 's/\+rb[0-9]+\.[0-9]+\.[0-9]+$//') || true rb_branch=$(echo "$version" | sed -E 's/^.*\+rb([0-9]+\.[0-9]+\.[0-9]+)$/\1/') || true if [ -z "$base_version" ] || [ -z "$rb_branch" ]; then echo "ERROR: $toml has malformed version '$version'" >&2 echo " Expected format: +rb (e.g. 0.9.0+rb0.2.5)" >&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}') fork_mode=$(echo "$map_line" | awk '{print $4}') # diverged mode (Phase 2.4 introduced): fork is known to be substantially # diverged from upstream, content check is an advisory only. The fork # may have substantial Red Bear work committed on top of a much-older # upstream base. The operator must run upgrade-forks.sh manually # before content-check can be re-enabled. The build runs anyway with # an explicit warning printed to stderr. if [ "$fork_mode" = "diverged" ]; then FORK_STATUS[$fork_name]="DIVERGED" FORK_DETAIL[$fork_name]="mode=diverged; content check skipped" echo "WARN: $fork_name is in 'diverged' mode — content check skipped." >&2 echo " Fork has substantial post-fork work; run upgrade-forks.sh $fork_name" >&2 echo " manually when ready to rebase onto a newer upstream tag." >&2 if [ "${REDBEAR_STRICT_DIVERGED_CHECK:-0}" = "1" ]; then echo "ERROR: REDBEAR_STRICT_DIVERGED_CHECK=1 — operator override requested." >&2 violations=$((violations + 1)) fi continue fi if [ "$upstream_tag" = "PENDING_REBASE" ]; then FORK_STATUS[$fork_name]="PENDING_REBASE" FORK_DETAIL[$fork_name]="needs manual rebase" echo "ERROR: $fork_name is marked as PENDING_REBASE in $MAP_FILE." >&2 echo " A real rebase onto a chosen upstream tag is required." >&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 violations=$((violations + 1)) continue fi # Verify the +rb suffix matches the current branch if [ -n "$BRANCH_VERSION" ] && [ "$rb_branch" != "$BRANCH_VERSION" ]; then echo "ERROR: $fork_name version suffix +rb$rb_branch does not match" >&2 echo " current branch '$BRANCH' (expected +rb$BRANCH_VERSION)." >&2 violations=$((violations + 1)) continue fi # snapshot mode no longer skips content check — must match upstream tag. # 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 FORK_STATUS[$fork_name]="NO_UPSTREAM_TAG" FORK_DETAIL[$fork_name]="tag $upstream_tag not found at $upstream_url" echo "WARN: $fork_name: couldn't ls-remote $upstream_url tag $upstream_tag, skipping content check" >&2 continue fi # Per-fork content fingerprint cache: store the last successfully-verified # upstream commit + a hash of the file list. If the upstream commit hasn't # changed since the last verify, reuse the cached file list instead of # cloning + re-hashing from scratch. This makes the canonical preflight # cheap on the common case where upstream didn't move. fingerprint_dir="$ROOT/.redbear-fork-verify" mkdir -p "$fingerprint_dir" fingerprint="$fingerprint_dir/${fork_name}-${upstream_tag}.fingerprint" if [ -f "$fingerprint" ] && [ "$(cat "$fingerprint" 2>/dev/null)" = "$upstream_hash" ]; then upstream_files=$(cat "$fingerprint_dir/${fork_name}-${upstream_tag}.files" 2>/dev/null) else upstream_files="" fi # Compare file lists: the local fork should have the same files as # upstream at $upstream_hash, plus any Red Bear patch files. 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 upstream_dir="/tmp/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 upstream_files=$(cd "$upstream_dir" && 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 expected to differ (documented Red Bear patches) 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 while IFS= read -r f; do [ -z "$f" ] && continue f=$(echo "$f" | sed -E 's|^[ab]/||') expected_differ+=("$f") done < <(cd "$ROOT/$fork_dir" && git apply --numstat "$patch_dir/$patch_file" 2>/dev/null \ | awk '{print $3}' | sort -u) done < <(ls "$patch_dir" 2>/dev/null) fi expected_differ=($(printf '%s\n' "${expected_differ[@]}" | sort -u)) expected_differ+=("Cargo.toml") expected_differ+=("Cargo.toml.orig") # Per-fork declarative expected-differ list (Phase 2.4): forks # whose local-patches/ dir is empty but the fork has legitimately # diverged via direct commits need explicit entries here. case "$fork_name" in libredox) expected_differ+=("src/lib.rs") ;; # F_DUPFD_CLOEXEC + AcpiVerb at commit 6908adc esac expected_differ=($(printf '%s\n' "${expected_differ[@]}" | sort -u)) 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 echo "ERROR: $fork_name is missing files that exist in upstream $upstream_tag:" >&2 echo "$only_upstream" | sed 's/^/ /' >&2 violations=$((violations + 1)) fi if [ -n "$only_local" ]; then # Subtract files expected to differ (Red Bear patches + Cargo.toml). # Build the same expected_differ set as above so the subtraction is # consistent (Phase 2.4: previous logic used `find` on the patches # dir which only got patch-file names, not their actual contents). cd "$ROOT/$fork_dir" patch_dir="$ROOT/local/patches/$fork_name" patch_modified_files=() if [ -d "$patch_dir" ]; then while IFS= read -r pf; do [ -z "$pf" ] && continue while IFS= read -r f; do [ -z "$f" ] && continue f=$(echo "$f" | sed -E 's|^[ab]/||') patch_modified_files+=("$f") done < <(git apply --numstat "$patch_dir/$pf" 2>/dev/null | awk '{print $3}') done < <(ls "$patch_dir" 2>/dev/null) fi patch_modified_files=($(printf '%s\n' "${patch_modified_files[@]}" | sort -u)) patch_modified_files+=("Cargo.toml" "Cargo.toml.orig") # Phase 2.4: include common Red Bear-added subtrees that don't always # appear in patch outputs (notably the installer GUI brought in # via prior fork merges). patch_modified_files+=("gui/Cargo.toml" "gui/Cargo.lock" "gui/.gitignore" "gui/README.md" "gui/src/main.rs" "gui/src/sys.rs" "gui/src/sys/linux.rs" "gui/src/sys/redox.rs") # Per-fork declarative expected-differ list for forks whose # local-patches/ dir is empty (no patch files) but the fork # itself has legitimately diverged via direct commits. case "$fork_name" in libredox) patch_modified_files+=("src/lib.rs") ;; # F_DUPFD_CLOEXEC + AcpiVerb esac patch_modified_files=($(printf '%s\n' "${patch_modified_files[@]}" | sort -u)) local_non_patch=$(comm -23 <(echo "$only_local") <(printf '%s\n' "${patch_modified_files[@]}")) 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 violations=$((violations + 1)) fi fi # Verify content of shared files (skip files touched by Red Bear patches) diff_count=0 while IFS= read -r f; do [ -z "$f" ] && continue 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 violations=$((violations + 1)) fi rm -rf "/tmp/verify-$fork_name" # Cache successful verify result so subsequent preflight runs don't # re-clone upstream. Stored under .redbear-fork-verify/-. echo "$upstream_hash" > "$fingerprint" 2>/dev/null echo "$upstream_files" > "$fingerprint_dir/${fork_name}-${upstream_tag}.files" 2>/dev/null # Fork passed content check — record status for post-loop summary FORK_STATUS[$fork_name]="PASS" FORK_DETAIL[$fork_name]="content matches upstream $upstream_tag" done # Post-loop summary (Phase 13.1): enumerate each fork's verification status. # Return to script root (the per-fork content check does cd into each fork dir). cd "$ROOT" echo "" echo "=== Post-verification fork status ===" printf " %-12s %-18s %s\n" "fork" "status" "detail" printf " %-12s %-18s %s\n" "----------" "----------------" "------" for fork_dir in local/sources/*/; do [ -d "$fork_dir" ] || continue fork_name=$(basename "$fork_dir") status="${FORK_STATUS[$fork_name]:-}" if [ -z "$status" ]; then # Derive status from fork-upstream-map when the main loop didn't set one map_line=$(grep "^$fork_name\b" "$MAP_FILE" 2>/dev/null || true) if [ -n "$map_line" ]; then fork_mode=$(echo "$map_line" | awk '{print $4}') map_tag=$(echo "$map_line" | awk '{print $3}') case "${fork_mode:-snapshot}" in diverged) status="DIVERGED-BYPASS"; detail="diverged mode (skipped before content check)" ;; tracked) status="TRACKED-PASS"; detail="tracked-fork, version label passed" ;; snapshot) status="SNAPSHOT-PASS"; detail="snapshot fork, version label ${map_tag:-?} matched" ;; *) status="UNKNOWN-MODE"; detail="mode=${fork_mode:-?}" ;; esac else status="NOT-MAPPED" detail="not in fork-upstream-map.toml" fi else detail="${FORK_DETAIL[$fork_name]:-not checked}" fi printf " %-12s %-18s %s\n" "$fork_name" "$status" "$detail" done echo "" if [ "$violations" -gt 0 ]; then echo "" >&2 echo "FAIL: $violations fork version violations found." >&2 exit 1 fi echo "All Cat 2 forks pass the no-fake-version-label check." exit 0