#!/usr/bin/env bash # sync-recipe-source.sh — canonical version-propagation engine for tar recipes. # # THE MISSING STEP. Bumping a recipe's `tar=`/`blake3=` never propagated into the # vendored `source/` tree, so the build silently compiled a stale version (Qt # 6.11.0-vs-6.11.1, the whole KDE stack at 6.10.0 vs recipe 6.28.0). This engine # makes `source.tar`, the vendored `source/` tree, its version stamp, and the # recipe's declared version all agree — treating recipe.toml as the source of # truth (the version to converge ON). # # For each recipe it rebases the vendored fork onto the declared upstream: # source/(Vnew) = pristine(Vnew) + recipe patches + baked local delta # where the "baked local delta" is any change in the current source/ that is NOT # produced by a listed recipe patch (e.g. Redox shims committed directly). That # delta is captured against pristine(Vold) and re-applied onto pristine(Vnew) so # a re-lay can never silently drop a Redox port change. Injected line-duplication # corruption (identical consecutive lines) is dropped. # # Usage: # sync-recipe-source.sh [--check] [--commit] [--all|--config=NAME] ... # --check dry-run: classify each recipe, apply nothing, write nothing # --commit git commit each synced recipe individually # --all every tar recipe under local/recipes (excl. wip) # a recipe dir or bare name (e.g. kde/kf6-kio or kf6-kio) # # Exit: 0 all synced/consistent; 2 some need manual rebase (patches/shims reject). set -uo pipefail ROOT="$(cd "$(dirname "$0")/../.." && pwd)" CHECK=0; COMMIT=0; ALL=0; declare -a WANT=() for a in "$@"; do case "$a" in --check) CHECK=1;; --commit) COMMIT=1;; --all) ALL=1;; --config=*) : ;; # reserved --*) echo "unknown opt: $a" >&2; exit 1;; *) WANT+=("$a");; esac; done WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT ver_of(){ grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)*' <<<"$1" | tail -1; } # resolve a recipe token to its dir resolve(){ local t="$1" [ -f "$ROOT/local/recipes/$t/recipe.toml" ] && { echo "$ROOT/local/recipes/$t"; return; } local hit; hit=$(find "$ROOT/local/recipes" -name recipe.toml -not -path '*/wip/*' -path "*/$t/recipe.toml" 2>/dev/null | head -1) [ -n "$hit" ] && echo "$(dirname "$hit")" } # vendored source/ version marker (KDE KF_VERSION, Qt .cmake.conf, generic stamp, autotools) src_version(){ local s="$1" [ -f "$s/.redbear-src-version" ] && { ver_of "$(cat "$s/.redbear-src-version")"; return; } [ -f "$s/.cmake.conf" ] && { local v; v=$(ver_of "$(grep -m1 QT_REPO_MODULE_VERSION "$s/.cmake.conf")"); [ -n "$v" ] && { echo "$v"; return; }; } [ -f "$s/CMakeLists.txt" ] && { local v; v=$(grep -m1 -E 'set\(KF_VERSION|project\([A-Za-z0-9_]+ VERSION [0-9]' "$s/CMakeLists.txt" 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1); [ -n "$v" ] && { echo "$v"; return; }; } echo "" } # apply a patch trying -p1 then -p0; $1=tree-root $2=patchfile ; echo level or FAIL. # --fuzz=0: require EXACT context (line offsets are still allowed) so a patch # whose context no longer matches the new upstream is REJECTED, not silently # force-fitted with fuzz — that silent force-fit is how a stale/wrong port would # sneak through. apply_patch(){ local root="$1" pf="$2" lvl for lvl in 1 0; do if ( cd "$root" && patch -p$lvl --fuzz=0 --dry-run -f <"$pf" >/dev/null 2>&1 ); then ( cd "$root" && patch -p$lvl --fuzz=0 -s -f <"$pf" >/dev/null 2>&1 ); echo "$lvl"; return 0 fi done echo FAIL; return 1 } # Drop the line-duplication corruption pattern: a run of >=4 identical # consecutive NON-BLANK lines collapses to a single line (the observed # corruption injected 5-13 copies). Runs of <=3 and blank-line runs are kept # verbatim, so legitimate paired lines (`}`/`}`, `#endif`/`#endif`) survive. decorrupt(){ awk ' { a[NR]=$0 } END { i=1 while (i<=NR) { j=i while (j= 4 && a[i]!="") print a[i] else for (k=i;k<=j;k++) print a[k] i=j+1 } }' "$1"; } download(){ curl -sLf --retry 3 --connect-timeout 20 --max-time 600 "$1" -o "$2" 2>/dev/null; } # build the recipe list declare -a RECIPES=() if [ "$ALL" = 1 ]; then while IFS= read -r r; do grep -qE '^[[:space:]]*tar[[:space:]]*=' "$r" && RECIPES+=("$(dirname "$r")"); done \ < <(find "$ROOT/local/recipes" -name recipe.toml -not -path '*/wip/*') else for t in "${WANT[@]}"; do d=$(resolve "$t"); [ -n "$d" ] && RECIPES+=("$d") || echo "!! not found: $t" >&2; done fi printf '%-26s %-9s %-9s %-8s %-6s %s\n' RECIPE FROM TO PATCHES BAKED STATUS RC=0 for dir in "${RECIPES[@]}"; do rc="$dir/recipe.toml"; name="$(basename "$dir")" url="$(grep -m1 -E '^[[:space:]]*tar[[:space:]]*=' "$rc" | sed -E 's/^[^"]*"([^"]+)".*/\1/')"; [ -n "$url" ] || continue vnew="$(ver_of "$(basename "$url")")"; [ -n "$vnew" ] || continue blake="$(grep -m1 -E '^[[:space:]]*blake3[[:space:]]*=' "$rc" | grep -oE '[0-9a-f]{64}')" [ -d "$dir/source" ] || { printf '%-26s %-9s %-9s %-8s %-6s %s\n' "$name" "-" "$vnew" "-" "-" "transient(skip)"; continue; } vold="$(src_version "$dir/source")"; vold="${vold:-unknown}" mapfile -t patches < <(awk '/^[[:space:]]*patches[[:space:]]*=/{p=1} p{print} p&&/\]/{exit}' "$rc" | grep -oE '"[^"]+\.patch"' | tr -d '"') # baked marker files (Redox) not covered by patches baked=$(git -C "$ROOT" grep -lI "__redox__\|Q_OS_REDOX\|defined(__redox" HEAD -- "$dir/source" 2>/dev/null | wc -l | tr -d ' ') if [ "$vold" = "$vnew" ]; then printf '%-26s %-9s %-9s %-8s %-6s %s\n' "$name" "$vold" "$vnew" "${#patches[@]}" "$baked" "consistent" [ "$CHECK" = 1 ] && continue printf '%s\n' "$vnew" > "$dir/source/.redbear-src-version"; continue fi # Version marker unreadable: we cannot prove divergence, so we must NOT rebase # (that could clobber a carefully-ported tree like mesa). Report + skip; the # fix is to add a source/.redbear-src-version stamp once the tree is verified. if [ "$vold" = "unknown" ]; then printf '%-26s %-9s %-9s %-8s %-6s %s\n' "$name" "unknown" "$vnew" "${#patches[@]}" "$baked" "no-marker(skip; stamp to enable)" continue fi # --- obtain pristine Vnew --- base="$WORK/$name-new"; rm -rf "$base"; mkdir -p "$base" tarball="$dir/source.tar"; tv="" [ -f "$tarball" ] && tv="$(ver_of "$(tar tf "$tarball" 2>/dev/null | head -1)")" if [ "$tv" != "$vnew" ]; then download "$url" "$WORK/$name-new.tar" && tarball="$WORK/$name-new.tar" || { printf '%-26s %-9s %-9s %-8s %-6s %s\n' "$name" "$vold" "$vnew" "${#patches[@]}" "$baked" "DL-FAIL"; RC=2; continue; } fi tar xf "$tarball" -C "$base" 2>/dev/null top="$(tar tf "$tarball" 2>/dev/null | head -1 | cut -d/ -f1)" mv "$base/$top" "$base/source" 2>/dev/null || { printf '%-26s %-9s %-9s %s\n' "$name" "$vold" "$vnew" "EXTRACT-FAIL"; RC=2; continue; } # --- apply recipe patches --- preject=0; for p in "${patches[@]}"; do pf="$dir/$p"; [ -f "$pf" ] || pf="$ROOT/local/patches/$p"; [ -f "$pf" ] || { preject=1; continue; } lv=$(apply_patch "$base/source" "$pf"); [ "$lv" = FAIL ] && preject=1 done # --- capture + reapply baked delta (changes beyond patches) --- breject=0 if [ "$baked" -gt 0 ] && [ "$vold" != unknown ]; then oldurl="${url//$vnew/$vold}" if download "$oldurl" "$WORK/$name-old.tar"; then op="$WORK/$name-old"; rm -rf "$op"; mkdir -p "$op"; tar xf "$WORK/$name-old.tar" -C "$op" 2>/dev/null otop="$(tar tf "$WORK/$name-old.tar" 2>/dev/null | head -1 | cut -d/ -f1)"; mv "$op/$otop" "$op/source" 2>/dev/null # for each Redox-marker file uncovered by patches, port its delta onto base while IFS= read -r rel; do rel="${rel#*/source/}" [ -f "$op/source/$rel" ] && [ -f "$base/source/$rel" ] || continue # clean the committed file (drop corruption), diff vs pristine-old, apply to base gitf="$WORK/gitf"; git -C "$ROOT" show "HEAD:$dir/source/$rel" > "$gitf" 2>/dev/null || continue decorrupt "$gitf" > "$gitf.clean" if ! diff -u "$op/source/$rel" "$gitf.clean" 2>/dev/null | patch --fuzz=0 -s "$base/source/$rel" >/dev/null 2>&1; then breject=1; fi done < <(git -C "$ROOT" grep -lI "__redox__\|Q_OS_REDOX\|defined(__redox" HEAD -- "$dir/source" 2>/dev/null) else breject=1; fi fi printf '%s\n' "$vnew" > "$base/source/.redbear-src-version" status="ready"; [ "$preject" = 1 ] && status="PATCH-REJECT(manual)"; [ "$breject" = 1 ] && status="${status};SHIM-REJECT(manual)" [ "$preject" = 1 ] || [ "$breject" = 1 ] && RC=2 printf '%-26s %-9s %-9s %-8s %-6s %s\n' "$name" "$vold" "$vnew" "${#patches[@]}" "$baked" "$status" if [ "$CHECK" = 0 ] && [ "$preject" = 0 ] && [ "$breject" = 0 ]; then # --checksum: compare by content, never by size+mtime. Two files of equal # size with a coincidental mtime (e.g. "v1\n" vs "v2\n") would otherwise be # skipped by rsync's quick-check, leaving stale content in source/. rsync -a --delete --checksum "$base/source/" "$dir/source/" # keep source.tar authoritative for Vnew [ -f "$WORK/$name-new.tar" ] && cp "$WORK/$name-new.tar" "$dir/source.tar" if [ "$COMMIT" = 1 ]; then git -C "$ROOT" add "$dir/source" "$dir/source.tar" >/dev/null 2>&1 git -C "$ROOT" -c commit.gpgsign=false commit -q -m "$name: propagate vendored source/ bump $vold -> $vnew (sync-recipe-source)" -- "$dir" >/dev/null 2>&1 fi fi done exit $RC