Files
RedBear-OS/local/scripts/build-preflight.sh
T
vasilito cb424d7448 build: static patch-sanity linter (shift-left the malformed-patch class)
verify-patch-sanity.py validates every active recipe .patch has internally-
consistent hunk line counts — catching the 'malformed patch at line N' failure
at commit/CI/preflight time instead of hours into a cook. This cycle hit that
class three times (qtwaylandscanner, sddm, xwayland), each only discovered when
cookbook tried to apply the patch.

Running it across the repo found 29 latent malformed patches (validated against
GNU patch: e.g. relibc/P3-sysv-ipc reproduces 'malformed patch at line 22').
They were harmless only because they sit in vendored recipes (baked, not re-
applied) — but would fail on any version-bump re-derivation. --fix recounts the
hunk headers (body untouched) and repaired all 29.

Wired into build-preflight.sh (Phase 1.0D) and redbear-ci.yml, with a unit test
(test-patch-sanity.sh). Skips archived/legacy trees and unvalidatable formats
(empty placeholders, bare-@@ git hunks).
2026-08-01 05:13:02 +03:00

311 lines
14 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
CONFIG="redbear-full"
RELEASE="${REDBEAR_RELEASE:-}"
STRICT_DURABILITY="${REDBEAR_STRICT_DURABILITY:-0}"
STRICT_METADATA="${REDBEAR_STRICT_METADATA:-0}"
EXTRA_PACKAGES=()
usage() {
cat <<EOF
Usage: $(basename "$0") [--config=<name>] [--release=<ver>] [--strict-durability] [--strict-metadata] [--extra-package=<pkg> ...]
EOF
}
while [ $# -gt 0 ]; do
case "$1" in
--config=*) CONFIG="${1#*=}" ;;
--release=*) RELEASE="${1#*=}" ;;
--strict-durability) STRICT_DURABILITY=1 ;;
--strict-metadata) STRICT_METADATA=1 ;;
--extra-package=*) EXTRA_PACKAGES+=("${1#*=}") ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown: $1" >&2; usage >&2; exit 1 ;;
esac
shift
done
cd "$PROJECT_ROOT"
echo ">>> Build preflight: $CONFIG"
# Defensive: detect and repair broken recipe.toml symlinks.
# These are tracked files — broken symlinks mean corruption from unknown source.
# Check both local/recipes/ (regular files) and recipes/ (tracked symlinks).
broken_recipes=$(find local/recipes recipes -name "recipe.toml" -xtype l 2>/dev/null | wc -l)
if [ "$broken_recipes" -gt 0 ]; then
echo ">>> WARNING: $broken_recipes broken recipe.toml symlinks detected — auto-repairing from git..." >&2
find local/recipes recipes -name "recipe.toml" -xtype l -exec git checkout -q -- {} \; 2>/dev/null
remaining=$(find local/recipes recipes -name "recipe.toml" -xtype l 2>/dev/null | wc -l)
if [ "$remaining" -gt 0 ] && [ -x "$SCRIPT_DIR/guard-recipes.sh" ]; then
# `git checkout` only restores TRACKED links. Untracked/generated links
# (custom local/recipes symlinks that were never committed) must be
# regenerated by the durability guard, which recomputes correct relative
# targets from local/recipes/.
echo ">>> $remaining link(s) not restorable from git — regenerating via guard-recipes.sh --fix..." >&2
"$SCRIPT_DIR/guard-recipes.sh" --fix >/dev/null 2>&1 || true
remaining=$(find local/recipes recipes -name "recipe.toml" -xtype l 2>/dev/null | wc -l)
fi
if [ "$remaining" -gt 0 ]; then
echo ">>> ERROR: $remaining recipe.toml files could not be restored. Build may fail." >&2
else
echo ">>> Repaired: all recipe.toml symlinks restored."
fi
fi
if [ -x "$SCRIPT_DIR/verify-overlay-integrity.sh" ]; then
if ! "$SCRIPT_DIR/verify-overlay-integrity.sh" --quiet; then
echo ">>> Preflight note: overlay integrity script reported legacy issues; continuing with build-focused checks."
fi
fi
# Enforce the "no fake version label" rule (local/AGENTS.md):
# every Cat 2 fork's `<X.Y.Z>-rb<B.B.B>` version must match the source
# content from upstream `<X.Y.Z>` + Red Bear patches, and the `-rb<B.B.B>`
# suffix must match the current Red Bear OS git branch.
if [ -x "$SCRIPT_DIR/verify-fork-versions.sh" ]; then
if ! "$SCRIPT_DIR/verify-fork-versions.sh" >/tmp/fork-verify.out 2>&1; then
if [ "${REDBEAR_SKIP_FORK_VERIFY:-0}" != "1" ]; then
cat /tmp/fork-verify.out >&2
echo "ERROR: fork version verification failed." >&2
echo " Set REDBEAR_SKIP_FORK_VERIFY=1 to bypass (DANGEROUS, only for emergency)." >&2
exit 1
fi
echo ">>> Preflight note: fork version verification bypassed via REDBEAR_SKIP_FORK_VERIFY=1." >&2
else
echo ">>> Preflight: fork versions verified against upstream."
fi
fi
if [ -x "$SCRIPT_DIR/check-fork-drift.sh" ] && [ "${REDBEAR_SKIP_DRIFT_CHECK:-0}" != "1" ]; then
if ! "$SCRIPT_DIR/check-fork-drift.sh" --no-fetch --threshold=50 >/tmp/fork-drift.out 2>&1; then
echo ">>> WARNING: Upstream drift detected in local forks:" >&2
grep -E 'DRIFT' /tmp/fork-drift.out >&2 || cat /tmp/fork-drift.out >&2
echo ">>> Run ./local/scripts/check-fork-drift.sh for full report." >&2
echo ">>> Run ./local/scripts/upgrade-forks.sh to upgrade." >&2
echo ">>> Set REDBEAR_SKIP_DRIFT_CHECK=1 to suppress this warning." >&2
else
echo ">>> Preflight: fork drift check passed."
fi
fi
if [ -x "$SCRIPT_DIR/verify-fork-functions.sh" ] && [ "${REDBEAR_SKIP_FUNCTION_CHECK:-0}" != "1" ]; then
if ! "$SCRIPT_DIR/verify-fork-functions.sh" --no-fetch --quiet >/tmp/fork-functions.out 2>&1; then
cat /tmp/fork-functions.out >&2
echo ">>> ERROR: Fork function verification failed — upstream functions are missing." >&2
echo ">>> This indicates a bad merge/cherry-pick dropped upstream code silently." >&2
echo ">>> Fix: ./local/scripts/upgrade-forks.sh --no-fetch --force-reapply <fork>" >&2
if [ "${REDBEAR_SKIP_FORK_VERIFY:-0}" != "1" ]; then
exit 1
fi
echo ">>> WARNING: Bypassed via REDBEAR_SKIP_FORK_VERIFY=1." >&2
else
echo ">>> Preflight: fork function verification passed."
fi
fi
# Phase 1.0C: external tar-recipe version consistency.
# The local/sources/ forks are guarded above; this guards the OTHER fork family:
# tar recipes that vendor their upstream source/ in git while also declaring a
# [source] tar=/blake3. A version bump that rewrites recipe metadata but never
# propagates into the vendored source/ (or a stale cached source.tar) makes the
# build silently compile the wrong version (the Qt 6.11.0/6.11.1 and KDE
# 6.10.0/6.28.0 divergences). Fix: ./local/scripts/sync-recipe-source.sh <recipe>
if [ -x "$SCRIPT_DIR/verify-external-source-versions.sh" ] && [ "${REDBEAR_SKIP_EXTERNAL_SOURCE_CHECK:-0}" != "1" ]; then
if ! "$SCRIPT_DIR/verify-external-source-versions.sh" --quiet >/tmp/ext-source.out 2>&1; then
cat /tmp/ext-source.out >&2
echo ">>> ERROR: external tar-recipe source version divergence (recipe vs vendored source/)." >&2
echo ">>> Fix: ./local/scripts/sync-recipe-source.sh --commit <recipe> (propagates the bump)" >&2
if [ "${REDBEAR_SKIP_FORK_VERIFY:-0}" != "1" ]; then
exit 1
fi
echo ">>> WARNING: Bypassed via REDBEAR_SKIP_FORK_VERIFY=1." >&2
else
echo ">>> Preflight: external tar-recipe source versions consistent."
fi
fi
# Phase 1.0D: static patch-sanity. Validates that every active recipe .patch has
# internally-consistent hunk line counts, catching the "malformed patch at line N"
# class at preflight instead of hours into a cook (hit 3x this cycle:
# qtwaylandscanner/sddm/xwayland). Fix: ./local/scripts/verify-patch-sanity.py --fix
if [ -x "$SCRIPT_DIR/verify-patch-sanity.py" ] || [ -f "$SCRIPT_DIR/verify-patch-sanity.py" ]; then
if ! python3 "$SCRIPT_DIR/verify-patch-sanity.py" --quiet >/tmp/patch-sanity.out 2>&1; then
cat /tmp/patch-sanity.out >&2
echo ">>> ERROR: malformed recipe patch(es). Fix: ./local/scripts/verify-patch-sanity.py --fix" >&2
if [ "${REDBEAR_SKIP_FORK_VERIFY:-0}" != "1" ]; then exit 1; fi
echo ">>> WARNING: Bypassed via REDBEAR_SKIP_FORK_VERIFY=1." >&2
else
echo ">>> Preflight: recipe patches are well-formed."
fi
fi
# Phase 1.0B: enforce patch-presence invariant. Every patch in
# local/patches/<comp>/ MUST have its content reflected as commits in
# local/sources/<comp>/'s working tree, or the next fork reset will
# silently lose that work. See local/docs/legacy-obsolete-2026-07-25/PATCH-PRESERVATION-AUDIT-2026-07-12.md.
# Default is warn-only; --strict was decided against because current state
# is mid-recovery and we don't want to block every operator build.
if [ -x "$SCRIPT_DIR/verify-patch-content.sh" ] && [ "${REDBEAR_SKIP_PATCH_CONTENT_CHECK:-0}" != "1" ]; then
if ! "$SCRIPT_DIR/verify-patch-content.sh" >/tmp/patch-content.out 2>&1; then
cat /tmp/patch-content.out >&2
echo ">>> WARNING: Orphaned patches detected (Phase 1.0B). Build proceeding but Red Bear" >&2
echo ">>> work in orphaned patches is NOT in the fork. See audit doc for recovery." >&2
echo ">>> Set REDBEAR_SKIP_PATCH_CONTENT_CHECK=1 to suppress this warning." >&2
echo ">>> Run ./local/scripts/verify-patch-content.sh --strict for exit-1 enforcement." >&2
fi
fi
# Phase 4.2: collision detection — config [[files]] vs recipe installs.
# The original AGENTS.md "Collision Implications" claim that runtime
# collision detection exists was Phase 0 audit-disproven. This implements
# the GENERAL variant (config-vs-package) at pre-flight time so the
# silent-collision class of bug surfaces before install_packages().
if [ -x "$SCRIPT_DIR/verify-collision-detection.py" ] && [ "${REDBEAR_SKIP_COLLISION_CHECK:-0}" != "1" ]; then
if ! python3 "$SCRIPT_DIR/verify-collision-detection.py" >/tmp/collision.out 2>&1; then
cat /tmp/collision.out >&2
echo ">>> ERROR: Package-vs-config collision detected. Refusing to proceed." >&2
echo ">>> Set REDBEAR_SKIP_COLLISION_CHECK=1 to bypass (DANGEROUS)." >&2
else
echo ">>> Preflight: collision detection passed (config [[files]] vs recipe installs)."
fi
fi
if [ -n "$RELEASE" ]; then
bash "$SCRIPT_DIR/build-release-mode.sh" --release="$RELEASE" --config="$CONFIG" "${EXTRA_PACKAGES[@]/#/--extra-package=}"
fi
python3 "$SCRIPT_DIR/validate-source-trees.py" "$CONFIG" "${EXTRA_PACKAGES[@]/#/--extra-package=}"
python3 - "$PROJECT_ROOT" "$CONFIG" "$STRICT_METADATA" "${EXTRA_PACKAGES[@]}" <<'PY'
import sys
import tomllib
from pathlib import Path
project_root = Path(sys.argv[1])
config_name = sys.argv[2]
strict_metadata = sys.argv[3] == "1"
extra_packages = sys.argv[4:]
def build_lookup():
lookup = {}
for root in (project_root / "recipes", project_root / "local/recipes"):
for recipe_toml in root.rglob("recipe.toml"):
if not recipe_toml.exists():
continue
parts = recipe_toml.parts
if "source" in parts or "target" in parts:
continue
package_name = recipe_toml.parent.name
lookup.setdefault(package_name, recipe_toml)
return lookup
def resolve_config(config_path, visited=None):
if visited is None:
visited = set()
config_path = config_path.resolve()
if config_path in visited:
return {}
visited.add(config_path)
config = tomllib.loads(config_path.read_text())
packages = dict(config.get("packages", {}))
for include in config.get("include", []):
include_path = config_path.parent / include
if include_path.exists():
included = resolve_config(include_path, visited)
for name, value in packages.items():
included[name] = value
packages = included
return packages
lookup = build_lookup()
config_path = project_root / "config" / f"{config_name}.toml"
requested = resolve_config(config_path)
for pkg in extra_packages:
requested.setdefault(pkg, {})
known_package_paths = {
name for name in lookup
if not name.startswith("host:")
}
errors = []
warnings = []
for package_name, package_conf in sorted(requested.items()):
if str(package_conf) == "ignore" or package_name in {"libgcc", "libstdcxx"}:
continue
recipe_toml = lookup.get(package_name)
if recipe_toml is None:
continue
recipe = tomllib.loads(recipe_toml.read_text())
source = recipe.get("source", {})
rel = recipe_toml.relative_to(project_root).as_posix()
is_wip_or_local = rel.startswith("recipes/wip/") or rel.startswith("local/recipes/")
if isinstance(source, dict) and "tar" in source and "blake3" not in source:
msg = f"missing blake3 for tar recipe: {recipe_toml.relative_to(project_root)}"
(errors if strict_metadata and not is_wip_or_local else warnings).append(msg)
for patch in source.get("patches", []):
patch_path = (recipe_toml.parent / patch).resolve()
if not patch_path.exists():
msg = f"missing patch file: {patch} for {recipe_toml.relative_to(project_root)}"
(errors if strict_metadata else warnings).append(msg)
build = recipe.get("build", {})
script = build.get("script", "") if isinstance(build, dict) else ""
if script:
for dep in known_package_paths:
bad = f"/tmp/{dep}/"
if bad in script:
msg = (
f"recipe {recipe_toml.relative_to(project_root)} references {bad} "
f"in its build script. The cookbook does not stage host dev-dependencies "
f"under /tmp/<name>; use ${{COOKBOOK_TOOLCHAIN}} (the cross recipe's "
f"toolchain directory populated from dev-dependencies) instead."
)
(errors if strict_metadata else warnings).append(msg)
for warning in warnings:
print(f"WARN: {warning}", file=sys.stderr)
if errors:
for error in errors:
print(f"ERROR: {error}", file=sys.stderr)
raise SystemExit(1)
PY
if [ -x "$SCRIPT_DIR/classify-patch-state.py" ]; then
python3 "$SCRIPT_DIR/classify-patch-state.py"
fi
if [ -x "$SCRIPT_DIR/verify-durable-source-edits.py" ]; then
args=()
if [ "$STRICT_DURABILITY" = "1" ]; then
args+=(--strict)
fi
python3 "$SCRIPT_DIR/verify-durable-source-edits.py" "${args[@]}"
fi
if [ "$CONFIG" = "redbear-full" ]; then
relibc_include="$(PROJECT_ROOT="$PROJECT_ROOT" bash -c 'source "$0"; redbear_relibc_stage_include_dir' "$SCRIPT_DIR/lib/relibc-surface.sh")"
relibc_lib_dir="$(PROJECT_ROOT="$PROJECT_ROOT" bash -c 'source "$0"; redbear_relibc_stage_lib_dir' "$SCRIPT_DIR/lib/relibc-surface.sh")"
if [ -d "$relibc_include" ] && [ -f "$relibc_lib_dir/libc.so" ]; then
for hdr in sys/signalfd.h sys/timerfd.h sys/eventfd.h threads.h; do
if [ ! -f "$relibc_include/$hdr" ]; then
echo ">>> Preflight note: relibc staged surface missing $hdr; top-level build should refresh/sync it before compilation."
fi
done
if ! grep -q 'strtold' "$relibc_include/stdlib.h" 2>/dev/null; then
echo ">>> Preflight note: relibc staged stdlib.h missing strtold declaration; top-level build should refresh/sync it before compilation."
fi
if ! readelf -Ws "$relibc_lib_dir/libc.so" | grep -q '_Z7strtoldPKcPPc'; then
echo ">>> Preflight note: relibc staged libc.so missing C++ strtold compatibility export; top-level build should refresh/sync it before compilation."
fi
else
echo ">>> Preflight note: relibc staged surface not present yet; build will refresh it if needed."
fi
fi
echo ">>> Build preflight passed"