Files
RedBear-OS/local/scripts/build-redbear.sh
T
vasilito a287fe20b9
redbear-ci / check (push) Has been cancelled
toolchain: remove the GCC 13 path; document the GCC 16 transition
Operator decision 2026-08-05: "we will not have gcc 13." The cross
toolchain has been GCC 16.1.0 since the port landed, but the build system
still defaulted to GCC 13 in three places and nothing recorded the move.

Build system:
- mk/prefix.mk: GCC_RECIPE?=gcc13 -> gcc16.
- mk/prefix.mk: the HOSTED_REDOX package rule derives its names from
  $(GCC_RECIPE) instead of hardcoding gcc13.pkgar / gcc13.cxx.pkgar.
  static.redox-os.org publishes no gcc16 package, so that path now fails
  at download. Deliberate: a loud failure beats a wrong compiler.
- build-redbear.sh: refuse to build on a non-GCC-16 toolchain. This is
  the one that mattered on a Linux host. `make prefix` unpacks upstream's
  gcc-install.tar.gz, which IS GCC 13.2.0, and no make rule replaces it --
  GCC 16 arrives only via install-gcc16-toolchain.sh. A fresh prefix
  therefore put you silently back on GCC 13, surfacing ~40 minutes later
  as a kwin C++23 failure. The guard checks all three locations
  (gcc-install, sysroot, ~/.redoxer/<target>/toolchain -- the last has
  highest priority) and names the two scripts to run.

Docs:
- New local/docs/TOOLCHAIN-GCC16.md: why the move (kwin/plasma-workspace
  need std::ranges::to), build/install/rollback procedure, the three
  locations that must agree, the tree-wide -std=gnu17 and -fno-hardened
  consequences, and the open libstdc++ float16-formatter gap that
  currently blocks the kwin link.
- CHANGELOG, docs/README.md index and state summary, and the stale claim
  in NATIVE-TOOLCHAIN-WORKSTREAM.md that gcc13 "still builds the GCC 13
  cross toolchain".

Also corrects stale references found while auditing:
- AGENTS.md described prefix/ as "Clang/LLVM"; it is GCC 16.1.0 + LLVM + Rust.
- AGENTS.md cited local/reference/linux-7.0/ (tree is linux-7.1).
- AGENTS.md's durable-patching example cited a mesa patch that does not
  exist; replaced with one wired in recipe.toml. (The first replacement
  used patch 03, which the changelog records as orphaned -- it targets a
  file removed from Mesa 26.1.4 upstream.)

native_bootstrap.sh still installs gcc13/gcc13.cxx packages inside a
Redox VM. Left alone: that is the deferred gcc-native workstream and no
gcc16 package is published for Redox to point it at.

Verified: bash -n clean; make -n prefix parses; guard accepts the current
16.1.0 toolchain in all three locations and rejects a stubbed 13.2.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 10:11:06 +03:00

