Files
RedBear-OS/local/scripts/verify-tracked-sources.sh
T
vasilito d1bc24d61e verify-tracked-sources: cub and tlc are first-party too
Extends the fatal first-party check beyond redbear-* to cub (system) and tlc
(tui). Both are Red Bear's own programs with no upstream anywhere, so the
"restore it from the tarball" recovery that makes vendored drift a warning does
not exist for them.

They sit on opposite sides of the staging boundary, which is why both need the
gate for different reasons:
  cub  is staged out of tree (no escaping cargo path deps), so staging already
       keeps recipe seds off the tracked copy.
  tlc  is EXEMPT from staging: its manifest has a path dependency escaping the
       source tree, so nothing keeps a sed off the real files. For tlc this
       gate is the only protection.

The check is now a redbear_is_firstparty() helper rather than an inline glob, so
adding the next internal program is one line.

Verified: silent on a clean tree; fires on a one-line edit to cub and to tlc;
both restore cleanly.
2026-08-04 19:33:55 +03:00

273 lines
12 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 recipes. Everything here is Red Bear's own code with no upstream
# anywhere. Add new internal programs to this list -- if it is ours and it is
# not a fork of something external, it belongs here.
# redbear-* the redbear-prefixed daemons, drivers and tools
# cub internal
# tlc internal
# Note the two shapes: `cub` is staged out of tree (no escaping path deps, so
# staging protects it), while `tlc` is exempt because its cargo manifest has a
# path dependency that escapes its source tree -- so for tlc this gate is the
# ONLY thing standing between a bad sed and unrecoverable loss.
redbear_is_firstparty() {
case "$1" in
*/redbear-*/source/*) return 0 ;;
*/cub/source/*) return 0 ;;
*/tlc/source/*) return 0 ;;
esac
return 1
}
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"