Files
RedBear-OS/local/scripts/build-preflight.sh
T
vasilito fd11aa716c
redbear-ci / check (push) Has been cancelled
feat: port the missing KDE Plasma dependency chain and harden the auth stack
Unblocks plasma-workspace/plasma-desktop, which required 14 packages that had
no in-tree recipe. Sources are reproducible from each recipe's tar= + blake3;
the vendored source/ trees are deliberately not committed here (212M).

New recipes:
  KF6 6.28.0   kf6-kholidays, kf6-krunner, kf6-kstatusnotifieritem,
               kf6-kunitconversion
  Plasma 6.7.2 knighttime, layer-shell-qt, libkscreen, libksysguard,
               plasma-activities-stats, plasma5support, kscreenlocker
  Qt 6.11.1    qtpositioning, qtspeech, qttools

libksysguard carries P0-redox-process-backend.patch: processes_local_p.cpp
dispatches on platform macros and had no __redox__ arm, so ProcessesLocal was
entirely undefined. Adds a real backend reading /scheme/proc/ps (pid, ppid,
real+effective ids, thread count, state) and /scheme/sys/mem, with kill() for
signals. Upstream's generic fallback is a pure stub and was not used. Absent
facilities (no setpriority/sched_setscheduler/ioprio on a microkernel) report
NotSupported rather than pretending.

Restored, no longer disabled:
  - night colour: kcms/nighttime + kwin's nightlight plugin, now that
    KNightTime/Qt6Positioning/KF6Holidays exist
  - KIO FileWidgets (file dialog, places model), wrongly swept in with the
    unportable kiod/kssld/kioworkers subdirs
  - kf6-ktexteditor text-to-speech: removes a disguised stub that rewrote
    speechEngine() to return nullptr and mangled call sites into invalid C++
  - kwin KWIN_BUILD_SCREENLOCKER=ON (needs kscreenlocker; see below)

Toolchain and recipe fixes:
  - redox-toolchain.cmake: append -lgcc. __extendhfsf2/__extendbfsf2 are
    global in libgcc.a but hidden in libgcc_s.so (the Redox libgcc version map
    stops at GCC_7.0.0 and never emits the GCC_12/13 nodes upstream exports),
    and GCC omits -lgcc for -shared, so no C++23 shared library using
    std::format<_Float16|bfloat16_t> could link.
  - qtmultimedia: rewrite /usr/src/.../meta_types paths and stage metatypes,
    so consumers using qt_internal_add_qml_module can configure.
  - qtspeech: qtmultimedia is mandatory, not optional -- upstream return()s
    with only a NOTICE without it, yielding a green build over an empty
    package. Asserts its own output so that cannot recur.
  - narrow Linux-only guards with AND NOT REDOX where the toolchain sets
    CMAKE_SYSTEM_NAME=Linux purely to get UNIX=TRUE (kio LibMount,
    libksysguard NL/Sensors, plasma-workspace NetworkManagerQt).
  - drop REQUIRED components that are declared but referenced nowhere
    (Location, QCoro6, Qt6 Test) -- verified by exhaustive grep.
  - DESTDIR installs where KDE emits absolute KDE_INSTALL_FULL_* paths that
    --prefix cannot re-root, which otherwise write into the build host.

Auth stack:
  - pam-redbear: add PAM_MODULE_UNKNOWN (28), missing from both the header and
    lib.rs; realign 30/31 to Linux-PAM's PAM_CONV_AGAIN/PAM_INCOMPLETE, which
    previously held Red Bear-only names on standard values, so anything built
    against stock PAM headers mis-decoded them.
  - redbear-authd: support yescrypt ($y$ -- the default shadow format on
    current Debian/Ubuntu/Fedora, previously an unexplained login failure),
    bcrypt and md5-crypt. Plaintext shadow entries now require the
    /etc/redbear/allow-plaintext-passwords sentinel and warn on every use,
    instead of being compared silently. 19/19 host tests pass.

Build-system hardening:
  - verify-tracked-sources.sh, wired into preflight: fails on deletion of any
    tracked vendored source, and on modifications with no baseline entry.
    local/sources/ had a dirty gate and version checks; local/recipes/*/source/
    had none, which allowed an rm -rf to delete 7958 tracked files and a
    pristine re-extract to silently overwrite committed fixes (kwin's
    std::expected/vulkan-hpp and X11 gating, kio's Q_OS_REDOX resolver guards).
  - validate-source-trees.py: resolve recipe.toml through the overlay symlink;
    it reported false MISSING for symlinked recipes.
  - test-sddm-virgl-qemu.sh: the null-proxy check used report(), which PASSES
    on regex match -- so the known Qt6 Wayland null+8 fault would report PASS
    when present. Added report_absent().

kscreenlocker builds only its cmake-level Wayland-only changes so far; the
C++ X11 removal (61 call sites) is not yet applied, so kwin's screenlocker
flag is blocked on it.
2026-08-04 10:06:39 +03:00

331 lines
15 KiB
Bash
Executable File

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