#!/usr/bin/env bash # rebuild-cascade.sh — Rebuild a package and all packages that depend on it # # Usage: # ./local/scripts/rebuild-cascade.sh [package2 ...] # ./local/scripts/rebuild-cascade.sh --dry-run # # When a low-level package (e.g. relibc) changes, every package that # transitively depends on it must be rebuilt. This script: # 1. Identifies all packages that depend on the target(s) # 2. Cooks the target package(s) first # 3. Cooks all dependents in dependency order # 4. Pushes all rebuilt packages to the sysroot # # A package is considered a dependent if its recipe.toml lists the target # in its [package].dependencies array, OR if its recipe.toml has # dependencies = [...] that includes the target. # # Exit codes: # 0 — all packages rebuilt and pushed successfully # 1 — one or more packages failed to build set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)" REPO_BIN="${ROOT_DIR}/target/release/repo" DRY_RUN=0 PACKAGES=() for arg in "$@"; do case "$arg" in --dry-run|-n) DRY_RUN=1 ;; --help|-h) echo "Usage: $0 [--dry-run] [package2 ...]" echo "" echo "Rebuilds the named package(s) and all packages that transitively" echo "depend on them, then pushes all to the sysroot." exit 0 ;; -*) echo "Unknown option: $arg" >&2 exit 1 ;; *) PACKAGES+=("$arg") ;; esac done if [ ${#PACKAGES[@]} -eq 0 ]; then echo "Usage: $0 [--dry-run] [package2 ...]" >&2 exit 1 fi # Build the repo binary if needed if [ ! -x "${REPO_BIN}" ]; then echo "Building repo binary..." (cd "${ROOT_DIR}" && cargo build --release --bin repo) fi cd "${ROOT_DIR}" # Precompute the entire dependency graph in a single pass. With 3000+ recipes, # doing per-target awk invocations in the BFS loop is O(N²) and unworkable. # Precompute once, query in O(1). Function definitions must come before # their use; place them here so the index build below can call them. extract_recipe_deps() { local pkg_dir="$1" local recipe_toml="${pkg_dir}/recipe.toml" local deps="" if [ ! -f "${recipe_toml}" ]; then echo "" return fi # Both [package] and [build] sections contribute deps. The cookbook's # get_build_deps_recursive() walks both; this script must match. local in_section="" local in_deps=0 while IFS= read -r line; do if [[ "${line}" =~ ^[[:space:]]*\[([^]]+)\][[:space:]]*$ ]]; then local sect="${BASH_REMATCH[1]// /}" if [ "${sect}" = "package" ] || [ "${sect}" = "build" ]; then in_section="${sect}" else in_section="" fi in_deps=0 continue fi [ -z "${in_section}" ] && continue if [ "${in_deps}" = 1 ]; then local item="${line}" item="${item## }"; item="${item%% }" item="${item#\"}"; item="${item%\"}" item="${item%,}" [ -n "${item}" ] && deps+="${item}," if [[ "${line}" =~ ^[[:space:]]*\] ]]; then in_deps=0 fi continue fi if [[ "${line}" =~ ^[[:space:]]*(dependencies|dev_dependencies)[[:space:]]*=[[:space:]]*\[ ]]; then in_deps=1 local inline="${line#*=}" inline="${inline#[}" if [[ "${inline}" == *\]* ]]; then in_deps=0 inline="${inline%]*}" for item in ${inline//,/ }; do item="${item## }"; item="${item%% }" item="${item#\"}"; item="${item%\"}" [ -n "${item}" ] && deps+="${item}," done fi fi done < "${recipe_toml}" echo "${deps}" } recipe_source_dir() { local pkg_dir="$1" local recipe_toml="${pkg_dir}/recipe.toml" [ -f "${recipe_toml}" ] || return 0 local rel rel="$(awk '/^\[source\]/{flag=1; next} /^\[/{flag=0} flag' \ "${recipe_toml}" 2>/dev/null | \ awk -F'=' '/^path[[:space:]]*=/{gsub(/[" ]/, "", $2); print $2; exit}')" if [ -n "${rel}" ]; then (cd "${pkg_dir}" && cd "${rel}" 2>/dev/null && pwd) fi } mapfile -t ALL_RECIPE_TOMLS < <(find recipes/ local/recipes/ -name "recipe.toml" 2>/dev/null) echo "Cached ${#ALL_RECIPE_TOMLS[@]} recipe.toml paths" # recipe_index maps pkg_name -> "pkg_dir|recipe_toml|depends_csv" declare -A recipe_index=() for recipe_toml in "${ALL_RECIPE_TOMLS[@]}"; do pkg_dir="$(dirname "${recipe_toml}")" pkg_name="$(basename "${pkg_dir}")" deps="$(extract_recipe_deps "${pkg_dir}")" recipe_index["${pkg_name}"]="${pkg_dir}|${recipe_toml}|${deps}" done # Find all recipes that depend on the target by querying the precomputed # index. Returns newline-separated pkg names that depend on target. find_reverse_deps() { local target="$1" local result=() # Empty target would match every empty-deps entry — reject early. if [ -z "${target}" ]; then return 0 fi local pkg_name entry for pkg_name in "${!recipe_index[@]}"; do if [ "${pkg_name}" = "${target}" ]; then continue fi entry="${recipe_index[${pkg_name}]}" # entry format: "pkg_dir|recipe_toml|deps_csv" local deps_csv="${entry##*|}" if [[ ",${deps_csv}," == *",${target},"* ]]; then result+=("${pkg_name}") fi done if [ ${#result[@]} -gt 0 ]; then printf '%s\n' "${result[@]}" | sort -u fi } # Validate that the requested package names exist in the index for pkg in "${PACKAGES[@]}"; do if [ -z "${recipe_index[${pkg}]+_}" ]; then echo "ERROR: recipe not found for package '${pkg}'" >&2 exit 1 fi done # Build a complete cascade set using BFS echo "=== Analyzing dependency cascade ===" CASCADE=() VISITED=() QUEUE=("${PACKAGES[@]}") while [ ${#QUEUE[@]} -gt 0 ]; do current="${QUEUE[0]}" QUEUE=("${QUEUE[@]:1}") # Skip if already visited for v in "${VISITED[@]}"; do if [ "$v" = "$current" ]; then continue 2 fi done VISITED+=("$current") # Find packages that depend on current mapfile -t rdeps < <(find_reverse_deps "$current" 2>/dev/null || true) for dep in "${rdeps[@]}"; do CASCADE+=("${dep}") QUEUE+=("${dep}") done done # Remove duplicates and sort UNIQUE_CASCADE=($(printf '%s\n' "${CASCADE[@]}" | sort -u)) # Build order: cook the target packages first, then dependents BUILD_ORDER=("${PACKAGES[@]}" "${UNIQUE_CASCADE[@]}") TOTAL=${#BUILD_ORDER[@]} echo "" echo "=== Cascade rebuild plan (${TOTAL} packages) ===" for i in "${!BUILD_ORDER[@]}"; do pkg="${BUILD_ORDER[$i]}" if printf '%s\n' "${PACKAGES[@]}" | grep -q "^${pkg}$"; then echo " [$((i+1))/${TOTAL}] ${pkg} (ROOT)" else echo " [$((i+1))/${TOTAL}] ${pkg} (dependent)" fi done if [ $DRY_RUN -eq 1 ]; then echo "" echo "=== Dry run — no packages will be built ===" exit 0 fi echo "" echo "=== Starting cascade rebuild ===" FAILED=() BUILT=() export REDOXER_TOOLCHAIN="${ROOT_DIR}/prefix/x86_64-unknown-redox/relibc-install" for i in "${!BUILD_ORDER[@]}"; do pkg="${BUILD_ORDER[$i]}" echo "" echo "--- [$((i+1))/${TOTAL}] Building ${pkg} ---" if "${REPO_BIN}" cook "${pkg}"; then BUILT+=("${pkg}") echo "--- ${pkg} built successfully ---" else FAILED+=("${pkg}") echo "ERROR: ${pkg} failed to build" >&2 echo "Continuing with remaining packages..." fi done echo "" echo "=== Pushing ${#BUILT[@]} built packages to sysroot ===" for pkg in "${BUILT[@]}"; do echo "Pushing ${pkg}..." "${REPO_BIN}" push "${pkg}" || echo "WARNING: push ${pkg} failed" done echo "" echo "=== Cascade rebuild summary ===" echo " Built: ${#BUILT[@]}/${TOTAL}" echo " Failed: ${#FAILED[@]}/${TOTAL}" if [ ${#FAILED[@]} -gt 0 ]; then echo " Failed packages:" for pkg in "${FAILED[@]}"; do echo " - ${pkg}" done exit 1 fi echo " All packages rebuilt and pushed successfully." exit 0