79285e4ebf
When the upstream commit hash of the claimed upstream tag has not changed since the last successful verify, reuse the cached file list instead of re-cloning and re-hashing from scratch. This makes the canonical preflight fast on the common case where upstream didn't move. Cache is stored in .redbear-fork-verify/<fork>-<tag>.fingerprint. Touched only after a successful full content diff, so cache corruption is detected on the next run (commit hash mismatch triggers fresh clone).
230 lines
9.4 KiB
Bash
Executable File
230 lines
9.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# verify-fork-versions.sh — Enforce the "no fake version label" rule.
|
|
#
|
|
# For each local Cat 2 fork under local/sources/<name>/ that has a
|
|
# version field of the form `<X.Y.Z>+rb<B.B.B>`, verify that:
|
|
# 1. The fork's source content is a real rebase onto the matching
|
|
# upstream `<X.Y.Z>` release (with the Red Bear patches applied).
|
|
# 2. The `<B.B.B>` 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
|
|
|
|
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
|
|
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: <X.Y.Z>+rb<B.B.B> (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}')
|
|
|
|
if [ "$upstream_tag" = "PENDING_REBASE" ]; then
|
|
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
|
|
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")
|
|
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
|
|
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
|
|
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/<fork>-<tag>.
|
|
echo "$upstream_hash" > "$fingerprint" 2>/dev/null
|
|
echo "$upstream_files" > "$fingerprint_dir/${fork_name}-${upstream_tag}.files" 2>/dev/null
|
|
done
|
|
|
|
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
|