Files
RedBear-OS/local/scripts/verify-tracked-sources.sh
T
vasilito 36ddc1f2db verify-tracked-sources: brush is a fork, match provenance case-insensitively
brush records `# Upstream: https://github.com/reubeno/brush`, but the check
matched `upstream:` case-sensitively AND required the URL to end in .tar/.git,
so it missed on both counts and filed a genuine fork as first-party.

Now matches an explicit upstream/snapshot/origin label followed by a URL, any
case. Deliberately NOT any bare URL in a comment: a stray bug-tracker link must
never read as provenance, since that error direction (first-party treated as
vendored) is the one that loses irreplaceable work.

Remaining inaccuracy is missing DATA, not detection. libepoxy, libpciaccess,
libudev, libxcvt and libdisplay-info are upstream projects whose recipes carry
only `path = "source"` with no origin recorded anywhere, so nothing can tell
them apart from our own code. They classify first-party, which is merely
stricter. The real fix is to record their upstream in the recipe.
2026-08-04 19:39:06 +03:00

298 lines
13 KiB
Bash
Executable File

#!/usr/bin/env bash
# verify-tracked-sources.sh — integrity gate for git-tracked vendored source trees.
#
# WHY THIS EXISTS
# ---------------
# Several recipes under local/recipes/ are "vendored forks": their source/ tree
# is committed to git and carries real Red Bear work baked directly into files
# that also exist upstream (see local/docs/VERSIONING.md, "baked shim"). That
# tree is durable state, but nothing guarded it, and two failure modes have
# actually happened:
#
# 1. DELETION. An ad-hoc `rm -rf <recipe>/source` (to force a clean
# re-extract) removes thousands of tracked files. AGENTS.md already warns
# that this "has wiped tracked local/recipes/*/source/ trees in the past".
#
# 2. SILENT OVERWRITE. For a `tar` recipe, deleting source/ makes the
# cookbook re-extract PRISTINE upstream over it. Files that exist upstream
# get their committed Red Bear edits replaced with upstream content. No
# error is raised -- the package may even still build -- so the loss ships
# as a silent regression. This is how kwin lost its std::expected /
# vulkan-hpp and X11-gating fixes, and kio lost the Q_OS_REDOX guards in
# hostinfo.cpp.
#
# Neither mode is detectable by the existing gates: verify-fork-versions.sh
# covers local/sources/ (the submodule forks), and the build-redbear.sh
# dirty-source gate likewise only looks at local/sources/. The vendored recipe
# trees carry equally durable work and had no equivalent check.
#
# WHAT IT CHECKS
# --------------
# Recipe seds legitimately rewrite tracked source files on every build (that is
# the current in-tree build model), so "modified" alone cannot mean "damaged".
# The gate therefore separates three cases:
#
# DELETED tracked file under */source/ -> ERROR, always. A build never
# legitimately deletes tracked
# source.
# MODIFIED, not in the baseline -> ERROR. Something changed a
# tracked source file that no
# recorded recipe sed accounts
# for -- the overwrite signature.
# MODIFIED, listed in the baseline -> known churn. Reported as a
# durability WARNING (this is
# uncommitted work living in a
# durable tree); escalate to an
# error with
# REDBEAR_STRICT_TRACKED_SOURCES=1.
#
# The baseline (local/tracked-source-baseline.txt) is itself tracked, so the set
# of "expected to be dirty" files is reviewable in git rather than invisible.
#
# USAGE
# verify-tracked-sources.sh [--quiet] # check (preflight mode)
# verify-tracked-sources.sh --update # regenerate the baseline
# verify-tracked-sources.sh --list # show current dirty tracked sources
#
# Bypass: REDBEAR_SKIP_TRACKED_SOURCE_CHECK=1 (emergency only).
# Escalate baseline churn to an error: REDBEAR_STRICT_TRACKED_SOURCES=1.
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
BASELINE="$ROOT/local/tracked-source-baseline.txt"
QUIET=0
MODE=check
for arg in "$@"; do
case "$arg" in
--quiet) QUIET=1 ;;
--update) MODE=update ;;
--list) MODE=list ;;
-h|--help)
sed -n '2,60p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
exit 0 ;;
*) echo "verify-tracked-sources.sh: unknown argument '$arg'" >&2; exit 2 ;;
esac
done
say() { [ "$QUIET" -eq 1 ] || echo "$@"; }
cd "$ROOT" || exit 2
# Collect tracked files under any local/recipes/**/source/ that git reports as
# changed. -z keeps paths with spaces intact (several KDE avatar assets have
# them, which is exactly where a naive `xargs` split goes wrong).
collect() { # $1 = status filter letter (M or D)
local want="$1"
git status --porcelain -z -- local/recipes 2>/dev/null \
| while IFS= read -r -d '' entry; do
local st="${entry:0:2}" path="${entry:3}"
case "$st" in
*"$want"*) [[ "$path" == */source/* ]] && printf '%s\n' "$path" ;;
esac
done
}
mapfile -t DELETED < <(collect D | LC_ALL=C sort)
mapfile -t MODIFIED < <(collect M | LC_ALL=C sort)
if [ "$MODE" = list ]; then
printf '%s\n' "${MODIFIED[@]}" | sed '/^$/d'
exit 0
fi
if [ "$MODE" = update ]; then
{
echo "# tracked-source-baseline.txt"
echo "#"
echo "# Tracked files under local/recipes/*/source/ that are EXPECTED to differ"
echo "# from HEAD during a normal build -- almost all of it recipe-sed churn."
echo "# Consumed by local/scripts/verify-tracked-sources.sh."
echo "#"
echo "# A file listed here is uncommitted work sitting in a DURABLE tree. That is"
echo "# tolerated, not endorsed: commit it, or convert the change into a recipe sed"
echo "# or a patch, so a clean re-extract cannot lose it."
echo "#"
echo "# A line prefixed 'D ' records an INTENTIONAL deletion of a tracked source"
echo "# file (e.g. removing a vendored stub). Without an entry, any deletion is"
echo "# treated as damage."
echo "#"
echo "# Regenerate with: ./local/scripts/verify-tracked-sources.sh --update"
printf '%s\n' "${MODIFIED[@]}" | sed '/^$/d'
printf 'D %s\n' "${DELETED[@]}" | sed '/^D $/d'
} > "$BASELINE"
echo ">>> baseline updated: $(printf '%s\n' "${MODIFIED[@]}" | sed '/^$/d' | wc -l) modified," \
"$(printf '%s\n' "${DELETED[@]}" | sed '/^$/d' | wc -l) deleted -> $BASELINE"
exit 0
fi
# ---- check mode ----------------------------------------------------------
rc=0
if [ "${REDBEAR_SKIP_TRACKED_SOURCE_CHECK:-0}" = "1" ]; then
echo ">>> WARNING: tracked-source integrity check bypassed via REDBEAR_SKIP_TRACKED_SOURCE_CHECK=1." >&2
exit 0
fi
# Load the baseline first: it records both expected modifications (plain lines)
# and intentional deletions ("D <path>").
declare -A known=()
declare -A known_del=()
if [ -f "$BASELINE" ]; then
while IFS= read -r line; do
[[ -z "$line" || "$line" == \#* ]] && continue
if [[ "$line" == "D "* ]]; then
known_del["${line:2}"]=1
else
known["$line"]=1
fi
done < "$BASELINE"
fi
# 1. Deletions are damage unless explicitly recorded as intentional.
unexpected_del=()
for f in "${DELETED[@]}"; do
[ -z "$f" ] && continue
[ -n "${known_del[$f]:-}" ] || unexpected_del+=("$f")
done
DELETED=("${unexpected_del[@]}")
if [ "${#DELETED[@]}" -gt 0 ] && [ -n "${DELETED[0]:-}" ]; then
echo "" >&2
echo ">>> ERROR: ${#DELETED[@]} git-tracked file(s) under a vendored source/ tree are DELETED." >&2
echo " A build never legitimately deletes tracked source. This is the signature of an" >&2
echo " ad-hoc 'rm -rf <recipe>/source'." >&2
printf ' %s\n' "${DELETED[@]:0:15}" >&2
[ "${#DELETED[@]}" -gt 15 ] && echo " ... and $(( ${#DELETED[@]} - 15 )) more" >&2
echo "" >&2
echo " Restore with: git checkout -- local/recipes/<recipe>/source" >&2
echo " To force a clean re-extract of a tracked tree, restore it from git afterwards;" >&2
echo " never delete it." >&2
rc=1
fi
# 2. Modifications the baseline does not account for.
unexpected=()
for f in "${MODIFIED[@]}"; do
[ -z "$f" ] && continue
[ -n "${known[$f]:-}" ] || unexpected+=("$f")
done
if [ "${#unexpected[@]}" -gt 0 ]; then
echo "" >&2
echo ">>> ERROR: ${#unexpected[@]} tracked source file(s) changed with no baseline entry." >&2
echo " Either a recipe gained a new sed, or committed Red Bear work was overwritten" >&2
echo " (e.g. a pristine tarball re-extract replacing baked-in fixes)." >&2
printf ' %s\n' "${unexpected[@]:0:15}" >&2
[ "${#unexpected[@]}" -gt 15 ] && echo " ... and $(( ${#unexpected[@]} - 15 )) more" >&2
echo "" >&2
echo " Inspect: git diff -- <path>" >&2
echo " If the change is LOST WORK: git checkout -- <path>" >&2
echo " If the change is EXPECTED: ./local/scripts/verify-tracked-sources.sh --update" >&2
rc=1
fi
# 3. Baseline churn: uncommitted work in a durable tree. Visible, not fatal by
# default -- this is the local/recipes/ counterpart of the local/sources/
# dirty gate in build-redbear.sh.
baseline_dirty=0
for f in "${MODIFIED[@]}"; do
[ -z "$f" ] && continue
[ -n "${known[$f]:-}" ] && baseline_dirty=$(( baseline_dirty + 1 ))
done
# 3a. FIRST-PARTY source is always fatal, never a note.
#
# redbear-* recipes are not vendored upstream code -- they are Red Bear's own
# programs and exist NOWHERE ELSE. A vendored tree can be restored from its
# upstream tarball or git remote; first-party source cannot. If a recipe sed or
# an `rm` damages it and the damage is committed, the work is simply gone.
#
# This is not hypothetical: seven redbear-* recipes rewrite their own source
# during the build (redbear-greeter, -btusb, -btctl, -ime, -dnsd,
# -accessibility, -keymapd), and all seven are exempt from out-of-tree staging
# because their cargo manifests carry path dependencies that escape the source
# tree. They are therefore the LEAST protected and the MOST irreplaceable code
# in the repository, so uncommitted drift in them stops the build outright
# rather than printing a note that scrolls past.
# FIRST-PARTY detection, DERIVED -- deliberately not a hardcoded list.
#
# A name list was the first attempt (redbear-*, then cub, then tlc) and it was
# wrong in principle: protection that depends on someone remembering to edit
# this file is not protection. Every new internal program would start out
# unguarded, and the omission is invisible until the code is already lost.
# `tlc` proved the point -- it is first-party, it is exempt from out-of-tree
# staging, and nothing but this gate protects it, yet it matched no pattern.
#
# The real property is structural: OUR code has no upstream to restore from.
# So a recipe is first-party when its recipe.toml records no fetchable origin
# -- no `tar =`, no `git =`, and no upstream URL. That currently identifies ~96
# recipes, against the 3 the list covered.
#
# The heuristic ERRS TOWARD first-party on purpose. A vendored recipe that
# records its origin only in prose gets classified as ours, which merely makes
# drift fatal instead of a warning -- conservative. The opposite mistake, a
# first-party recipe treated as vendored, is the one that loses irreplaceable
# work, so the default must fail in this direction.
redbear_is_firstparty() {
local path="$1" dir
dir="$(dirname "$path")"
# Walk up to the recipe root (the directory holding recipe.toml).
while [ "$dir" != "." ] && [ "$dir" != "/" ] && [ ! -f "$dir/recipe.toml" ]; do
dir="$(dirname "$dir")"
done
[ -f "$dir/recipe.toml" ] || return 1
# Fetchable origin keys.
grep -qE '^[[:space:]]*(tar|git)[[:space:]]*=' "$dir/recipe.toml" && return 1
# Provenance recorded in prose by a vendored fork. Case-INSENSITIVE and not
# restricted to .tar/.git URLs: brush writes
# # Upstream: https://github.com/reubeno/brush
# which a case-sensitive `upstream:` + \.(tar|git) match missed on both
# counts, misfiling a genuine fork as first-party.
#
# Matched narrowly -- an explicit `upstream:`/`snapshot:` label followed by a
# URL -- NOT any bare URL in a comment. A stray bug-tracker link must never
# be read as provenance, because that error direction (first-party treated
# as vendored) is the one that loses irreplaceable work.
grep -qiE '^[[:space:]]*#?[[:space:]]*(upstream|snapshot|origin)[[:space:]]*:[[:space:]]*(https?://|[A-Za-z0-9_.-]+/)' \
"$dir/recipe.toml" && return 1
return 0
}
firstparty_dirty=0
for f in "${MODIFIED[@]}" "${DELETED[@]}"; do
[ -z "$f" ] && continue
redbear_is_firstparty "$f" && firstparty_dirty=$(( firstparty_dirty + 1 ))
done
if [ "$firstparty_dirty" -gt 0 ]; then
echo ">>> ERROR: $firstparty_dirty uncommitted change(s) in FIRST-PARTY source (redbear-*, cub, tlc)." >&2
echo " This code exists nowhere but this project -- there is no upstream to restore" >&2
echo " from. Commit it, or revert it with: git checkout -- <path>" >&2
for f in "${MODIFIED[@]}" "${DELETED[@]}"; do
redbear_is_firstparty "$f" && echo " $f" >&2
done
echo " Override (accepts the risk): REDBEAR_ALLOW_DIRTY_FIRSTPARTY=1" >&2
[ "${REDBEAR_ALLOW_DIRTY_FIRSTPARTY:-0}" = "1" ] || rc=1
fi
if [ "$baseline_dirty" -gt 0 ]; then
if [ "${REDBEAR_STRICT_TRACKED_SOURCES:-0}" = "1" ]; then
echo ">>> ERROR: $baseline_dirty uncommitted change(s) in tracked source trees" \
"(REDBEAR_STRICT_TRACKED_SOURCES=1)." >&2
rc=1
else
say ">>> Preflight note: $baseline_dirty uncommitted change(s) in tracked vendored" \
"source trees (baseline-known)."
say " These live in a DURABLE tree but are not committed; a clean re-extract would" \
"lose them. Commit them, or move them into a recipe sed / patch."
fi
fi
if [ "$rc" -eq 0 ]; then
say ">>> Preflight: tracked vendored source trees intact (no deletions, no unexplained edits)."
fi
exit "$rc"