1362 lines
68 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
# ── Build-system script version ──────────────────────────────
# Semantic version of this build orchestrator itself, independent of
# REDBEAR_VERSION (which tracks the OS release derived from the git branch).
# Starts at 1.0 and is bumped AUTOMATICALLY on every change by the pre-commit
# git hook (local/scripts/bump-build-version.sh); do not edit the minor by hand.
BUILD_REDBEAR_VERSION="1.13"
# ── Colorized output ──────────────────────────────────
# Enabled only on a TTY with NO_COLOR unset, so redirected build logs and CI
# output stay plain. Helpers preserve the existing ">>>" prefix convention.
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
C_RESET=$'\033[0m'; C_BOLD=$'\033[1m'
C_INFO=$'\033[1;36m'; C_OK=$'\033[1;32m'
C_WARN=$'\033[1;33m'; C_ERR=$'\033[1;31m'; C_DIM=$'\033[2m'
C_VER=$'\033[1;35m' # bright magenta — the startup version banner
else
C_RESET=; C_BOLD=; C_INFO=; C_OK=; C_WARN=; C_ERR=; C_DIM=; C_VER=
fi
log() { printf '%s>>> %s%s\n' "$C_INFO" "$*" "$C_RESET"; }
ok() { printf '%s>>> %s%s\n' "$C_OK" "$*" "$C_RESET"; }
warn() { printf '%s>>> WARNING: %s%s\n' "$C_WARN" "$*" "$C_RESET" >&2; }
err() { printf '%s>>> ERROR: %s%s\n' "$C_ERR" "$*" "$C_RESET" >&2; }
hdr() { printf '%s%s%s\n' "$C_BOLD$C_INFO" "$*" "$C_RESET"; }
# Print full version/build provenance and exit (used by --version).
print_version() {
local git_hash branch
git_hash="$(git -C "$PROJECT_ROOT" rev-parse --short HEAD 2>/dev/null || echo unknown)"
branch="$(git -C "$PROJECT_ROOT" branch --show-current 2>/dev/null || echo unknown)"
hdr "Red Bear OS build system"
printf ' build-redbear.sh : %s\n' "$BUILD_REDBEAR_VERSION"
printf ' OS version : %s\n' "$(git -C "$PROJECT_ROOT" branch --show-current 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || echo 0.0.0)"
printf ' repo commit : %s (branch %s)\n' "$git_hash" "$branch"
printf ' host : %s\n' "$(uname -n 2>/dev/null || echo unknown)"
printf ' arch : %s\n' "$(uname -m 2>/dev/null || echo unknown)"
}
# Startup version banner — printed first, highlighted in its own color so the
# running build script's version is always visible at a glance.
printf '%s══════ build-redbear.sh v%s ══════%s\n' "$C_VER" "$BUILD_REDBEAR_VERSION" "$C_RESET"
# Concurrent-build guard. Two builds running at once corrupt each other's
# cookbook caches, repo/ artefacts and prefix, and produce non-deterministic
# ISOs. Take an exclusive, non-blocking flock on a lockfile; if another build
# already holds it, refuse rather than silently racing. Set REDBEAR_ALLOW_
# CONCURRENT=1 to override (e.g. builds targeting isolated output dirs).
if [ -z "${REDBEAR_ALLOW_CONCURRENT:-}" ] && command -v flock >/dev/null 2>&1; then
REDBEAR_BUILD_LOCK="${TMPDIR:-/tmp}/redbear-build.lock"
exec 9>"$REDBEAR_BUILD_LOCK"
if ! flock -n 9; then
echo "${C_ERR}>>> ERROR:${C_RESET} another RedBear build is already running (lock: $REDBEAR_BUILD_LOCK)." >&2
echo "${C_INFO}>>>${C_RESET} Wait for it to finish, or set REDBEAR_ALLOW_CONCURRENT=1 to override." >&2
exit 1
fi
fi
source "$SCRIPT_DIR/lib/relibc-surface.sh"
# =============================================================================
# Canonical-build markers. Set ONLY by build-redbear.sh itself.
# Downstream tools (cookbook, preflight) read REDBEAR_CANONICAL_BUILD to know
# they are running inside the canonical pipeline. Manual `repo cook` invocations
# outside this script will NOT have this set, which enables:
# - gentle "you are off the canonical path" warnings
# - strict-by-default dirty-source gates
# =============================================================================
export REDBEAR_CANONICAL_BUILD=1
export REDBEAR_BUILD_HOST="$(uname -n 2>/dev/null || echo unknown)"
export REDBEAR_BUILD_START_EPOCH="$(date +%s)"
export REDBEAR_BUILD_STATE_DIR="$(mktemp -d -t redbear-build-state.XXXXXX)"
# Diagnostic capture: every cookbook log goes here in addition to /tmp
REDBEAR_BUILD_LOGS_DIR="$REDBEAR_BUILD_STATE_DIR/logs"
mkdir -p "$REDBEAR_BUILD_LOGS_DIR"
declare -ga REDBEAR_FORK_SOURCES=(
"relibc:$PROJECT_ROOT/local/sources/relibc"
"kernel:$PROJECT_ROOT/local/sources/kernel"
"base:$PROJECT_ROOT/local/sources/base"
"bootloader:$PROJECT_ROOT/local/sources/bootloader"
"installer:$PROJECT_ROOT/local/sources/installer"
"redoxfs:$PROJECT_ROOT/local/sources/redoxfs"
"libredox:$PROJECT_ROOT/local/sources/libredox"
"syscall:$PROJECT_ROOT/local/sources/syscall"
"userutils:$PROJECT_ROOT/local/sources/userutils"
)
redbear_is_fork_dirty() {
local dir="$1"
if [ ! -d "$dir/.git" ]; then
return 0
fi
git -C "$dir" diff --quiet HEAD 2>/dev/null || return 0
git -C "$dir" diff --cached --quiet HEAD 2>/dev/null || return 0
if [ -n "$(git -C "$dir" ls-files --others --exclude-standard 2>/dev/null)" ]; then
return 0
fi
return 1
}
# Per AGENTS.md § "BRANCH AND SUBMODULE POLICY (ABSOLUTE)": each Red Bear
# fork worktree MUST be checked out on the canonical "submodule/<name>"
# branch. This gate prevents drift from the canonical branch topology.
redbear_check_fork_branches() {
local err=0
local wrong=()
for entry in "${REDBEAR_FORK_SOURCES[@]}"; do
local label="${entry%%:*}"
local dir="${entry#*:}"
[ -d "$dir" ] || continue
local cur_branch
cur_branch=$(git -C "$dir" branch --show-current 2>/dev/null) || continue
local expected="submodule/${label}"
if [ "$cur_branch" != "$expected" ]; then
wrong+=("$label (got '$cur_branch', expected '$expected')")
err=1
fi
done
if [ "$err" != "0" ]; then
echo "========================================" >&2
echo " REFUSING TO BUILD — FORKS NOT ON CANONICAL BRANCHES" >&2
echo "========================================" >&2
echo "" >&2
echo "Per AGENTS.md § 'BRANCH AND SUBMODULE POLICY', each Red Bear" >&2
echo "fork worktree must be checked out on its canonical 'submodule/<name>'" >&2
echo "branch. The following forks are not:" >&2
echo "" >&2
for w in "${wrong[@]}"; do
echo " - $w" >&2
done
echo "" >&2
echo "Fix with (from the parent repo):" >&2
echo " for f in base bootloader installer kernel libredox redoxfs relibc syscall userutils; do" >&2
echo " (cd \"local/sources/\$f\" && git branch -m HEAD submodule/\$f 2>/dev/null || true)" >&2
echo " done" >&2
echo " ./local/scripts/push-fork-branches.sh" >&2
echo "" >&2
echo "Override (emergency only): REDBEAR_ALLOW_WRONG_BRANCH=1 $0 $*" >&2
exit 1
fi
}
# Cookbook binary fingerprint: BLAKE3-hash (via sha256sum on this system)
# of all .rs files in src/. Used to detect when the cookbook needs to be
# rebuilt. The hash is written to target/release/.cookbook-src-fingerprint
# after each successful build. Hashing every .rs file (sorted by path
# for determinism) gives content-stable fingerprints: git operations that
# change content without touching mtime (checkout, bisect, etc.) still
# trigger a rebuild.
redbear_compute_cookbook_fingerprint() {
if [ -d "$PROJECT_ROOT/src" ]; then
find "$PROJECT_ROOT/src" -name "*.rs" -type f -print0 2>/dev/null \
| sort -z \
| xargs -0 sha256sum 2>/dev/null \
| sha256sum \
| cut -d' ' -f1
else
echo "no-src-dir"
fi
}
redbear_record_cookbook_fingerprint() {
mkdir -p "$PROJECT_ROOT/target/release"
redbear_compute_cookbook_fingerprint > "$PROJECT_ROOT/target/release/.cookbook-src-fingerprint"
}
# ── Pre-build cargo check sweep (--check-sweep) ──────────────────────
# Fast type/borrow check of the target's Rust sources BEFORE the expensive
# cook/prefix cycle, so ALL compile errors surface at once instead of one per
# full build. Uses the redoxer toolchain's cargo with `cargo check` (which does
# no linking, so it needs neither relibc.a nor the C linker) against the redox
# target. Scope: the fork sources (every target builds these) plus the local
# Rust recipes that are in the config being built.
redbear_resolve_config_packages() {
python3 - "$PROJECT_ROOT" "$1" <<'PYEOF' 2>/dev/null || true
import sys, tomllib
from pathlib import Path
root = Path(sys.argv[1]); cfg = sys.argv[2]
def resolve(p, seen=None):
seen = seen or set(); p = p.resolve()
if p in seen or not p.exists():
return {}
seen.add(p); c = tomllib.loads(p.read_text()); pkgs = dict(c.get("packages", {}))
for inc in c.get("include", []):
ip = p.parent / inc
if ip.exists():
m = resolve(ip, seen); m.update(pkgs); pkgs = m
return pkgs
for name in sorted(resolve(root / "config" / f"{cfg}.toml")):
print(name)
PYEOF
}
redbear_check_sweep() {
local target="${TARGET:-x86_64-unknown-redox}"
local tc="$HOME/.redoxer/$target/toolchain"
if [ ! -x "$tc/bin/cargo" ]; then
warn "check-sweep: redoxer toolchain not found at $tc/bin/cargo — skipping (run a full build once to provision it)"
return 0
fi
log "Check-sweep: cargo check --target $target (pre-build type/borrow check)"
local -a manifests=() names=()
local d
# Forks that do NOT build for the redox userspace target — checking them with
# --target x86_64-unknown-redox is meaningless (false positives). bootloader
# is a bare-metal/UEFI crate built for its own custom targets
# (targets/*.json), so skip it here.
local CHECK_SKIP_FORKS=" bootloader "
# 1) Fork sources — every target builds these; the recurring compile-error
# source (relibc, base, ...). A relibc error here would otherwise only
# surface during the prefix rebuild after a multi-minute cook.
for d in "$PROJECT_ROOT"/local/sources/*/; do
[ -f "${d}Cargo.toml" ] || continue
local fork_name; fork_name="$(basename "$d")"
case "$CHECK_SKIP_FORKS" in *" $fork_name "*)
log " (skip $fork_name — not built for $target)"; continue ;;
esac
manifests+=("${d}Cargo.toml"); names+=("$fork_name")
done
# 2) Local Rust recipes that are in THIS config (target-scoped — a mini build
# does not check graphics recipes).
local pkg src
while IFS= read -r pkg; do
[ -n "$pkg" ] || continue
for src in "$PROJECT_ROOT"/local/recipes/*/"$pkg"/source/Cargo.toml; do
[ -f "$src" ] || continue
manifests+=("$src"); names+=("$pkg")
done
done < <(redbear_resolve_config_packages "$CONFIG")
local i failed=0 checked=0
local -a failed_names=()
for i in "${!manifests[@]}"; do
local n="${names[$i]}"
checked=$((checked + 1))
local logf="$REDBEAR_BUILD_LOGS_DIR/check-sweep-$n.log"
if PATH="$tc/bin:$PATH" RUSTUP_TOOLCHAIN= "$tc/bin/cargo" check \
--manifest-path "${manifests[$i]}" --target "$target" --offline \
>"$logf" 2>&1; then
ok " check OK: $n"
else
err "check FAILED: $n"
grep -E "^error(\[|:)|could not compile" "$logf" 2>/dev/null | sed 's/^/ /' | head -12 >&2
failed=$((failed + 1)); failed_names+=("$n")
fi
done
echo ""
if [ "$failed" -gt 0 ]; then
err "Check-sweep: $failed/$checked package(s) FAILED cargo check: ${failed_names[*]}"
err "Full logs under $REDBEAR_BUILD_LOGS_DIR/check-sweep-*.log — fix these before building."
return 1
fi
ok "Check-sweep passed: all $checked package(s) type-check clean."
return 0
}
# Derive Red Bear OS version from the current git branch name.
# Branch "0.2.4" → REDBEAR_VERSION="0.2.4".
# Exported so the redbear-release recipe and other consumers can use it.
if [ -z "${REDBEAR_VERSION:-}" ]; then
REDBEAR_VERSION=$(git -C "$PROJECT_ROOT" branch --show-current 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || echo "0.0.0")
fi
export REDBEAR_VERSION
echo "${C_INFO}>>>${C_RESET} Red Bear OS version: $REDBEAR_VERSION (from git branch)"
# Source .config for build settings, but NEVER auto-set REDBEAR_RELEASE.
# Release mode requires explicit REDBEAR_RELEASE= in the environment.
if [ -f "$PROJECT_ROOT/.config" ]; then
while IFS= read -r line; do
line="${line%%#*}"
line=$(echo "$line" | xargs)
[ -z "$line" ] && continue
if [[ "$line" == *"?="* ]]; then
key="${line%%\?=*}"
value="${line#*\?=}"
elif [[ "$line" == *"="* ]]; then
key="${line%%=*}"
value="${line#*=}"
else
continue
fi
key=$(echo "$key" | xargs)
value=$(echo "$value" | xargs)
[ -z "$key" ] && continue
# Skip REDBEAR_RELEASE — dev builds must not use release mode
[ "$key" = "REDBEAR_RELEASE" ] && continue
# Only set if not already set in environment
[ -n "${!key:-}" ] || export "$key=$value"
done < "$PROJECT_ROOT/.config"
fi
# ── Operator knobs ─────────────────────────────────────────────
# The common knobs below have CLI flags (the canonical interface); the matching
# REDBEAR_*/JOBS environment variables remain DEPRECATED fallback defaults so
# older automation keeps working. Rarer escape hatches stay env-only (listed in
# usage()). Resolved values are re-exported after the parser so this script's
# gates and the sub-scripts observe the flag-provided values.
CONFIG="redbear-full"
JOBS="${JOBS:-$(nproc)}"
# Dep-level parallel recipe cooking: cook this many independent (same-level)
# recipes at once.
#
# DEFAULT IS 1 (SERIAL) ON PURPOSE. Recipes are NOT isolated from each other:
# redbear_qt_ensure_dep_sysroots (local/scripts/lib/qt-sysroot.sh) repairs OTHER
# recipes' sysroots, so a cook can relink the very include/ or lib/ directory a
# concurrent cook is compiling against. That produced failures that moved
# between source files from run to run -- the signature of a race, not a bug:
#
# kirigami: cstdio:47: fatal error: .../qtdeclarative/.../sysroot/include/
# stdio.h: No such file or directory
# (build 59 failed in KirigamiTemplates, build 61 in KirigamiPrivateplugin)
#
# Making that relink atomic removed one window, but the underlying sharing
# remains: recipes write into each other's sysroots by design. Until that
# coupling is gone, cooking recipes concurrently is not sound, and a build that
# fails intermittently costs far more than a serial one that does not.
#
# This does NOT serialise compilation. Each recipe still gets the full -j${JOBS}
# make budget, so the machine stays busy inside a recipe; only the number of
# recipes in flight drops to one. Set COOKBOOK_COOK_JOBS>1 to opt back into
# parallel cooking if you accept the risk.
COOK_JOBS_DEFAULT=1; [ "$JOBS" -lt "$COOK_JOBS_DEFAULT" ] && COOK_JOBS_DEFAULT="$JOBS"
export COOKBOOK_COOK_JOBS="${COOKBOOK_COOK_JOBS:-$COOK_JOBS_DEFAULT}"
APPLY_PATCHES="${APPLY_PATCHES:-1}"
NO_CACHE=0
CHECK_SWEEP=0
ALLOW_UPSTREAM="${REDBEAR_ALLOW_UPSTREAM:-0}"
ALLOW_DIRTY="${REDBEAR_ALLOW_DIRTY:-0}"
KEEP_BUILD_STATE="${REDBEAR_KEEP_BUILD_STATE:-0}"
RELEASE="${REDBEAR_RELEASE:-}"
# ARCH and HOST_ARCH are used by the Makefile (mk/config.mk) and the prefix
# rule (mk/prefix.mk). They are derived from `uname -m` if unset.
export ARCH="${ARCH:-$(uname -m)}"
export HOST_ARCH="${HOST_ARCH:-$(uname -m)}"
usage() {
cat <<EOF
Usage: $(basename "$0") [OPTIONS] [CONFIG]
Build a tracked Red Bear OS profile.
Options:
-j, --jobs N Parallel build jobs (default: nproc)
--upstream Allow Redox/upstream recipe source refresh during build
--no-cache Force clean rebuild, discarding cached packages
--release VER Build from the immutable release archive VER
--allow-dirty Build with uncommitted fork edits (cooks the tree AS-IS)
--keep-build-state Keep the diagnostic state dir after exit
--check-sweep Run 'cargo check' on the target's Rust sources (forks +
config recipes) BEFORE building; abort on any error so all
compile errors surface at once instead of one per cook
-h, --help Show this help
-V, --version Show build-system version/provenance and exit
Configs:
redbear-full Desktop/graphics target (default)
redbear-mini Text-only console/recovery target
redbear-grub Text-only with GRUB boot manager
redbear-bare Stripped-to-bare-minimum: kernel+7-initfs-daemons+zsh login
Advanced (rare escape hatches — set as environment variables):
REDBEAR_ALLOW_CONCURRENT=1 Skip the single-build lock
REDBEAR_ALLOW_WRONG_BRANCH=1 Build with a fork off its submodule/<name> branch
REDBEAR_SKIP_PREFIX_REBUILD=1 Do not auto-rebuild a stale prefix sysroot
REDBEAR_FORCE_TOOLCHAIN_RECOOK=1 Re-cook llvm21/clang21/lld21/rust on a relibc change
REDBEAR_SKIP_FORK_VERIFY=1 Skip fork version verification (preflight)
REDBEAR_SKIP_DRIFT_CHECK=1 Skip upstream drift check (preflight)
REDBEAR_SKIP_FUNCTION_CHECK=1 Skip fork-function verification (preflight)
REDBEAR_SKIP_COLLISION_CHECK=1 Skip config-vs-package collision detection (preflight)
REDBEAR_SKIP_PATCH_CONTENT_CHECK=1 Skip orphaned-patch content check (preflight)
REDBEAR_STRICT_DURABILITY=1 Fail on non-durable source edits (preflight)
REDBEAR_STRICT_METADATA=1 Fail on recipe metadata issues (preflight)
REDBEAR_NO_VALIDATE=1 Skip the post-build image validation chain (make)
REDBEAR_COLLISIONS_WARN=1 Downgrade install-collision failures to warnings (make)
EOF
}
POSITIONAL=()
while [ $# -gt 0 ]; do
case "$1" in
--upstream) ALLOW_UPSTREAM=1 ;;
--no-cache) NO_CACHE=1 ;;
-j|--jobs) shift; JOBS="${1:?--jobs requires a number}" ;;
--jobs=*) JOBS="${1#*=}" ;;
--release) shift; RELEASE="${1:?--release requires a version}" ;;
--release=*) RELEASE="${1#*=}" ;;
--allow-dirty) ALLOW_DIRTY=1 ;;
--keep-build-state) KEEP_BUILD_STATE=1 ;;
--check-sweep) CHECK_SWEEP=1 ;;
-h|--help)
usage
exit 0
;;
-V|--version)
print_version
exit 0
;;
-*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
*)
POSITIONAL+=("$1")
;;
esac
shift
done
if [ ${#POSITIONAL[@]} -gt 1 ]; then
echo "ERROR: Too many positional arguments" >&2
usage >&2
exit 1
fi
[ ${#POSITIONAL[@]} -eq 1 ] && CONFIG="${POSITIONAL[0]}"
case "$CONFIG" in
redbear-full|redbear-mini|redbear-grub|redbear-bare)
;;
*)
echo "ERROR: Unknown config '$CONFIG'" >&2
echo "Supported: redbear-full, redbear-mini, redbear-grub, redbear-bare" >&2
exit 1
;;
esac
# Re-export resolved common knobs so this script's gates, build-preflight.sh and
# the Makefile see the flag-provided values (flags override the deprecated
# REDBEAR_* env fallbacks). REDBEAR_RELEASE is exported as-is (empty = dev build).
export REDBEAR_ALLOW_UPSTREAM="$ALLOW_UPSTREAM"
export REDBEAR_ALLOW_DIRTY="$ALLOW_DIRTY"
export REDBEAR_KEEP_BUILD_STATE="$KEEP_BUILD_STATE"
export REDBEAR_RELEASE="$RELEASE"
hdr "========================================"
hdr " Red Bear OS Build System"
hdr "========================================"
printf 'Config: %s%s%s\n' "$C_BOLD" "$CONFIG" "$C_RESET"
echo "Jobs: $JOBS"
echo "Apply patches: $APPLY_PATCHES"
echo "Upstream: $ALLOW_UPSTREAM"
echo "Root: ${PROJECT_ROOT##*/}"
echo "Script version: $BUILD_REDBEAR_VERSION"
hdr "========================================"
echo ""
cd "$PROJECT_ROOT"
# Strict dirty-source gate: refuse to build with uncommitted edits in any
# Red Bear fork source. Without this, builds cook from WIP trees whose
# pkgar fingerprints never match the committed HEAD on subsequent builds,
# producing "works on my machine" failures and stale cache invalidation.
# The escape hatch REDBEAR_ALLOW_DIRTY=1 is for emergency CI runs where
# the operator accepts the risk.
if [ "${REDBEAR_ALLOW_DIRTY:-0}" != "1" ] && [ -z "${REDBEAR_RELEASE:-}" ]; then
DIRTY_FORKS=()
for entry in "${REDBEAR_FORK_SOURCES[@]}"; do
label="${entry%%:*}"
dir="${entry#*:}"
[ -d "$dir" ] || continue
if redbear_is_fork_dirty "$dir"; then
DIRTY_FORKS+=("$label ($dir)")
fi
done
if [ ${#DIRTY_FORKS[@]} -gt 0 ]; then
echo "========================================" >&2
echo " REFUSING TO BUILD WITH UNCOMMITTED EDITS" >&2
echo "========================================" >&2
echo "" >&2
echo "Uncommitted edits detected in:" >&2
for fork in "${DIRTY_FORKS[@]}"; do
echo " - $fork" >&2
done
echo "" >&2
echo "Each fork must have a clean HEAD so pkgar fingerprints reflect" >&2
echo "committed state. Commit, stash, or reset your changes before" >&2
echo "running a canonical build." >&2
echo "" >&2
echo "Override (emergency only): $0 --allow-dirty $*" >&2
exit 1
fi
fi
# Per AGENTS.md § "BRANCH AND SUBMODULE POLICY (ABSOLUTE)": each Red Bear
# fork worktree must be on the canonical submodule/<name> branch.
if [ "${REDBEAR_ALLOW_WRONG_BRANCH:-0}" != "1" ] && [ -z "${REDBEAR_RELEASE:-}" ]; then
redbear_check_fork_branches
fi
if [ -x "$PROJECT_ROOT/local/scripts/verify-overlay-integrity.sh" ] && [ -z "${REDBEAR_RELEASE:-}" ]; then
echo "${C_INFO}>>>${C_RESET} Verifying overlay integrity..."
if ! "$PROJECT_ROOT/local/scripts/verify-overlay-integrity.sh" --quiet; then
echo "${C_INFO}>>>${C_RESET} Overlay integrity check FAILED — auto-repairing..."
"$PROJECT_ROOT/local/scripts/apply-patches.sh" || true
if "$PROJECT_ROOT/local/scripts/verify-overlay-integrity.sh" --quiet; then
echo "${C_INFO}>>>${C_RESET} Overlay integrity restored."
else
echo "WARNING: overlay integrity check still failing after repair."
echo " Continuing build — some packages may miscompile."
fi
else
echo "${C_INFO}>>>${C_RESET} Overlay integrity OK."
fi
echo ""
fi
# Per AGENTS.md: local recipes ALWAYS supersede WIP.
# Any WIP directory that shadows a local/recipes/ package must be
# replaced with a symlink to the local version.
if [ -z "${REDBEAR_RELEASE:-}" ]; then
echo "${C_INFO}>>>${C_RESET} Enforcing local-over-WIP recipe policy..."
for local_recipe in "$PROJECT_ROOT"/local/recipes/*/*/; do
pkg=$(basename "$local_recipe")
[ ! -f "$local_recipe/recipe.toml" ] && continue
while IFS= read -r -d '' wip_dir; do
if [ ! -L "$wip_dir" ]; then
wip_rel=$(realpath --relative-to="$(dirname "$wip_dir")" "$local_recipe")
rm -rf "$wip_dir"
ln -sf "$wip_rel" "$wip_dir"
echo " WIP $pkg -> local ($wip_rel)"
fi
done < <(find "$PROJECT_ROOT"/recipes/wip -maxdepth 5 -name "$pkg" -type d -print0 2>/dev/null || true)
done
echo ""
fi
# The build system no longer stashes your working tree (it cooks committed HEAD,
# or your tree AS-IS under REDBEAR_ALLOW_DIRTY=1). A prior version stashed dirty
# forks and, due to a git-output-parsing bug, sometimes failed to restore them,
# leaving "redbear-build-*" stashes behind. Surface any leftovers read-only so
# stranded WIP is never silently forgotten. This NEVER touches your tree.
_rb_leftover_total=0
for _entry in "${REDBEAR_FORK_SOURCES[@]}"; do
_d="${_entry#*:}"
[ -d "$_d/.git" ] || continue
_n=$(git -C "$_d" stash list 2>/dev/null | grep -c "redbear-build-" || true)
if [ "${_n:-0}" -gt 0 ]; then
echo "${C_WARN}>>> NOTE:${C_RESET} ${_entry%%:*}: $_n leftover 'redbear-build-*' stash(es) from an older build — recover: git -C '$_d' stash list" >&2
_rb_leftover_total=$((_rb_leftover_total + _n))
fi
done
[ "$_rb_leftover_total" -gt 0 ] && echo "${C_WARN}>>> NOTE:${C_RESET} $_rb_leftover_total stranded build-stash(es) total; see local/recovered-stashes/README.md" >&2
# EXIT trap: always dump diagnostics and preserve the original exit code. The trap fires
# even on signals (SIGINT/SIGTERM/SIGHUP) because we don't override
# ERR/DEBUG and we use EXIT (the POSIX-standard pseudo-signal that fires
# for any exit path). The trap runs LAST in the shell's exit order, so
# any explicit `exit N` we do inside the script triggers it with $? = N.
redbear_exit_trap() {
local rc=$?
# Phase 2: dump failure diagnostics if the build failed.
if [ "$rc" -ne 0 ]; then
redbear_dump_failure_diagnostics "$rc"
fi
# Phase 3: clean up the state dir unless the user wants to inspect it.
if [ -d "$REDBEAR_BUILD_STATE_DIR" ] && [ "${REDBEAR_KEEP_BUILD_STATE:-0}" != "1" ]; then
rm -rf "$REDBEAR_BUILD_STATE_DIR"
fi
exit "$rc"
}
trap redbear_exit_trap EXIT
# Failure diagnostics: collect the last N lines of every cookbook log in
# $REDBEAR_BUILD_LOGS_DIR plus any mktemp leftovers from the build, list
# partial recipe build states, report orphaned cook processes, and point
# the operator at the state directory for post-mortem analysis. Designed to
# give the operator a complete picture without requiring them to manually
# grep through /tmp.
redbear_dump_failure_diagnostics() {
local rc="$1"
echo ""
printf '%s========================================%s\n' "$C_ERR" "$C_RESET"
printf '%s BUILD FAILED (exit code: %s)%s\n' "$C_ERR" "$rc" "$C_RESET"
echo "========================================"
echo ""
echo "Diagnostic state preserved at:"
echo " $REDBEAR_BUILD_STATE_DIR"
echo ""
echo "Pass --keep-build-state to retain this directory"
echo "after the script exits."
echo ""
# Last 30 lines of each cookbook log we captured.
if [ -d "$REDBEAR_BUILD_LOGS_DIR" ] && [ -n "$(ls -A "$REDBEAR_BUILD_LOGS_DIR" 2>/dev/null)" ]; then
echo "--- Last 30 lines of each captured build log ---"
local log
for log in "$REDBEAR_BUILD_LOGS_DIR"/*.log; do
[ -f "$log" ] || continue
echo ""
echo "==> $(basename "$log")"
tail -30 "$log"
done
fi
# Cookbook / cargo processes that may be orphaned from a signal kill.
# These are often the actual cause of subsequent build failures (held
# locks, half-written files).
echo ""
echo "--- Orphaned cook/cargo processes (if any) ---"
local orphan
orphan=$(ps -eo pid,ppid,etime,comm 2>/dev/null | \
grep -E 'repo|cargo|rustc|cc1|cookbook_redbear_redoxer' | \
grep -v grep || true)
if [ -n "$orphan" ]; then
echo "$orphan"
else
echo " (none)"
fi
# Recipe build states: which packages have a stage.pkgar (success)
# vs which have only stage.tmp (in-progress).
echo ""
echo "--- Per-recipe build state ---"
# Scope to THIS build only. The dump walks the whole recipe tree, so without
# a time filter a bare/mini build would list graphical packages (mesa, qt,
# kf6-*, sddm, ...) whose stage/stage.tmp are leftovers from an earlier
# redbear-full build — packages not even part of the target being built.
# Report an artifact only if it was modified during this build.
local recipe_dir target_arch
local _rb_start="${REDBEAR_BUILD_START_EPOCH:-0}"
local _rb_shown=0
for recipe_dir in "$PROJECT_ROOT"/recipes/*/ "$PROJECT_ROOT"/local/recipes/*/*/; do
[ -d "$recipe_dir" ] || continue
[ -f "$recipe_dir/recipe.toml" ] || continue
local pkg
pkg=$(basename "$recipe_dir")
for target_arch in "$PROJECT_ROOT"/recipes/"$pkg"/target/*/ \
"$PROJECT_ROOT"/local/recipes/*/"$pkg"/target/*/; do
[ -d "$target_arch" ] || continue
local stage stage_tmp size ts
stage="$target_arch/stage"
stage_tmp="$target_arch/stage.tmp"
if [ -d "$stage_tmp" ]; then
ts=$(stat -c %Y "$stage_tmp" 2>/dev/null || echo 0)
[ "$ts" -ge "$_rb_start" ] || continue
size=$(du -sh "$stage_tmp" 2>/dev/null | cut -f1)
echo " $pkg [$target_arch]: INCOMPLETE (stage.tmp: $size)"
_rb_shown=1
elif [ -d "$stage" ]; then
ts=$(stat -c %Y "$stage" 2>/dev/null || echo 0)
[ "$ts" -ge "$_rb_start" ] || continue
echo " $pkg [$target_arch]: complete (this build)"
_rb_shown=1
fi
done
done
[ "$_rb_shown" = "0" ] && echo " (no recipe artifacts were modified during this build)"
echo ""
echo "========================================"
echo ""
}
if [ "$APPLY_PATCHES" = "1" ] && [ -z "${REDBEAR_RELEASE:-}" ]; then
echo "${C_INFO}>>>${C_RESET} Local fork sources already have patches committed. Recipe patches (non-fork recipes) are applied atomically by 'repo fetch' via recipe.toml."
elif [ -n "${REDBEAR_RELEASE:-}" ]; then
echo "${C_INFO}>>>${C_RESET} Release mode: fork sources include all patches; no separate patch step needed."
fi
if [ ! -f "target/release/repo" ]; then
echo "${C_INFO}>>>${C_RESET} Building cookbook binary (missing)..."
cargo build --release
redbear_record_cookbook_fingerprint
else
# Rebuild cookbook if any .rs file in src/ changed since the last
# successful build. The previous existence-only check let stale
# binaries survive `git pull`, causing CLI flags to silently no-op.
CURRENT_COOKBOOK_HASH=$(redbear_compute_cookbook_fingerprint)
STORED_COOKBOOK_HASH=""
[ -f "target/release/.cookbook-src-fingerprint" ] && \
STORED_COOKBOOK_HASH=$(cat "target/release/.cookbook-src-fingerprint")
if [ "$CURRENT_COOKBOOK_HASH" != "$STORED_COOKBOOK_HASH" ]; then
echo "${C_INFO}>>>${C_RESET} Rebuilding cookbook binary (source changed: ${CURRENT_COOKBOOK_HASH:0:12} != ${STORED_COOKBOOK_HASH:0:12})..."
cargo build --release
redbear_record_cookbook_fingerprint
else
echo "${C_INFO}>>>${C_RESET} Cookbook binary is up to date (${CURRENT_COOKBOOK_HASH:0:12})"
fi
fi
if [ "$CONFIG" = "redbear-full" ]; then
redbear_ensure_relibc_desktop_surface
fi
FW_AMD_DIR="$PROJECT_ROOT/local/firmware/amdgpu"
if [ "$CONFIG" = "redbear-full" ]; then
if [ -d "$FW_AMD_DIR" ] && [ -n "$(ls -A "$FW_AMD_DIR" 2>/dev/null)" ]; then
FW_COUNT=$(ls "$FW_AMD_DIR"/*.bin 2>/dev/null | wc -l)
echo "${C_INFO}>>>${C_RESET} Found $FW_COUNT AMD firmware blobs"
else
echo "${C_WARN}>>> WARNING:${C_RESET} No AMD firmware blobs found."
echo " Run: ./local/scripts/fetch-firmware.sh"
echo " GPU driver will NOT function without firmware."
fi
echo ""
fi
# Pre-build cargo check sweep (opt-in via --check-sweep). Runs before any cook
# so all Rust compile errors surface at once; a failure aborts before the
# expensive prefix/cook cycle.
if [ "$CHECK_SWEEP" = "1" ]; then
redbear_check_sweep || exit 1
fi
echo "${C_INFO}>>>${C_RESET} Building Red Bear OS with config: $CONFIG"
echo "${C_INFO}>>>${C_RESET} Build time varies by config (redbear-mini ~30-60 min, redbear-full ~60-120 min on first build)..."
# Stale-build prevention: if a low-level source repo has commits newer
# than its pkgar, delete that package's pkgar and target dir AND clean
# build/sysroot dirs across all recipes. Low-level packages (relibc,
# kernel, base) provide the C runtime and compiler support libs; when
# they change, autotools packages (pcre2, gettext, libiconv, etc.)
# retain stale configure/libtool scripts that reference the old runtime,
# causing "libtool version mismatch" and "not a valid libtool object"
# errors. Cleaning build/ and sysroot/ forces re-configuration while
# preserving stage/ and source/ so the cookbook can skip unchanged
# packages that don't use autotools.
if [ "$NO_CACHE" != "1" ]; then
STALE_DETECTED=0
RELIBC_INVALIDATED=0
# Whether a fork that userspace binaries actually LINK changed. Only relibc
# (the C runtime) and base (compiler-support/startup libs) are linked into
# userspace; kernel/bootloader/installer are standalone and a change to them
# must NOT force a full userspace re-cook (that wasted ~1h on kernel-only
# fixes). Gates the build/sysroot clean below.
USERSPACE_RUNTIME_STALE=0
for src in relibc kernel base bootloader installer; do
src_dir="$PROJECT_ROOT/local/sources/$src"
pkgar="$PROJECT_ROOT/repo/x86_64-unknown-redox/$src.pkgar"
fingerprint="$PROJECT_ROOT/repo/x86_64-unknown-redox/$src.source-fingerprint"
if [ -d "$src_dir/.git" ] && [ -f "$pkgar" ]; then
src_commit=$(git -C "$src_dir" rev-parse HEAD 2>/dev/null || echo "")
last_commit=$(cat "$fingerprint" 2>/dev/null || echo "")
# Working-tree dirtiness: tracked modifications, staged
# changes, and untracked files all count. Without this,
# uncommitted edits (e.g. an in-progress debug session)
# silently bypass the stale-detect and the next build
# cooks from a fingerprint that claims "up to date" but
# is actually older than the working tree.
src_dirty=0
if ! git -C "$src_dir" diff --quiet HEAD 2>/dev/null \
|| ! git -C "$src_dir" diff --cached --quiet HEAD 2>/dev/null \
|| [ -n "$(git -C "$src_dir" ls-files --others --exclude-standard 2>/dev/null)" ]; then
src_dirty=1
fi
if [ -n "$src_commit" ] && { [ "$src_commit" != "$last_commit" ] || [ "$src_dirty" = "1" ]; }; then
if [ "$src_dirty" = "1" ] && [ "$src_commit" = "$last_commit" ]; then
echo "${C_INFO}>>>${C_RESET} Stale $src detected (working tree has uncommitted changes); invalidating..."
else
echo "${C_INFO}>>>${C_RESET} Stale $src detected (source newer than last build); invalidating..."
fi
rm -f "$PROJECT_ROOT/repo/x86_64-unknown-redox/$src".*
find "$PROJECT_ROOT/recipes" -path "*/$src/target" -type d -exec rm -rf {} + 2>/dev/null || true
STALE_DETECTED=1
[ "$src" = "relibc" ] && RELIBC_INVALIDATED=1
# NB: base ships NO libraries (daemons/drivers/init) and relibc is
# a shared libc.so.6 — neither forces a userspace-wide relink. Only
# the static initfs-critical binaries are handled, selectively, below.
fi
fi
done
# Shared forks not covered by the pkgar loop above. userutils ships a pkgar
# (login/getty/etc); syscall/libredox/redox-scheme are Cargo path-dep crates
# with NO pkgar of their own but are linked into most of userspace, so
# cookbook's per-recipe cache cannot detect a change in them. Detect a source
# change and force a full userspace relink to prevent stale-binary skew.
for src in userutils syscall libredox redox-scheme; do
src_dir="$PROJECT_ROOT/local/sources/$src"
fingerprint="$PROJECT_ROOT/repo/x86_64-unknown-redox/$src.source-fingerprint"
[ -d "$src_dir/.git" ] || continue
src_commit=$(git -C "$src_dir" rev-parse HEAD 2>/dev/null || echo "")
last_commit=$(cat "$fingerprint" 2>/dev/null || echo "")
src_dirty=0
if ! git -C "$src_dir" diff --quiet HEAD 2>/dev/null \
|| ! git -C "$src_dir" diff --cached --quiet HEAD 2>/dev/null \
|| [ -n "$(git -C "$src_dir" ls-files --others --exclude-standard 2>/dev/null)" ]; then
src_dirty=1
fi
# Only act when a prior fingerprint exists (skip the first build, which
# cooks everything anyway).
if [ -n "$last_commit" ] && { [ "$src_commit" != "$last_commit" ] || [ "$src_dirty" = "1" ]; }; then
echo "${C_INFO}>>>${C_RESET} Stale shared fork $src detected; forcing full userspace relink (correctness)..."
STALE_DETECTED=1
USERSPACE_RUNTIME_STALE=1
[ "$src" = "userutils" ] && rm -f "$PROJECT_ROOT/repo/x86_64-unknown-redox/userutils".*
fi
done
# Vendored-recipe source staleness (the Qt/KDE 6.11.0->6.11.1 / 6.10->6.28
# class). Many recipes under local/recipes vendor their upstream tree in git
# at <recipe>/source/ AND declare a [source] tar=. Cookbook keys its build
# cache on the source.tar hash (source_identifier), but the ACTUAL build
# input is the vendored source/ tree. When a version bump propagates into
# source/ WITHOUT changing source.tar (e.g. source.tar was already the new
# version, or a manual source/ edit), cookbook sees an unchanged
# source_identifier and reuses a STALE stage — the desktop then builds the
# old version (qtshadertools wanted Qt 6.11.1 but got a cached 6.11.0 qtbase).
#
# Fix: fingerprint each vendored source/ by its git tree hash (content-based,
# mtime-independent) plus a working-tree-dirty flag, and rm that recipe's
# target/ when it changes. First observation for a recipe only SEEDS the
# fingerprint (no invalidation) so this never triggers a spurious full
# rebuild; thereafter any source/ change forces exactly that recipe to
# re-cook. Mirrors verify-external-source-versions.sh at the cache layer.
while IFS= read -r _rt; do
_rdir="$(dirname "$_rt")"
_sdir="$_rdir/source"
[ -d "$_sdir" ] || continue
_tgt="$_rdir/target/x86_64-unknown-redox"
[ -f "$_tgt/stage.pkgar" ] || continue # nothing cooked yet -> nothing stale
_relsrc="${_sdir#"$PROJECT_ROOT"/}"
_tree="$(git -C "$PROJECT_ROOT" rev-parse "HEAD:$_relsrc" 2>/dev/null || echo "")"
[ -n "$_tree" ] || continue # source/ not git-tracked -> skip
_dirty=""
git -C "$PROJECT_ROOT" diff --quiet HEAD -- "$_relsrc" 2>/dev/null || _dirty="-dirty"
[ -n "$(git -C "$PROJECT_ROOT" ls-files --others --exclude-standard -- "$_relsrc" 2>/dev/null)" ] && _dirty="-dirty"
_cur="${_tree}${_dirty}"
_fp="$_tgt/.redbear-source-tree"
_last="$(cat "$_fp" 2>/dev/null || echo "")"
if [ -z "$_last" ]; then
printf '%s\n' "$_cur" > "$_fp" # seed only, trust the existing build
elif [ "$_cur" != "$_last" ]; then
echo "${C_INFO}>>>${C_RESET} Stale vendored source: $(basename "$_rdir") (source/ changed since last cook) — invalidating target/"
rm -rf "$_rdir/target"
STALE_DETECTED=1
fi
done < <(find "$PROJECT_ROOT/local/recipes" -name recipe.toml -not -path '*/wip/*' 2>/dev/null)
# (The blanket "wipe every recipe's build/sysroot when a runtime fork changed"
# step used to live here. It was the bug: it recompiled the entire dynamically
# -linked desktop — Qt, KF6, mesa, sddm — on any relibc/base/syscall change,
# which is exactly what a shared-libc system never needs. Replaced by the
# selective, static-consumer-only invalidation below.)
# Downstream ABI-staleness prevention (relibc consumers).
#
# The loop above rebuilds relibc/kernel/base when THEIR OWN sources
# change, but statically-linked consumers keep their pkgars as long as
# THEIR sources are unchanged. relibc is both the C runtime AND the
# Redox scheme/syscall protocol shim, so a consumer built against an
# older relibc bakes in the OLD protocol. For boot-critical binaries
# copied into the initfs from the sysroot by
# recipes/core/base-initfs/recipe.toml this skew is FATAL, not cosmetic:
# - a stale getty issued the pre-"userspace fd allocation" two-step
# scheme-open and got EACCES opening /scheme/rand (login dead);
# - a stale redoxfs panicked "failed to create pipe".
# Both shipped in an ISO whose freshly-built daemons (randd/ptyd/...)
# already spoke the new protocol — a silent, hard-to-trace mismatch.
#
# Rule: invalidate an ABI-critical consumer when it would link an older
# relibc than the one this build will ship. That happens either because
# relibc is about to rebuild (RELIBC_INVALIDATED=1, or its pkgar is
# already gone) or because a prior run left relibc.pkgar newer than the
# consumer's pkgar (the exact getty/redoxfs case: relibc rebuilt hours
# after userutils/redoxfs).
#
# NB: if you add a binary to the base-initfs sysroot copy-set, add its
# recipe name to ABI_CRITICAL_PKGS below.
# relibc is statically linked into EVERY userspace binary, so a relibc
# change invalidates every consumer — not just the initfs-critical few. A
# stale consumer silently bakes in OLD relibc behaviour (old scheme/syscall
# protocol, missing syscall fixes, the tokio/epoll retry, ...), and
# diagnosing bugs against such stale binaries wastes enormous time. The rule
# is therefore bulletproof, not selective:
#
# * If relibc is (re)building this run, OR its pkgar is missing, invalidate
# ALL package pkgars + recipe build trees so every binary relinks the
# freshly-built relibc.
# * Otherwise (relibc unchanged this run), still catch the interrupted-
# build case: any consumer pkgar OLDER than relibc.pkgar was left behind
# by a prior run that rebuilt relibc but died before relinking it — that
# exact skew (relibc fixed, daemons stale) shipped a broken login image.
#
# source/ trees live under the recipe dir (not target/), so re-cook is safe.
REPO_DIR="$PROJECT_ROOT/repo/x86_64-unknown-redox"
RELIBC_PKGAR="$REPO_DIR/relibc.pkgar"
relibc_rebuilding=0
if [ "$RELIBC_INVALIDATED" = "1" ] || [ ! -f "$RELIBC_PKGAR" ]; then
relibc_rebuilding=1
fi
# Heavy, ABI-inert toolchain packages preserved across relibc invalidation.
#
# These are BUILD-TIME-ONLY static archives (built with BUILD_SHARED_LIBS=off;
# they ship no runtime .so of their own). Their references to libc/relibc
# symbols are UNRESOLVED in the .a and get resolved at the CONSUMER's final
# link — e.g. mesa links libLLVM*.a into libgallium and resolves malloc/etc.
# against the freshly-built relibc at THAT point. So a relibc protocol change
# is absorbed by relinking the consumer (mesa is NOT on this list and stays
# invalidated), and recompiling all of LLVM/Rust from scratch to pick up a
# relibc change is unnecessary — it just burns 1-2h per base/relibc bump.
#
# This trades a rare theoretical risk (a relibc change to a libc struct/inline
# actually baked into an llvm .o) for a large, repeated speedup. The churn
# during driver/desktop work is Redox scheme/syscall-protocol shims, which
# these compilers never call, so the default is safe. Escape hatch:
# REDBEAR_FORCE_TOOLCHAIN_RECOOK=1 restores the full from-scratch nuke.
TOOLCHAIN_PRESERVE="llvm21 clang21 lld21 rust"
_rb_preserve_toolchain() {
[ "${REDBEAR_FORCE_TOOLCHAIN_RECOOK:-0}" = "1" ] && return 1
case " $TOOLCHAIN_PRESERVE " in
*" $1 "*) return 0 ;;
*) return 1 ;;
esac
}
# ── Selective, static-consumer-only ABI invalidation ───────────────
# relibc is a SHARED library (libc.so.6): dynamically-linked userspace — Qt,
# KF6, mesa, sddm, the whole desktop — resolves it at RUNTIME, so a relibc
# rebuild needs NO recompile there. That is exactly why upgrading glibc on
# Linux does not recompile the system. `base` ships no libraries at all
# (daemons/drivers/init). The ONLY binaries that can genuinely go stale are
# the STATIC, boot-critical initfs binaries that bake in the Redox
# scheme/syscall protocol before libc.so is available — the getty / redoxfs /
# init / randd class. Those come from a small fixed set of forks; when the
# boot ABI changes (relibc rebuilds, or a syscall/libredox/redox-scheme
# protocol crate changed -> USERSPACE_RUNTIME_STALE), relink ONLY them.
#
# This restores the selective rule the old "nuke every pkgar + wipe every
# build/sysroot" version had replaced (which recompiled the entire desktop on
# any driver/acpi/base edit). For a genuine relibc *C-ABI* break — a libc
# struct/inline baked into a compiled consumer, which is rare — force a full
# clean rebuild with `--no-cache`.
# base-initfs builds the STATIC boot-critical binaries baked into the initfs
# (init, logd, ramfs, randd, zerod, acpid, pcid, vesad, fbcond, …). It is a
# `custom` recipe whose content-hash cache does NOT track the boot-ABI crates
# (relibc/libredox/syscall/redox-scheme), so a boot-ABI change leaves it
# "cached" with stale binaries — pid1 `init` then dies at boot with a bogus
# "memory allocation failed" (ABI skew), never reaching login. It must relink
# alongside base/redoxfs/userutils whenever the boot ABI changes.
ABI_CRITICAL_PKGS="base base-initfs redoxfs userutils bootstrap"
_boot_abi_changed=0
[ "$relibc_rebuilding" = "1" ] && _boot_abi_changed=1
[ "$USERSPACE_RUNTIME_STALE" = "1" ] && _boot_abi_changed=1
if [ "$_boot_abi_changed" = "1" ]; then
echo "${C_INFO}>>>${C_RESET} Boot ABI touched (relibc / syscall protocol fork) — relinking ONLY the"
echo "${C_INFO}>>>${C_RESET} static initfs-critical binaries: $ABI_CRITICAL_PKGS."
echo "${C_INFO}>>>${C_RESET} Dynamically-linked userspace (Qt/KF6/mesa/sddm/desktop) is untouched —"
echo "${C_INFO}>>>${C_RESET} shared libc.so resolved at runtime (the Linux glibc model)."
for _pkg in $ABI_CRITICAL_PKGS; do
rm -f "$REPO_DIR/$_pkg".*
find "$PROJECT_ROOT/recipes" "$PROJECT_ROOT/local/recipes" \
-path "*/$_pkg/target" -type d -exec rm -rf {} + 2>/dev/null || true
done
fi
fi
# Host fstools staleness (redox_installer / redoxfs FUSE used by `make live`).
#
# `make live` assembles the image with the HOST-compiled redox_installer from
# build/fstools/bin, built once by the `fstools` make target via
# `cargo install --path local/sources/installer`. That target is a DIRECTORY
# target (build/fstools) with NO prerequisite on the installer/redoxfs sources,
# so once build/fstools exists make never re-runs the cargo install — a source
# change to either fork silently never reaches image assembly. This bit us: an
# installer change to config-override handling appeared to "do nothing" because
# make live kept running a 6-day-old installer binary. Mirror the recipe
# staleness logic: if either fork's committed HEAD or working tree is newer
# than build/fstools, remove it so make rebuilds the host tools. Runs
# unconditionally (the NO_CACHE repo clean above does not touch build/fstools).
FSTOOLS_DIR="$PROJECT_ROOT/build/fstools"
if [ -d "$FSTOOLS_DIR" ]; then
fstools_ts=$(stat -c %Y "$FSTOOLS_DIR" 2>/dev/null || echo "0")
fstools_stale=0
for src in installer redoxfs; do
src_dir="$PROJECT_ROOT/local/sources/$src"
[ -d "$src_dir/.git" ] || continue
src_ts=$(git -C "$src_dir" log -1 --format=%ct HEAD 2>/dev/null || echo "0")
src_dirty=0
if ! git -C "$src_dir" diff --quiet HEAD 2>/dev/null \
|| ! git -C "$src_dir" diff --cached --quiet HEAD 2>/dev/null \
|| [ -n "$(git -C "$src_dir" ls-files --others --exclude-standard 2>/dev/null)" ]; then
src_dirty=1
fi
if { [ "$src_ts" != "0" ] && [ "$fstools_ts" != "0" ] && [ "$src_ts" -gt "$fstools_ts" ]; } \
|| [ "$src_dirty" = "1" ]; then
echo "${C_INFO}>>>${C_RESET} Stale host fstools detected ($src newer than build/fstools); rebuilding host installer/redoxfs..."
fstools_stale=1
fi
done
if [ "$fstools_stale" = "1" ]; then
rm -rf "$FSTOOLS_DIR"
fi
fi
PREFIX_LIBC="$PROJECT_ROOT/prefix/x86_64-unknown-redox/sysroot/x86_64-unknown-redox/lib/libc.a"
STALE_PREFIX=0
STALE_SRCS=()
if [ -f "$PREFIX_LIBC" ]; then
for src in relibc kernel base; do
src_dir="$PROJECT_ROOT/local/sources/$src"
if [ -d "$src_dir/.git" ]; then
fork_ts=$(git -C "$src_dir" log -1 --format=%ct HEAD 2>/dev/null || echo "0")
prefix_ts=$(stat -c %Y "$PREFIX_LIBC" 2>/dev/null || echo "0")
# Also check for uncommitted changes in the working tree
src_dirty=0
if ! git -C "$src_dir" diff --quiet HEAD 2>/dev/null \
|| ! git -C "$src_dir" diff --cached --quiet HEAD 2>/dev/null \
|| [ -n "$(git -C "$src_dir" ls-files --others --exclude-standard 2>/dev/null)" ]; then
src_dirty=1
fi
if [ "$fork_ts" != "0" ] && [ "$prefix_ts" != "0" ] && { [ "$fork_ts" -gt "$prefix_ts" ] || [ "$src_dirty" = "1" ]; }; then
fork_date=$(date -d "@$fork_ts" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "unknown")
prefix_date=$(date -d "@$prefix_ts" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "unknown")
if [ "$src_dirty" = "1" ]; then
echo "${C_INFO}>>>${C_RESET} Stale $src detected (working tree has uncommitted changes); prefix will be rebuilt."
else
echo "${C_INFO}>>>${C_RESET} Stale $src detected (fork $fork_date newer than prefix $prefix_date); prefix will be rebuilt."
fi
STALE_SRCS+=("$src")
STALE_PREFIX=1
fi
fi
done
fi
# Auto-rebuild prefix when stale (replaces previous warning-only behavior).
# Per Phase 1.3 of local/docs/SYSTEM-STABILITY-AND-UPSTREAM-SYNC-PLAN.md, this
# actually invokes `make prefix` so the toolchain stays in sync with the
# fork sources — without this, recipes link against a stale libc.a and
# runtime artefacts (initfs, base daemons) can crash on boot.
if [ "$STALE_PREFIX" = "1" ] && [ -z "${REDBEAR_SKIP_PREFIX_REBUILD:-}" ]; then
PREFIX_TARGET="${TARGET:-x86_64-unknown-redox}"
PREFIX_BASE="$PROJECT_ROOT/prefix/$PREFIX_TARGET"
# Only relibc actually feeds the prefix toolchain sysroot (which carries the
# cross compilers + relibc libc.a/headers). kernel/base do NOT contribute to
# the prefix, so a kernel/base-only staleness needs no toolchain rebuild.
_relibc_stale=0
for _s in "${STALE_SRCS[@]}"; do [ "$_s" = "relibc" ] && _relibc_stale=1; done
if [ "$_relibc_stale" = "1" ]; then
echo "${C_INFO}>>>${C_RESET} Auto-rebuilding prefix (${#STALE_SRCS[@]} stale fork(s): ${STALE_SRCS[*]})..."
# The `prefix` / `$(PREFIX)/sysroot` make targets have NO prerequisite on
# the relibc SOURCE tree, so once the sysroot directory exists `make
# prefix` reports "nothing to be done" and a relibc change never reaches
# the prefix libc.a. Previously the script then printed "rebuilt
# successfully" and touch'd libc.a — hiding real staleness (the #1 cause
# of undefined-reference link errors). Remove the DERIVED prefix markers
# so the relibc-install recipe re-cooks relibc and the sysroot is
# repopulated from the fresh stage. The (preserved) clang/rust/gcc-install
# markers make this cheap: only relibc is re-cooked.
rm -rf "$PREFIX_BASE/relibc-install" "$PREFIX_BASE/sysroot"
# ARCH and HOST_ARCH are exported at the top of this script. The make
# rule for `prefix` (mk/prefix.mk) derives TARGET from ARCH; pass the
# explicit TARGET override to match how recipe builds pass the triple.
if ! make prefix TARGET="$PREFIX_TARGET" 2>&1 | tail -n 20; then
echo "${C_ERR}>>> ERROR:${C_RESET} prefix rebuild failed. Aborting build to avoid producing broken binaries." >&2
exit 1
fi
# A genuine rebuild regenerates libc.a with a fresh mtime (newer than the
# fork commit), so the staleness check will not re-fire. Verify the
# artifact actually appeared — a silent no-op here must never be reported
# as success.
if [ ! -f "$PREFIX_LIBC" ]; then
echo "${C_ERR}>>> ERROR:${C_RESET} prefix rebuild did not produce $PREFIX_LIBC. Aborting." >&2
exit 1
fi
echo "${C_INFO}>>>${C_RESET} Prefix rebuilt successfully (relibc refreshed into sysroot)."
else
# kernel/base advanced but neither feeds the prefix toolchain. No rebuild
# is required; re-stamp libc.a so the mtime-based staleness check settles
# instead of firing on every subsequent build.
echo "${C_INFO}>>>${C_RESET} Prefix staleness from '${STALE_SRCS[*]}' only (does not feed the prefix toolchain); no rebuild needed."
touch "$PREFIX_LIBC"
fi
STALE_PREFIX=0
fi
# ── Toolchain guard: this fork is GCC 16 only ────────────────────────────────
# `make prefix` seeds gcc-install from upstream's static.redox-os.org tarball,
# which is GCC 13.2.0, and no make rule replaces it — the GCC 16 compiler is
# installed by local/scripts/install-gcc16-toolchain.sh. A GCC 13 toolchain
# compiles most of the tree and only fails deep into kwin, on the missing
# C++23 std::ranges::to, so it is far cheaper to refuse it here.
#
# All three locations are checked because the redoxer one has the highest
# priority (src/cook/script.rs prepends it to PATH last) and a mismatch
# between them is exactly how a "GCC 16" build silently uses GCC 13.
_rb_tc_target="${TARGET:-x86_64-unknown-redox}"
_rb_tc_bad=0
for _rb_tc_dir in \
"$PROJECT_ROOT/prefix/$_rb_tc_target/gcc-install" \
"$PROJECT_ROOT/prefix/$_rb_tc_target/sysroot" \
"$HOME/.redoxer/$_rb_tc_target/toolchain"; do
_rb_tc_cc="$_rb_tc_dir/bin/$_rb_tc_target-gcc"
[ -x "$_rb_tc_cc" ] || continue
_rb_tc_ver="$("$_rb_tc_cc" -dumpversion 2>/dev/null || echo unknown)"
case "$_rb_tc_ver" in
16.*) ;;
*)
echo "${C_ERR}>>> ERROR:${C_RESET} $_rb_tc_dir reports GCC $_rb_tc_ver; Red Bear requires GCC 16." >&2
_rb_tc_bad=1
;;
esac
done
if [ "$_rb_tc_bad" = "1" ]; then
echo "${C_ERR}>>>${C_RESET} Build and install the GCC 16 cross toolchain, then re-run:" >&2
echo " local/scripts/build-gcc16-cross.sh" >&2
echo " local/scripts/install-gcc16-toolchain.sh" >&2
echo " See local/docs/TOOLCHAIN-GCC16.md." >&2
exit 1
fi
unset _rb_tc_target _rb_tc_bad _rb_tc_dir _rb_tc_cc _rb_tc_ver
if [ "$NO_CACHE" = "1" ]; then
echo "${C_INFO}>>>${C_RESET} Cleaning repo and recipe caches for clean build..."
make repo_clean 2>/dev/null || true
rm -rf "$PROJECT_ROOT"/repo
find "$PROJECT_ROOT"/local/recipes -maxdepth 4 -name "target" -type d -exec rm -rf {} + 2>/dev/null || true
find "$PROJECT_ROOT"/recipes -maxdepth 3 -name "target" -type d -exec rm -rf {} + 2>/dev/null || true
fi
if [ -n "${REDBEAR_RELEASE:-}" ]; then
bash "$PROJECT_ROOT/local/scripts/build-release-mode.sh" --release="$REDBEAR_RELEASE" --config="$CONFIG" --extra-package=relibc
fi
bash "$PROJECT_ROOT/local/scripts/build-preflight.sh" --config="$CONFIG" ${REDBEAR_RELEASE:+--release="$REDBEAR_RELEASE"} --extra-package=relibc
bash "$PROJECT_ROOT/local/scripts/sync-versions.sh" --check || {
echo "WARNING: In-house crate version drift detected. Run './local/scripts/sync-versions.sh' to fix."
echo " Continuing build — versions will be corrected on next source fetch."
}
# Pre-cook critical packages that may fail in the dependency chain.
# --with-package-deps resolves ALL transitive deps; pre-cooking ensures
# the repo has valid pkgars before make live processes the full graph.
# Only pre-cook the desktop chain for redbear-full; mini/grub don't need it.
# llvm21 is a Mesa (graphics) dep — only needed when the Mesa chain is in scope.
# --- Host redoxer toolchain provisioning ------------------------------------
# Cooking a HOST tool (e.g. host:xz, pulled in to extract .tar.xz sources for the
# libclc -> clang21 host-build path) makes cookbook_redbear_redoxer look for a
# host toolchain at ~/.redoxer/<host-target>/toolchain. Unlike the redox-target
# toolchain, host toolchains are NOT published on static.redox-os.org, so
# redoxer's fallback download 404s ("redoxer env: unable to init toolchain") and
# every host cook fails — silently, since pre-cook failures are non-fatal, so it
# surfaces later as the whole desktop stack going missing.
#
# The redox-target toolchain already on disk is a full host+redox bundle (its
# clang defaults to x86_64-unknown-linux-gnu and it ships rust-std for the host),
# so it is a valid host toolchain. Provision the host toolchain as a symlink to
# it. A symlink (rather than exporting REDOXER_TOOLCHAIN) avoids the documented
# REDOXER_TOOLCHAIN Rust-version pitfall in PACKAGE-BUILD-QUIRKS.md. Idempotent.
_REDOX_TC="$HOME/.redoxer/x86_64-unknown-redox/toolchain"
_HOST_TC_DIR="$HOME/.redoxer/x86_64-unknown-linux-gnu"
if [ ! -e "$_HOST_TC_DIR/toolchain" ] && [ -d "$_REDOX_TC" ]; then
echo "${C_INFO}>>>${C_RESET} Provisioning host redoxer toolchain (symlink -> redox toolchain; host toolchains are not downloadable)..."
mkdir -p "$_HOST_TC_DIR"
rm -rf "$_HOST_TC_DIR/toolchain.partial"
ln -sfn "$_REDOX_TC" "$_HOST_TC_DIR/toolchain"
fi
# --- Ensure the redoxer toolchain LLVM carries the AMDGPU (+NVPTX) backend ---
# Mesa resolves `dependency('llvm', modules: [amdgpu, ...])` against the host
# llvm-config in the redoxer toolchain (COOKBOOK_HOST_SYSROOT, set by the mesa
# recipe). The prebuilt toolchain LLVM is compiled X86;AArch64;RISCV only — no
# GPU codegen backend — so radeonsi/radv AND the Intel iris/ANV CLC precompile
# path fail that dependency and the whole desktop (Mesa) stack silently drops.
# The prebuilt host toolchain LLVM is not regenerated by the (binary) prefix, so
# cook host:llvm21 (its recipe carries X86;AMDGPU;NVPTX) and splice its
# llvm-config + libLLVM.so + AMDGPU headers/static libs into the toolchain.
# Idempotent: a fast no-op once the toolchain already reports AMDGPU.
if [ "$CONFIG" = "redbear-full" ]; then
_TC="$HOME/.redoxer/x86_64-unknown-redox/toolchain"
_REPO="$PROJECT_ROOT/target/release/repo"
if [ -x "$_TC/bin/llvm-config" ] \
&& ! "$_TC/bin/llvm-config" --targets-built 2>/dev/null | grep -qw AMDGPU; then
echo "${C_INFO}>>>${C_RESET} Toolchain LLVM lacks the AMDGPU backend — cooking host:llvm21 (X86;AMDGPU;NVPTX) and splicing it into the toolchain (Mesa needs this)..."
if REDBEAR_CANONICAL_BUILD=1 "$_REPO" cook host:llvm21; then
_ST="$PROJECT_ROOT/recipes/dev/llvm21/target/x86_64-unknown-linux-gnu"
_LC="$_ST/stage.runtime/usr/bin/llvm-config"
if [ -x "$_LC" ] && "$_LC" --targets-built | grep -qw AMDGPU; then
if [ ! -d "$_TC/.pre-amdgpu-backup" ]; then
mkdir -p "$_TC/.pre-amdgpu-backup/bin" "$_TC/.pre-amdgpu-backup/lib"
cp -a "$_TC/bin/llvm-config" "$_TC/.pre-amdgpu-backup/bin/" 2>/dev/null || true
cp -a "$_TC"/lib/libLLVM* "$_TC/.pre-amdgpu-backup/lib/" 2>/dev/null || true
fi
rsync -a "$_LC" "$_TC/bin/llvm-config"
rsync -a "$_ST"/stage/usr/lib/libLLVM.so "$_ST"/stage/usr/lib/libLLVM.so.* "$_ST"/stage/usr/lib/libLLVM-*.so "$_TC/lib/" 2>/dev/null || true
rsync -a "$_ST"/stage.dev/usr/lib/libLLVM*.a "$_TC/lib/" 2>/dev/null || true
rsync -a --delete "$_ST"/stage.dev/usr/include/llvm "$_TC/include/" 2>/dev/null || true
rsync -a --delete "$_ST"/stage.dev/usr/include/llvm-c "$_TC/include/" 2>/dev/null || true
if "$_TC/bin/llvm-config" --targets-built | grep -qw AMDGPU; then
echo "${C_INFO}>>>${C_RESET} Toolchain LLVM now provides AMDGPU/NVPTX."
else
echo "${C_ERR}>>> ERROR:${C_RESET} AMDGPU splice failed (toolchain still lacks AMDGPU)." >&2
exit 1
fi
else
echo "${C_ERR}>>> ERROR:${C_RESET} host:llvm21 did not stage an AMDGPU-enabled llvm-config." >&2
exit 1
fi
else
echo "${C_ERR}>>> ERROR:${C_RESET} host:llvm21 cook failed (required for the Mesa AMDGPU backend)." >&2
exit 1
fi
fi
fi
echo "${C_INFO}>>>${C_RESET} Pre-cooking critical packages..."
if [ "$CONFIG" = "redbear-full" ]; then
# The Qt stack has a strict internal build order (qtbase -> qtshadertools ->
# qtdeclarative -> qtsvg -> qtwayland) and downstream consumers (sddm, the
# kf6 modules pulled by kwin) do find_package(Qt6Qml)/find_package(Qt6Quick).
# If these are only resolved transitively during the parallel `make live`
# graph, a consumer can be cooked before qtdeclarative publishes Qt6Qml and
# configure fails ("Could not find a package configuration file provided by
# Qt6Qml"), aborting the whole build. Pre-cook the Qt stack IN ORDER, before
# sddm/kwin, so every Qt6 CMake package is cached in the sysroot first.
PRECOOK_PKGS="relibc base redoxfs userutils base-initfs icu llvm21 libclc mesa libdrm libepoxy redox-drm lcms2 libdisplay-info libxcvt qtbase qtshadertools qtdeclarative qtsvg qtwayland sddm redbear-compositor redbear-greeter"
else
PRECOOK_PKGS="relibc base redoxfs userutils base-initfs icu"
fi
# NB: base/redoxfs/userutils are added to the pre-cook set so the boot-ABI
# "relink ONLY the static initfs-critical binaries" invalidation above
# (which rm's their repo pkgar + target) reliably re-publishes them via the
# single-recipe `repo cook` path — the same recovery relibc already relies on.
# Without this they cook "successful" inside `make live`'s nonstop graph but
# lose their stage.toml before publish (repo marks them outdated -> installer
# fails with `Package "base" not found`). The `[ ! -f repo/$pkg.pkgar ]` guard
# below makes this zero-cost on builds where they were not invalidated.
# bootstrap is intentionally omitted: it is part of the base recipe, not a
# standalone cookable package.
for pkg in $PRECOOK_PKGS; do
if [ ! -f "$PROJECT_ROOT/repo/x86_64-unknown-redox/$pkg.pkgar" ]; then
echo " cooking $pkg..."
log=$(mktemp)
# Pre-cook offline/upstream policy must match the main build phase
# (see the `make live` dispatch below). The main phase goes online when
# EITHER the REDBEAR_ALLOW_UPSTREAM env var is 1 OR the --upstream flag
# (ALLOW_UPSTREAM=1) is passed; it goes offline in release mode. The
# pre-cook previously honored only REDBEAR_ALLOW_UPSTREAM, so a plain
# `--upstream` invocation left the pre-cook OFFLINE while the main phase
# was ONLINE — any package missing its source.tar cache (e.g. after a
# version bump) then failed the pre-cook with "Opening file for blake3
# failed ... source.tar: No such file". Honor ALLOW_UPSTREAM here too.
if [ "${REDBEAR_ALLOW_UPSTREAM:-0}" = "1" ] || [ "${ALLOW_UPSTREAM:-0}" -eq 1 ]; then
PRECOOK_OFFLINE=false
else
PRECOOK_OFFLINE=true
fi
if [ -n "${REDBEAR_RELEASE:-}" ]; then
PRECOOK_OFFLINE=true
fi
if ! REDBEAR_BUILD_PHASE=pre-cook CI=1 COOKBOOK_OFFLINE="$PRECOOK_OFFLINE" \
"$PROJECT_ROOT/target/release/repo" cook "$pkg" >"$log" 2>&1; then
echo "WARNING: pre-cook of $pkg failed (non-fatal; will build during main phase). Tail of repo log:" >&2
tail -50 "$log" >&2
cp "$log" "$REDBEAR_BUILD_LOGS_DIR/${pkg}.pre-cook.log" 2>/dev/null || true
rm -f "$log"
else
cp "$log" "$REDBEAR_BUILD_LOGS_DIR/${pkg}.pre-cook.log" 2>/dev/null || true
rm -f "$log"
fi
fi
done
# NB: fork source-fingerprints used to be recorded HERE, before `make live`.
# That meant a build which failed in make live (or the critical-package gate)
# still stamped every fork as "built", so the next run's staleness detection
# skipped work it should have redone. The fingerprint write now happens only
# after a successful, complete build — see below, past the critical gate.
# Per-target env propagation. The cookbook's [build.env] section is not
# read by the current parser, so per-target env-var hooks must be set
# here in the dispatch script. Specifically the redbear-bare target
# reads REDBEAR_BARE_INITFS=1 in recipes/core/base-initfs/recipe.toml
# to switch from a 22-daemon initfs to a 7-daemon minimal initfs
# (init logd ramfs randd zerod ptyd getty). Auto-set when CONFIG is
# redbear-bare unless the operator has overridden the env var.
if [ "$CONFIG" = "redbear-bare" ] && [ -z "${REDBEAR_BARE_INITFS:-}" ]; then
REDBEAR_BARE_INITFS=1
export REDBEAR_BARE_INITFS
echo "${C_INFO}>>>${C_RESET} redbear-bare target: REDBEAR_BARE_INITFS=1 (auto-set; minimal 7-daemon initfs)"
fi
if [ "${REDBEAR_ALLOW_UPSTREAM:-0}" = "1" ]; then
echo "${C_WARN}>>> WARNING:${C_RESET} Upstream fetch ENABLED (REDBEAR_ALLOW_UPSTREAM=1)"
REPO_OFFLINE=0 COOKBOOK_OFFLINE=false CI=1 REDBEAR_BARE_INITFS="${REDBEAR_BARE_INITFS:-0}" make live "CONFIG_NAME=$CONFIG" "JOBS=$JOBS" 2>&1
elif [ -n "${REDBEAR_RELEASE:-}" ]; then
echo "${C_INFO}>>>${C_RESET} Release mode: building from local forks (offline)"
REPO_OFFLINE=1 COOKBOOK_OFFLINE=true CI=1 REDBEAR_BARE_INITFS="${REDBEAR_BARE_INITFS:-0}" make live "CONFIG_NAME=$CONFIG" "JOBS=$JOBS" 2>&1
elif [ "$ALLOW_UPSTREAM" -eq 1 ]; then
echo "${C_INFO}>>>${C_RESET} Upstream recipe refresh enabled"
REPO_OFFLINE=0 COOKBOOK_OFFLINE=false CI=1 REDBEAR_BARE_INITFS="${REDBEAR_BARE_INITFS:-0}" make live "CONFIG_NAME=$CONFIG" "JOBS=$JOBS" 2>&1
else
echo "${C_INFO}>>>${C_RESET} Upstream recipe refresh disabled (default: offline)"
REPO_OFFLINE=1 COOKBOOK_OFFLINE=true CI=1 REDBEAR_BARE_INITFS="${REDBEAR_BARE_INITFS:-0}" make live "CONFIG_NAME=$CONFIG" "JOBS=$JOBS" 2>&1
fi
# --- Critical-package gate --------------------------------------------------
# cookbook `make live` returns 0 even when individual recipes fail: it builds
# whatever it can and the installer packages whatever pkgars already exist. A
# failed critical recipe (e.g. mesa) therefore silently yields an ISO missing
# the entire desktop stack while this script still prints "Build Complete!".
# That happened for real: a mesa failure (libclc) cascaded to qtbase, qt*,
# sddm and redbear-greeter all being absent, yet the build exited 0.
#
# Gate on the target's critical packages actually existing as pkgars. If any is
# missing, fail LOUDLY (non-zero) and list them, so an incomplete desktop build
# can never masquerade as a working one. Console-only targets have no desktop
# critical set and are unaffected.
REPO_ARCH_DIR="$PROJECT_ROOT/repo/x86_64-unknown-redox"
case "$CONFIG" in
redbear-full)
CRITICAL_PKGS="mesa qtbase qtdeclarative qtwayland sddm redbear-compositor redbear-greeter"
;;
*)
CRITICAL_PKGS=""
;;
esac
_missing_critical=""
for _p in $CRITICAL_PKGS; do
[ -f "$REPO_ARCH_DIR/$_p.pkgar" ] || _missing_critical="$_missing_critical $_p"
done
if [ -n "$_missing_critical" ]; then
echo "" >&2
echo "========================================" >&2
echo " BUILD INCOMPLETE for $CONFIG" >&2
echo " Critical packages missing (no pkgar):$_missing_critical" >&2
echo "" >&2
echo " cookbook 'make live' returned success but these recipes did not" >&2
echo " produce a package, so the ISO is missing the desktop stack. This is" >&2
echo " a real failure, not a warning. Inspect the per-recipe logs under" >&2
echo " $REDBEAR_BUILD_LOGS_DIR and the build output above (search 'failed')." >&2
echo "========================================" >&2
exit 1
fi
# Record fork source-fingerprints ONLY now — after make live and the critical-
# package gate have both succeeded. Recording earlier (before make live) stamped
# forks as "built" even when the build later failed, so the next run's staleness
# detection skipped work it should have redone.
for src in relibc kernel base bootloader installer userutils syscall libredox redox-scheme; do
src_dir="$PROJECT_ROOT/local/sources/$src"
if [ -d "$src_dir/.git" ]; then
git -C "$src_dir" rev-parse HEAD 2>/dev/null > \
"$PROJECT_ROOT/repo/x86_64-unknown-redox/$src.source-fingerprint" 2>/dev/null || true
fi
done
ARCH="${ARCH:-$(uname -m)}"
echo ""
if [ "$STALE_PREFIX" = "1" ]; then
echo "${C_WARN}>>> REMINDER:${C_RESET} prefix sysroot is stale. Run 'make prefix' to avoid link errors on next build."
fi
printf '%s========================================%s\n' "$C_OK" "$C_RESET"
printf '%s Build Complete!%s\n' "$C_OK" "$C_RESET"
printf '%s========================================%s\n' "$C_OK" "$C_RESET"
echo "ISO: build/$ARCH/$CONFIG.iso"
echo ""
echo "To run in QEMU:"
echo " make qemu QEMUFLAGS=\"-m 4G\""
ls -lh "$PROJECT_ROOT/build/$ARCH/$CONFIG.iso" 2>/dev/null