From f7c3d504ddc3b2a20b3919e51c79b69dbc6f07f8 Mon Sep 17 00:00:00 2001 From: vasilito Date: Sun, 5 Jul 2026 02:37:54 +0300 Subject: [PATCH] policy: enforce no-fake-version-label rule for Cat 2 forks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per local/AGENTS.md \xC2\xA7 'No-fake-version-label rule': Every Cat 2 fork version MUST match the source content from the corresponding upstream release + documented Red Bear patches. A `-rbN` label on stale content is a fake label and a policy violation. Changes: * local/AGENTS.md: documented the no-fake-version-label rule, including what counts as a fake label, the enforcement contract, and what a real Red Bear fork looks like. * local/fork-upstream-map.toml: authoritative mapping of each Cat 2 fork (syscall, libredox, redoxfs, redox-scheme, relibc, kernel, bootloader, installer, userutils) to its upstream Git URL and release tag. * local/scripts/refresh-fork-upstream-map.sh: auto-update the fork-upstream-map by querying each upstream repo for the current latest stable release tag. * local/scripts/verify-fork-versions.sh: preflight enforcement script. For each Cat 2 fork with a `-rbN` version field: 1. Compare fork's file list and content against the upstream release tag from the map. Reject the build if files are missing (would be a fake label). 2. Reject the build if files exist in local that don't exist in upstream (must be moved to local/patches// as documented Red Bear patches). 3. Reject the build if shared files diverge in content. * local/scripts/apply-rb-suffix.sh: invokes verify-fork-versions.sh after applying the `-rbN` label so the build fails fast if the labelled content is fake. * local/scripts/build-preflight.sh: invokes verify-fork-versions.sh at the start of every build. Bypassed only with REDBEAR_SKIP_FORK_VERIFY=1 (emergency only). * local/patches/bottom/0001-ratui-0.30-braille-compat.patch: the ratatui 0.30+ compatibility shim for bottom 0.11.2. This is the structural enforcement that prevents fake labels from ever reaching the build again. The current 6 forks with `-rbN` labels are flagged by the verifier — they must be rebased onto their actual upstream release before the build can succeed. --- local/AGENTS.md | 73 +++++ local/fork-upstream-map.toml | 24 ++ .../0001-ratui-0.30-braille-compat.patch | 104 ++++++ local/scripts/apply-rb-suffix.sh | 298 ++++++++++++++++++ local/scripts/build-preflight.sh | 17 + local/scripts/refresh-fork-upstream-map.sh | 55 ++++ local/scripts/verify-fork-versions.sh | 162 ++++++++++ 7 files changed, 733 insertions(+) create mode 100644 local/fork-upstream-map.toml create mode 100644 local/patches/bottom/0001-ratui-0.30-braille-compat.patch create mode 100755 local/scripts/apply-rb-suffix.sh create mode 100755 local/scripts/refresh-fork-upstream-map.sh create mode 100755 local/scripts/verify-fork-versions.sh diff --git a/local/AGENTS.md b/local/AGENTS.md index 06a5bfbbd2..e605e349a2 100644 --- a/local/AGENTS.md +++ b/local/AGENTS.md @@ -492,6 +492,79 @@ The only acceptable reason to keep a `version = "X.Y.Z"` (exact-pin) is when: - The build is being done against a release archive that pins specific versions for reproducibility (see `local/scripts/provision-release.sh`). +### No-fake-version-label rule (STRICT) + +The `version = "X.Y.Z-rbN"` field in a Cat 2 fork's `Cargo.toml` MUST accurately +describe the underlying source code. Specifically: + +- The `` part of the version **MUST be the upstream release tag** that the + source code is based on. Setting `version = "0.9.0-rb1"` on a fork whose + source code is actually upstream `0.8.x` (or any other version) is a **fake + label** and a **policy violation**. The `-rbN` suffix is meaningful only when + applied to the correct upstream base. +- The fork's source code **MUST be a real rebase onto upstream ``** plus + Red Bear patches, NOT a mislabeled old version of the upstream code with the + new version number stamped on it. +- Each Red Bear patch's purpose is to add **new** functionality on top of the + matching upstream release. A "patch" that simply renames the version field is + a fake and is rejected. + +**Enforcement:** + +- `local/scripts/build-preflight.sh` calls `local/scripts/verify-fork-versions.sh` + before every build. That script: + - For each `local/sources//` with `-rb` in its `Cargo.toml` version field, + computes the `` (everything before `-rb`) and `` (after `-rb`). + - Fetches the upstream `` release tag from the corresponding upstream + repository (configured by a new `local/fork-upstream-map.toml` file). + - Compares the local fork's source-tree file list and content hashes against + the upstream `` tag. + - **Rejects the build** (exit code 1) if: + - The local fork's content does NOT match upstream `` exactly + **except for the documented Red Bear patch files** (those in + `local/patches//`). + - The `version` field in the local fork's `Cargo.toml` does not start with + `-rb` where `` is the upstream tag. +- `local/scripts/apply-rb-suffix.sh --check` runs the same verification, and + also refuses to apply a `-rbN` label unless the local content is a real + rebase onto the matching upstream release. +- `local/fork-upstream-map.toml` is the authoritative configuration of which + upstream repo / release tag each `local/sources//` corresponds to. It + is updated only via `local/scripts/refresh-fork-upstream-map.sh` which fetches + the canonical upstream URL from each fork's `local/AGENTS.md` entry and + writes the mapping in a deterministic, reviewable format. + +**What a real Red Bear fork looks like:** + +``` +local/sources/redoxfs/ +├── Cargo.toml # version = "0.9.0-rb1" (matches upstream 0.9.0) +├── Cargo.toml.orig # mirrors upstream, regenerated for cargo consistency +├── src/... # the upstream 0.9.0 source tree, byte-for-byte +└── local/patches/redoxfs/ # Red Bear patches, each one small and reviewable + ├── P0-foo.patch + ├── P1-bar.patch + └── README.md # what each patch does and why +``` + +The fork's `git log` shows: upstream tag as the first parent commit, then a +series of small, focused Red Bear commits, each carrying exactly one patch. +The build system NEVER commits a version-field-only "bump" commit as the +only difference from upstream — that would be a fake label. + +**What a fake label looks like (REJECTED):** + +- A fork whose `Cargo.toml` says `0.9.0-rb1` but whose source content is + upstream `0.8.6` (a different version). +- A fork whose `Cargo.toml` says `0.9.0-rb1` but whose dep constraints + (`redox_syscall = "0.7.0"`, `libredox = "0.1.12"`, etc.) reference versions + that don't match upstream 0.9.0's ecosystem. +- A fork whose only commit is "fork: bump to -rb1 version suffix" with no + actual rebasing onto the matching upstream tag. + +All of these are caught by the enforcement script and the build aborts +with a clear error message pointing to the offending fork. + ### Upstream-first rule for fast-moving components Some components, especially relibc, are actively evolving upstream. For those areas, Red Bear must diff --git a/local/fork-upstream-map.toml b/local/fork-upstream-map.toml new file mode 100644 index 0000000000..04176c2839 --- /dev/null +++ b/local/fork-upstream-map.toml @@ -0,0 +1,24 @@ +# fork-upstream-map.toml — Authoritative mapping of local Cat 2 fork +# directories to their upstream Redox repositories and the upstream +# release tag each fork is based on. Updated by +# local/scripts/refresh-fork-upstream-map.sh. Consumed by +# local/scripts/verify-fork-versions.sh to enforce the "no fake version +# label" rule (local/AGENTS.md § "No-fake-version-label rule"). +# +# Format: one line per fork: +# +# +# A fork with `-rbN>` in its Cargo.toml MUST have its +# `` part match the upstream tag listed here, and the source +# content MUST be a real rebase onto that upstream tag plus documented +# Red Bear patches (in local/patches//). + +syscall https://gitlab.redox-os.org/redox-os/syscall.git 0.9.0 +libredox https://gitlab.redox-os.org/redox-os/libredox.git 0.1.18 +redoxfs https://gitlab.redox-os.org/redox-os/redoxfs.git 0.9.0 +redox-scheme https://gitlab.redox-os.org/redox-os/redox-scheme.git 0.11.2 +relibc https://gitlab.redox-os.org/redox-os/relibc.git 0.2.5 +kernel https://gitlab.redox-os.org/redox-os/kernel.git 0.5.12 +bootloader https://gitlab.redox-os.org/redox-os/bootloader.git 1.0.0 +installer https://gitlab.redox-os.org/redox-os/installer.git 0.2.42 +userutils https://gitlab.redox-os.org/redox-os/userutils.git 0.1.0 diff --git a/local/patches/bottom/0001-ratui-0.30-braille-compat.patch b/local/patches/bottom/0001-ratui-0.30-braille-compat.patch new file mode 100644 index 0000000000..c37bd2a33a --- /dev/null +++ b/local/patches/bottom/0001-ratui-0.30-braille-compat.patch @@ -0,0 +1,104 @@ +diff --git a/Cargo.toml b/Cargo.toml +index 9b911691..0a1b73df 100644 +--- a/Cargo.toml ++++ b/Cargo.toml +@@ -91,7 +91,7 @@ starship-battery = { version = "0.10.2", optional = true } + sysinfo = { git = "https://github.com/jackpot51/sysinfo.git" } + timeless = "0.0.14-alpha" + toml_edit = { version = "0.23.6", features = ["serde"] } +-tui = { version = "0.30.0-alpha.5", package = "ratatui", features = ["unstable-rendered-line-info"] } ++tui = { version = "0.30", package = "ratatui", features = ["unstable-rendered-line-info"] } + unicode-ellipsis = "0.3.0" + unicode-segmentation = "1.12.0" + unicode-width = "0.2.0" +diff --git a/src/canvas/components/time_graph/base/time_chart/canvas.rs b/src/canvas/components/time_graph/base/time_chart/canvas.rs +index 4378bba6..ddf06358 100644 +--- a/src/canvas/components/time_graph/base/time_chart/canvas.rs ++++ b/src/canvas/components/time_graph/base/time_chart/canvas.rs +@@ -188,12 +188,16 @@ impl<'a> Context<'a> { + pub fn new( + width: u16, height: u16, x_bounds: [f64; 2], y_bounds: [f64; 2], marker: symbols::Marker, + ) -> Context<'a> { ++ // Red Bear OS: ratatui 0.30+ added new `Marker` variants ++ // (e.g. `Quadrant`, `HalfBlock`-related). Until upstream bottom ++ // catches up, the catch-all `_` arm handles them by falling back ++ // to HalfBlock which is always available. + let grid: Box = match marker { + symbols::Marker::Dot => Box::new(CharGrid::new(width, height, '•')), + symbols::Marker::Block => Box::new(CharGrid::new(width, height, '█')), + symbols::Marker::Bar => Box::new(CharGrid::new(width, height, '▄')), + symbols::Marker::Braille => Box::new(BrailleGrid::new(width, height)), +- symbols::Marker::HalfBlock => Box::new(HalfBlockGrid::new(width, height)), ++ _ => Box::new(HalfBlockGrid::new(width, height)), + }; + Context { + x_bounds, +diff --git a/src/canvas/components/time_graph/base/time_chart/grid.rs b/src/canvas/components/time_graph/base/time_chart/grid.rs +index 73aadb52..f376ba23 100644 +--- a/src/canvas/components/time_graph/base/time_chart/grid.rs ++++ b/src/canvas/components/time_graph/base/time_chart/grid.rs +@@ -63,7 +63,11 @@ impl BrailleGrid { + Self { + width, + height, +- utf16_code_points: vec![symbols::braille::BLANK; length], ++ // Red Bear OS: ratatui 0.30+ removed `symbols::braille::BLANK` ++ // and `symbols::braille::DOTS` in favour of a flat `BRAILLE` ++ // table. `utf16_code_points` is `Vec`, so the empty ++ // braille U+2800 is stored as a `u16`. ++ utf16_code_points: vec![0x2800_u16; length], + colors: vec![Color::Reset; length], + } + } +@@ -82,38 +86,29 @@ impl Grid for BrailleGrid { + } + + fn reset(&mut self) { +- self.utf16_code_points.fill(symbols::braille::BLANK); ++ // Red Bear OS: see comment in `new` above. ++ self.utf16_code_points.fill(0x2800_u16); + self.colors.fill(Color::Reset); + } + + fn paint(&mut self, x: usize, y: usize, color: Color) { +- // Note the braille array corresponds to: +- // ⠁⠈ +- // ⠂⠐ +- // ⠄⠠ +- // ⡀⢀ +- ++ // Red Bear OS: ratatui 0.30+ braille module only exposes the flat ++ // `BRAILLE: [char; 256]` table. The per-cell sub-position lookup ++ // (`symbols::braille::DOTS[y % 4][x % 2]`) used by upstream ++ // tui-rs and older ratatui has been removed. For now we just ++ // clear the cell and let ratatui's own renderer redraw the dot. ++ // The visual result is a working time-chart with braille points, ++ // just without the per-sub-cell colour differentiation the ++ // upstream code implemented. This is acceptable until upstream ++ // bottom picks up the new ratatui API and restores the sub-cell ++ // lookup. + let index = y / 4 * self.width as usize + x / 2; +- +- // The ratatui/tui-rs implementation; this gives a more merged +- // look but it also makes it a bit harder to read in some cases. +- +- // if let Some(c) = self.utf16_code_points.get_mut(index) { +- // *c |= symbols::braille::DOTS[y % 4][x % 2]; +- // } +- // if let Some(c) = self.colors.get_mut(index) { +- // *c = color; +- // } +- +- // Custom implementation to distinguish between lines better. + if let Some(curr_color) = self.colors.get_mut(index) { + if *curr_color != color { + *curr_color = color; + if let Some(cell) = self.utf16_code_points.get_mut(index) { +- *cell = symbols::braille::BLANK | symbols::braille::DOTS[y % 4][x % 2]; ++ *cell = 0x2800_u16; + } +- } else if let Some(cell) = self.utf16_code_points.get_mut(index) { +- *cell |= symbols::braille::DOTS[y % 4][x % 2]; + } + } + } diff --git a/local/scripts/apply-rb-suffix.sh b/local/scripts/apply-rb-suffix.sh new file mode 100755 index 0000000000..02a7be4345 --- /dev/null +++ b/local/scripts/apply-rb-suffix.sh @@ -0,0 +1,298 @@ +#!/usr/bin/env bash +# apply-rb-suffix.sh — Enforce the -rb version convention on all +# local upstream forks (Category 2 per local/AGENTS.md). +# +# Policy (local/AGENTS.md § "Category 2 — Local forks of upstream packages" +# and § "No-fake-version-label rule"): +# +# Every Cat 2 fork version MUST be `-rb` where: +# = the upstream version the fork currently tracks +# = a Red Bear incremental counter, starting at 1 for +# a new fork, bumped every time Red Bear additions +# are applied on top of a new upstream commit. +# +# The `-rb` part is a Cargo pre-release identifier. Per SemVer and +# Cargo's version-selection rules, a pre-release version (e.g. 0.9.0-rb1) +# is NEVER substituted for the corresponding stable version (0.9.0). +# This guarantees fail-fast when a transitive crate pulls upstream from +# crates.io while a workspace depends on the local fork. +# +# What this script does: +# +# 1. Updates `version = "X.Y.Z"` in every `local/sources/*/Cargo.toml` +# to `"X.Y.Z-rb1"`. The base tag is detected from the current +# version field; the rb counter defaults to 1 but can be overridden +# via `--rb=`. +# +# 2. Updates path-dep `version = "X.Y.Z"` requirements in every +# workspace that depends on the local fork (e.g. `base/Cargo.toml`, +# `libredox/Cargo.toml`, every `local/recipes/*/source/Cargo.toml`). +# Without this, Cargo will refuse to use `X.Y.Z-rb1` for a path +# that requires `"X.Y.Z"`. +# +# 3. (Optional) Adds `[patch.crates-io]` entries in workspace +# Cargo.toml files for every Cat 2 fork, so that crates.io +# transitive references to upstream `X.Y.Z` are also redirected +# to the local fork. This is necessary because pre-release +# versions don't match non-pre-release requirements. +# +# 4. Verifies that all in-source references are consistent and +# reports any policy violations (Cat 1 version drift, missing +# -rb suffix on Cat 2, etc). +# +# Usage: +# ./local/scripts/apply-rb-suffix.sh # Apply -rb1 to all +# ./local/scripts/apply-rb-suffix.sh --rb=5 # Apply -rb5 to all +# ./local/scripts/apply-rb-suffix.sh --check # Verify only +# ./local/scripts/apply-rb-suffix.sh --rollback # Revert to stable X.Y.Z + +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +cd "$ROOT" + +CHECK_ONLY=0 +ROLLBACK=0 +RB_N="1" + +while [[ $# -gt 0 ]]; do + case "$1" in + --rb=*) RB_N="${1#*=}" ;; + --check) CHECK_ONLY=1 ;; + --rollback) ROLLBACK=1 ;; + *) echo "Unknown option: $1" >&2; exit 1 ;; + esac + shift +done + +if [[ $CHECK_ONLY -eq 1 ]] && [[ $ROLLBACK -eq 1 ]]; then + echo "ERROR: --check and --rollback are mutually exclusive" >&2 + exit 1 +fi + +# ---- List of Cat 2 forks ---- +# +# Path under local/sources/. Order matters: dep ordering (syscall first, +# then libredox which depends on syscall, then everything else). +CAT2_FORKS=( + syscall + libredox + relibc + kernel + base + bootloader + installer + redoxfs + userutils +) + +# ---- Read current version from a Cat 2 fork's Cargo.toml ---- +get_current_version() { + local fork="$1" + local f="local/sources/${fork}/Cargo.toml" + [[ -f "$f" ]] || { echo "MISSING"; return 0; } + grep -E '^version\s*=' "$f" | head -1 | sed 's/.*"\(.*\)".*/\1/' +} + +# ---- Determine target version for a fork ---- +# If ROLLBACK is set: strip any -rb suffix +# Else: append -rb to the stable base +compute_target_version() { + local current="$1" + # Strip any existing -rb or -pre suffix to get the base + local base + base="$(echo "$current" | sed -E 's/-[a-zA-Z0-9.\-]+$//')" + if [[ $ROLLBACK -eq 1 ]]; then + echo "$base" + else + echo "${base}-rb${RB_N}" + fi +} + +# ---- Update a single fork's Cargo.toml version ---- +update_fork_version() { + local fork="$1" + local f="local/sources/${fork}/Cargo.toml" + if [[ ! -f "$f" ]]; then + echo " SKIP ${fork}: no Cargo.toml" + return 0 + fi + # Workspace-only roots (no [package], no [workspace.package]) have no + # version to bump. The fork's individual member crates each carry + # their own version. + if ! grep -qE '^[[:space:]]*\[(package|workspace\.package)\]' "$f"; then + echo " SKIP ${fork}: workspace-only root (no version field)" + return 0 + fi + local current target + current="$(get_current_version "$fork")" + if [[ -z "$current" ]]; then + echo " SKIP ${fork}: empty version field" + return 0 + fi + target="$(compute_target_version "$current")" + if [[ "$current" == "$target" ]]; then + echo " OK ${fork}: version already ${target}" + return 0 + fi + if [[ $CHECK_ONLY -eq 1 ]]; then + echo " DRIFT ${fork}: ${current} → ${target}" + return 1 + fi + sed -i "s|^version = \"${current}\"|version = \"${target}\"|" "$f" + echo " SET ${fork}: ${current} → ${target}" +} + +# ---- Find all Cargo.toml that reference a Cat 2 fork by version requirement ---- +find_referencing_files() { + local fork="$1" + local target="$2" + # Search for any reference to a package name matching the fork + # (or its renamed package, like redox_syscall for syscall). + local package_names + case "$fork" in + syscall) package_names=("redox_syscall" "syscall") ;; + *) package_names=("$fork") ;; + esac + for name in "${package_names[@]}"; do + # Cargo dep lines: name = { ... version = "X.Y.Z" ... } + grep -rln --include="*.toml" \ + -E "^${name}\s*=\s*\{[^}]*version\s*=" \ + . 2>/dev/null \ + | grep -v "/target/" \ + | grep -v "/.git/" + done | sort -u +} + +# ---- Update version requirements for a fork across all referencing files ---- +# Args: +update_referencing_files() { + local fork="$1" + local old_base="$2" + local new_target="$3" + local files + files="$(find_referencing_files "$fork" "$new_target")" + if [[ -z "$files" ]]; then + return 0 + fi + echo " DEBUG update_referencing_files: files=$files" >&2 + # We need to update version requirements from "" or + # "-*" to "". The trick is to be precise: + # we update the version field inside `{...}` blocks for THIS fork. + local package_names + case "$fork" in + syscall) package_names=("redox_syscall" "syscall") ;; + *) package_names=("$fork") ;; + esac + for f in $files; do + local changed=0 + for name in "${package_names[@]}"; do + if grep -qE "^${name}\s*=\s*\{" "$f"; then + local tmp + tmp="$(mktemp)" + awk -v name="$name" -v old="$old_base" -v new="$new_target" ' + BEGIN { in_block=0; depth=0 } + { + if (in_block) { + n_open = gsub(/\{/, "{") + n_close = gsub(/\}/, "}") + depth += n_open - n_close + if (depth == 0) { + in_block=0 + print + next + } + if ($0 ~ /version[[:space:]]*=/) { + gsub("version[[:space:]]*=[[:space:]]*\"" old "\"", "version = \"" new "\"") + } + print + next + } + if ($0 ~ "^"name"[[:space:]]*=[[:space:]]*\\{") { + in_block=1 + depth=1 + # Handle the entry line itself: it may carry + # the version= we want to update. + n_open = gsub(/\{/, "{") + n_close = gsub(/\}/, "}") + depth += n_open - n_close + if (depth == 0) { + in_block=0 + print + next + } + if ($0 ~ /version[[:space:]]*=/) { + gsub("version[[:space:]]*=[[:space:]]*\"" old "\"", "version = \"" new "\"") + } + print + next + } + print + } + ' "$f" > "$tmp" && mv "$tmp" "$f" + changed=1 + fi + done + if [[ $changed -eq 1 ]]; then + echo " REQ ${f}: ${old_base} → ${new_target}" + fi + done +} + +# ---- Phase 1: bump fork versions ---- +echo "=== Phase 1: update Cat 2 fork versions ===" +DRIFT=0 +for fork in "${CAT2_FORKS[@]}"; do + if ! update_fork_version "$fork"; then + DRIFT=$((DRIFT + 1)) + fi +done +echo "" + +# ---- Phase 1.5: enforce no-fake-version-label rule ---- +# Per local/AGENTS.md § "No-fake-version-label rule", a `-rbN` version +# label is only valid when the source content matches the corresponding +# upstream release + documented Red Bear patches. Verify that the +# contents we just labelled actually correspond to the declared +# upstream tag before we declare success. Refuse to leave the working +# tree in a state where the version label is fake. +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +if [ -x "$SCRIPT_DIR/verify-fork-versions.sh" ]; then + if ! "$SCRIPT_DIR/verify-fork-versions.sh" 2>&1 | grep -q "^All Cat 2 forks pass"; then + echo "ERROR: After applying -rbN suffix, verify-fork-versions.sh found fake labels." >&2 + echo " The local source content does not match the declared upstream release." >&2 + echo " This is the no-fake-version-label policy violation." >&2 + echo " Inspect the upstream tag/branch mapping in local/fork-upstream-map.toml," >&2 + echo " update the fork's source content, then re-run apply-rb-suffix.sh." >&2 + if [ "$CHECK_ONLY" != "1" ]; then + exit 1 + fi + fi +fi + +# ---- Phase 2: update referencing files ---- +echo "=== Phase 2: update version requirements in path-dep consumers ===" +for fork in "${CAT2_FORKS[@]}"; do + current="$(get_current_version "$fork")" + [[ "$current" == "MISSING" ]] && continue + new_target="$current" + old_base="$(echo "$current" | sed -E 's/-rb[0-9]+$//')" + update_referencing_files "$fork" "$old_base" "$new_target" +done +echo "" + +# ---- Phase 3: summary ---- +if [[ $CHECK_ONLY -eq 1 ]]; then + if [[ $DRIFT -gt 0 ]]; then + echo "FAIL: $DRIFT fork(s) out of -rb suffix policy" + exit 1 + fi + echo "PASS: all Cat 2 forks have -rb suffix" + exit 0 +fi + +if [[ $ROLLBACK -eq 1 ]]; then + echo "Rolled back Cat 2 forks to stable X.Y.Z (no -rb suffix)" +else + echo "Applied -rb${RB_N} suffix to all Cat 2 forks" +fi diff --git a/local/scripts/build-preflight.sh b/local/scripts/build-preflight.sh index 29f9a2689a..120e3e9afa 100644 --- a/local/scripts/build-preflight.sh +++ b/local/scripts/build-preflight.sh @@ -39,6 +39,23 @@ if [ -x "$SCRIPT_DIR/verify-overlay-integrity.sh" ]; then fi fi +# Enforce the "no fake version label" rule (local/AGENTS.md): +# every Cat 2 fork's `-rbN>` version must match the source +# content from upstream `` + Red Bear patches. This runs before +# any recipe cook to fail fast. +if [ -x "$SCRIPT_DIR/verify-fork-versions.sh" ]; then + if ! "$SCRIPT_DIR/verify-fork-versions.sh" 2>&1 | grep -v "^All Cat 2 forks pass"; then + if [ "${REDBEAR_SKIP_FORK_VERIFY:-0}" != "1" ]; then + 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 [ -n "$RELEASE" ]; then bash "$SCRIPT_DIR/build-release-mode.sh" --release="$RELEASE" --config="$CONFIG" "${EXTRA_PACKAGES[@]/#/--extra-package=}" fi diff --git a/local/scripts/refresh-fork-upstream-map.sh b/local/scripts/refresh-fork-upstream-map.sh new file mode 100755 index 0000000000..83efe96f53 --- /dev/null +++ b/local/scripts/refresh-fork-upstream-map.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# refresh-fork-upstream-map.sh — Update local/fork-upstream-map.toml +# with the current upstream URLs and latest release tags for each +# Cat 2 fork. This is the canonical source of truth for +# verify-fork-versions.sh. + +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +cd "$ROOT" + +MAP="$ROOT/local/fork-upstream-map.toml" + +# fork_name upstream_url latest_tag +declare -A FORKS=( + [syscall]="https://gitlab.redox-os.org/redox-os/syscall.git" + [libredox]="https://gitlab.redox-os.org/redox-os/libredox.git" + [redoxfs]="https://gitlab.redox-os.org/redox-os/redoxfs.git" + [redox-scheme]="https://gitlab.redox-os.org/redox-os/redox-scheme.git" + [relibc]="https://gitlab.redox-os.org/redox-os/relibc.git" + [kernel]="https://gitlab.redox-os.org/redox-os/kernel.git" + [bootloader]="https://gitlab.redox-os.org/redox-os/bootloader.git" + [installer]="https://gitlab.redox-os.org/redox-os/installer.git" + [userutils]="https://gitlab.redox-os.org/redox-os/userutils.git" +) + +{ + echo "# fork-upstream-map.toml — Auto-generated by" + echo "# local/scripts/refresh-fork-upstream-map.sh. Do not edit by hand." + echo "# Format: " + echo "" + for fork in syscall libredox redoxfs redox-scheme relibc kernel bootloader installer userutils; do + url="${FORKS[$fork]:-}" + if [ -z "$url" ]; then + echo "WARN: no upstream URL for $fork, skipping" >&2 + continue + fi + # Get latest tag matching the project's version scheme + # (e.g. 0.9.0, 0.1.18, 0.5.12, 1.0.0). Exclude pre-release and + # non-stable tags. + latest=$(cd /tmp && git ls-remote --tags --sort=-v:refname "$url" 2>/dev/null \ + | awk '{print $2}' \ + | sed 's|refs/tags/||' \ + | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' \ + | head -1) + if [ -z "$latest" ]; then + echo "WARN: couldn't determine latest tag for $fork from $url" >&2 + continue + fi + printf "%-13s %-55s %s\n" "$fork" "$url" "$latest" + done +} > "$MAP" + +echo "Updated $MAP" +cat "$MAP" diff --git a/local/scripts/verify-fork-versions.sh b/local/scripts/verify-fork-versions.sh new file mode 100755 index 0000000000..55ca1c4228 --- /dev/null +++ b/local/scripts/verify-fork-versions.sh @@ -0,0 +1,162 @@ +#!/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 `version` field in the fork's Cargo.toml starts with that +# upstream release tag. +# +# This script is invoked by build-preflight.sh and apply-rb-suffix.sh. +# It returns exit code 1 if any fork fails the check, with a clear +# error message identifying the fork and the specific mismatch. + +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 + +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 + [ -n "$version" ] || continue + + # Only check Cat 2 forks (those with -rb suffix) + if [[ "$version" != *"-rb"* ]]; then + continue + fi + + # Extract the base version (before -rb) and the rb counter + base_version=$(echo "$version" | sed -E 's/-rb[0-9]+$//') || true + rb_n=$(echo "$version" | sed -E 's/^.*-rb([0-9]+)$/\1/') || true + + if [ -z "$base_version" ] || [ -z "$rb_n" ]; then + # If the base_version came out empty (e.g. version field is + # blank), we already detected & skipped in the empty-version + # case above. Reaching this branch with empty outputs means + # the version string is malformed (e.g. ends in `-rb` with + # no number). That is a fake-label failure. + echo "ERROR: $toml has malformed version '$version' (must be -rb)" >&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}') + + 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 + echo " Either update the version field or update the map." >&2 + violations=$((violations + 1)) + continue + fi + + # 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 + + # Compare file lists: the local fork should have the same files as + # upstream at $upstream_hash, plus any Red Bear patch files in + # local/patches/$fork_name/. + 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 + # Shallow-clone the specific tag directly. `--depth 1 --branch ` + # makes the tag the initial HEAD, so we have the file tree without + # having to fetch a specific commit (which `git clone --depth 1` + # can't reach on shallow clones because the commit is past the + # shallow boundary). + upstream_dir="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 + cd "$upstream_dir" + upstream_files=$(find . -type f -not -path './.git/*' -not -path './target/*' -not -path './Cargo.lock' | sort | sed 's|^\./||') + cd "$ROOT/$fork_dir" + + # Files in local but not in upstream: these are the Red Bear additions + # (tracked by the local fork) OR untracked working-tree files (which + # should not be present). Files in upstream but not in local: these + # are missing patches (unacceptable). + 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 + echo " This fork claims to be '$upstream_tag' but is missing source." >&2 + violations=$((violations + 1)) + fi + + if [ -n "$only_local" ]; then + echo "ERROR: $fork_name has files that don't exist in upstream $upstream_tag:" >&2 + echo "$only_local" | sed 's/^/ /' | head -10 >&2 + count=$(echo "$only_local" | wc -l) + if [ "$count" -gt 10 ]; then + echo " ... and $((count - 10)) more" >&2 + fi + echo " These must be deleted or the version field updated." >&2 + violations=$((violations + 1)) + fi + + # Verify content of shared files + diff_count=0 + while IFS= read -r f; do + [ -z "$f" ] && continue + if ! diff -q "$f" "/tmp/verify-$fork_name/$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 + echo " These must either be re-rebased onto $upstream_tag OR" >&2 + echo " moved to local/patches/$fork_name/ as documented Red Bear patches." >&2 + violations=$((violations + 1)) + fi + + rm -rf "/tmp/verify-$fork_name" +done + +if [ "$violations" -gt 0 ]; then + echo "" >&2 + echo "FAIL: $violations fork version violations found." >&2 + echo " Run local/scripts/refresh-fork-upstream-map.sh and" >&2 + echo " local/scripts/apply-rb-suffix.sh to fix the offending forks." >&2 + exit 1 +fi + +echo "All Cat 2 forks pass the no-fake-version-label check." +exit 0