#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" # 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 ">>> ERROR: another RedBear build is already running (lock: $REDBEAR_BUILD_LOCK)." >&2 echo ">>> 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" # Stash-and-restore: every Red Bear fork source whose working tree is dirty # is stashed before the build runs, then popped on EXIT (success or failure). # This ensures pkgar fingerprints always reflect the committed HEAD, not # transient WIP that could be rolled back between builds. declare -ga REDBEAR_STASHED_REPOS=() declare -gA REDBEAR_STASH_MAP=() 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/" # 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/'" >&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 } redbear_stash_one() { local label="$1" local dir="$2" if [ -n "${REDBEAR_STASH_MAP[$label]:-}" ]; then return 0 fi if ! redbear_is_fork_dirty "$dir"; then echo " [clean] $label" return 0 fi rm -f "$dir/.git/index.lock" local msg="redbear-build-$REDBEAR_BUILD_START_EPOCH-$label" local sha local stash_output stash_output=$(git -C "$dir" stash push --include-untracked -m "$msg" 2>/dev/null) sha=$(echo "$stash_output" | grep -E '^[0-9a-f]{40}$' | head -1) if [ -n "$sha" ]; then REDBEAR_STASH_MAP[$label]="$sha" REDBEAR_STASHED_REPOS+=("$label:$dir") echo " [stashed] $label: $sha" else # Stash produced no SHA — either nothing to stash or locale issue. # If the fork is now clean, treat as success. if ! redbear_is_fork_dirty "$dir"; then echo " [clean] $label (stash noop)" return 0 fi echo " [FAIL] $label: git stash push failed" >&2 return 1 fi } redbear_unstash_one() { local label="$1" local dir="$2" local sha="${REDBEAR_STASH_MAP[$label]:-}" if [ -z "$sha" ]; then return 0 fi rm -f "$dir/.git/index.lock" if git -C "$dir" stash pop --quiet 2>/dev/null; then echo " [restored] $label" else echo " [WARN] $label: stash pop failed — stash preserved at $sha" >&2 echo " Run: git -C '$dir' stash pop to restore manually" >&2 fi unset REDBEAR_STASH_MAP[$label] } redbear_stash_all_dirty_forks() { echo ">>> Stashing dirty fork sources (restored at exit)..." local any_dirty=0 local entry label dir for entry in "${REDBEAR_FORK_SOURCES[@]}"; do label="${entry%%:*}" dir="${entry#*:}" [ -d "$dir" ] || continue redbear_is_fork_dirty "$dir" && any_dirty=1 redbear_stash_one "$label" "$dir" || { echo "ERROR: failed to stash $label — aborting to avoid cooking from dirty tree" >&2 redbear_unstash_all_forks "stash-failure" exit 1 } done if [ "$any_dirty" = "0" ]; then echo " (all fork sources clean)" fi return 0 } redbear_unstash_all_forks() { local reason="${1:-exit}" [ "${#REDBEAR_STASHED_REPOS[@]}" -eq 0 ] && return 0 echo ">>> Restoring stashed fork sources (reason: $reason)..." local i for ((i=${#REDBEAR_STASHED_REPOS[@]}-1; i>=0; i--)); do local entry="${REDBEAR_STASHED_REPOS[$i]}" redbear_unstash_one "${entry%%:*}" "${entry#*:}" done } # 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" } # 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 ">>> 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 CONFIG="redbear-full" JOBS="${JOBS:-$(nproc)}" APPLY_PATCHES="${APPLY_PATCHES:-1}" ALLOW_UPSTREAM=0 NO_CACHE=0 # 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. Exporting # them at the top of this script ensures the auto-rebuild-prefix path # below receives a valid value, and the variable is visible to subprocesses. export ARCH="${ARCH:-$(uname -m)}" export HOST_ARCH="${HOST_ARCH:-$(uname -m)}" usage() { cat <&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 echo "========================================" echo " Red Bear OS Build System" echo "========================================" echo "Config: $CONFIG" echo "Jobs: $JOBS" echo "Apply patches: $APPLY_PATCHES" echo "Upstream: $ALLOW_UPSTREAM" echo "Root: ${PROJECT_ROOT##*/}" echo "========================================" 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): REDBEAR_ALLOW_DIRTY=1 $0 $*" >&2 exit 1 fi fi # Per AGENTS.md § "BRANCH AND SUBMODULE POLICY (ABSOLUTE)": each Red Bear # fork worktree must be on the canonical submodule/ 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 ">>> Verifying overlay integrity..." if ! "$PROJECT_ROOT/local/scripts/verify-overlay-integrity.sh" --quiet; then echo ">>> 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 ">>> Overlay integrity restored." else echo "WARNING: overlay integrity check still failing after repair." echo " Continuing build — some packages may miscompile." fi else echo ">>> 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 ">>> 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 redbear_stash_all_dirty_forks # EXIT trap: always restore stashes (success or failure), always dump # diagnostics, always 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 1: unstash so the operator's working tree is restored. redbear_unstash_all_forks "exit" # 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 "" echo "========================================" echo " BUILD FAILED (exit code: $rc)" echo "========================================" echo "" echo "Diagnostic state preserved at:" echo " $REDBEAR_BUILD_STATE_DIR" echo "" echo "Set REDBEAR_KEEP_BUILD_STATE=1 before running 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 ---" local recipe_dir target_arch 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 stage="$target_arch/stage" stage_tmp="$target_arch/stage.tmp" if [ -d "$stage_tmp" ]; then size=$(du -sh "$stage_tmp" 2>/dev/null | cut -f1) echo " $pkg [$target_arch]: INCOMPLETE (stage.tmp: $size)" elif [ -d "$stage" ]; then echo " $pkg [$target_arch]: complete" fi done done echo "" echo "========================================" echo "" } if [ "$APPLY_PATCHES" = "1" ] && [ -z "${REDBEAR_RELEASE:-}" ]; then echo ">>> 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 ">>> Release mode: fork sources include all patches; no separate patch step needed." fi if [ ! -f "target/release/repo" ]; then echo ">>> 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 ">>> Rebuilding cookbook binary (source changed: ${CURRENT_COOKBOOK_HASH:0:12} != ${STORED_COOKBOOK_HASH:0:12})..." cargo build --release redbear_record_cookbook_fingerprint else echo ">>> 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 ">>> Found $FW_COUNT AMD firmware blobs" else echo ">>> WARNING: No AMD firmware blobs found." echo " Run: ./local/scripts/fetch-firmware.sh" echo " GPU driver will NOT function without firmware." fi echo "" fi echo ">>> Building Red Bear OS with config: $CONFIG" echo ">>> 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 ">>> Stale $src detected (working tree has uncommitted changes); invalidating..." else echo ">>> 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 case "$src" in relibc|base) USERSPACE_RUNTIME_STALE=1 ;; esac 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 ">>> 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 if [ "$USERSPACE_RUNTIME_STALE" = "1" ] && [ "${REDBEAR_SKIP_ABI_STALENESS:-0}" != "1" ]; then echo ">>> Cleaning stale build/sysroot dirs (userspace-linked runtime changed)..." find "$PROJECT_ROOT/recipes" "$PROJECT_ROOT/local/recipes" \ \( -path "*/target/x86_64-unknown-redox/build" \ -o -path "*/target/x86_64-unknown-redox/sysroot" \) \ -type d -exec rm -rf {} + 2>/dev/null || true fi # 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 } invalidate_consumer() { local pkg="$1" 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 } if [ "${REDBEAR_SKIP_ABI_STALENESS:-0}" = "1" ]; then # Fast-iteration escape hatch: skip the entire relibc-consumer # invalidation. Safe ONLY when relibc's ABI is unchanged (e.g. iterating # on a single driver/app recipe). The cookbook's own BLAKE3 dep-hash # cache still rebuilds recipes whose sources changed, so your edited # package re-cooks while everything else stays cached — no 30-min churn # to re-reach it. Unset to restore the stale-binary safety net (which is # required whenever relibc/base actually changed). echo ">>> REDBEAR_SKIP_ABI_STALENESS=1 — skipping relibc-consumer invalidation" echo ">>> (fast iteration; assumes relibc ABI unchanged). Unset to restore it." elif [ "$relibc_rebuilding" = "1" ]; then echo ">>> relibc is rebuilding — it is statically linked into every" echo ">>> userspace binary, so ALL packages are invalidated to force a" echo ">>> clean relink (correctness over speed; prevents stale-binary skew)." for _pkgar in "$REPO_DIR"/*.pkgar; do [ -f "$_pkgar" ] || continue _pkg="$(basename "$_pkgar" .pkgar)" if _rb_preserve_toolchain "$_pkg"; then echo ">>> preserving ABI-inert toolchain pkg '$_pkg' (consumer relinks;" \ "set REDBEAR_FORCE_TOOLCHAIN_RECOOK=1 to force a full re-cook)." continue fi invalidate_consumer "$_pkg" done elif [ -f "$RELIBC_PKGAR" ]; then relibc_ts=$(stat -c %Y "$RELIBC_PKGAR" 2>/dev/null || echo "0") for _pkgar in "$REPO_DIR"/*.pkgar; do [ -f "$_pkgar" ] || continue _pkg="$(basename "$_pkgar" .pkgar)" [ "$_pkg" = "relibc" ] && continue _rb_preserve_toolchain "$_pkg" && continue _pkg_ts=$(stat -c %Y "$_pkgar" 2>/dev/null || echo "0") if [ "$relibc_ts" != "0" ] && [ "$relibc_ts" -gt "$_pkg_ts" ]; then echo ">>> ABI-stale $_pkg (older than relibc.pkgar — interrupted prior build); invalidating..." invalidate_consumer "$_pkg" fi 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 ">>> 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 ">>> Stale $src detected (working tree has uncommitted changes); prefix will be rebuilt." else echo ">>> 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 echo ">>> Auto-rebuilding prefix ($STALE_PREFIX stale fork(s))..." for src in "${STALE_SRCS[@]}"; do touch "$PROJECT_ROOT/local/sources/$src" done # 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 target triple. if ! make prefix TARGET="${TARGET:-x86_64-unknown-redox}" 2>&1 | tail -n 20; then echo ">>> ERROR: prefix rebuild failed. Aborting build to avoid producing broken binaries." >&2 exit 1 fi echo ">>> Prefix rebuilt successfully." # Stamp libc.a to now so the staleness check above does not re-fire on # every subsequent build. `make prefix` extracts a prebuilt toolchain # tarball and is a no-op when the prefix is already present, so without # this touch libc.a keeps its old mtime, stays "older" than the fork's # git commit timestamp, and the (expensive) prefix step runs on every # build forever. Touching after a successful rebuild means we only rebuild # again when the fork actually advances past this point. The uncommitted- # changes (src_dirty) path above is unaffected: it intentionally rebuilds # whenever the working tree is dirty, regardless of mtime. touch "$PREFIX_LIBC" STALE_PREFIX=0 fi if [ "$NO_CACHE" = "1" ]; then echo ">>> 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. echo ">>> 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 icu llvm21 mesa libdrm libepoxy redox-drm lcms2 libdisplay-info libxcvt qtbase qtshadertools qtdeclarative qtsvg qtwayland sddm kwin" else PRECOOK_PKGS="relibc icu" fi 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 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 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 # 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 ">>> redbear-bare target: REDBEAR_BARE_INITFS=1 (auto-set; minimal 7-daemon initfs)" fi if [ "${REDBEAR_ALLOW_UPSTREAM:-0}" = "1" ]; then echo ">>> WARNING: 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 ">>> 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 ">>> 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 ">>> 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 ARCH="${ARCH:-$(uname -m)}" echo "" if [ "$STALE_PREFIX" = "1" ]; then echo ">>> REMINDER: prefix sysroot is stale. Run 'make prefix' to avoid link errors on next build." fi echo "========================================" echo " Build Complete!" echo "========================================" 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