24b58896c8
- REGEN_LOCKFILES defaults to 0 (opt-in, not opt-out)
- New --regen flag explicitly enables lockfile regeneration
- New --dry-run flag: snapshots each Cargo.lock, regens, diffs, reverts
Reports per-lockfile changes that WOULD occur without modifying files
- Fixed --regen-only deadlock: CHECK_ONLY=1 no longer blocks regen
(mode_regen_only gate added to CHECK_ONLY skip check)
- Fixed ROOT_DRIFT unbound variable in --regen-only mode
(: ${ROOT_DRIFT:=0} before summary section)
- Moved regen_lockfile_for() definition before dry-run block
(was defined after call site — caused 'command not found')
409 lines
14 KiB
Bash
Executable File
409 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# sync-versions.sh — Synchronise ALL Red Bear crate versions to match the
|
|
# current git branch.
|
|
#
|
|
# Handles BOTH version categories (local/AGENTS.md § "Version conventions"):
|
|
#
|
|
# Cat 1 — In-house Red Bear crates (local/recipes/*/source/):
|
|
# version = "<branch>" (e.g. "0.3.1")
|
|
#
|
|
# Cat 2 — Upstream Redox forks (local/sources/*/):
|
|
# version = "<upstream-base>+rb<branch>" (e.g. "0.9.0+rb0.3.1")
|
|
#
|
|
# Cookbook root — Cargo.toml at repo root:
|
|
# version = "<branch>" (e.g. "0.3.1")
|
|
#
|
|
# The `+rb` suffix is Cargo build metadata, not a pre-release identifier.
|
|
# Using `-rb` is a policy violation.
|
|
#
|
|
# When the branch changes (e.g. 0.3.1 → 0.3.2), running this script bumps
|
|
# Cat 1 versions to the new branch version AND updates Cat 2 suffixes to
|
|
# match the new branch version. The upstream base versions are preserved.
|
|
#
|
|
# Phase 3 (new in Phase 1.4): regenerate Cargo.lock files after the
|
|
# version bump completes. 'cargo generate-lockfile' is run in each fork
|
|
# and the root cookbook. This ensures Cargo.lock drift NEVER accumulates
|
|
# across branch bumps.
|
|
#
|
|
# Exclusions:
|
|
# - zbus (upstream zbus fork, keeps its own versioning)
|
|
# - tlc (established project with independent versioning)
|
|
# - Meson test-case crates
|
|
#
|
|
# Usage:
|
|
# ./local/scripts/sync-versions.sh # Apply version sync (SAFE — no lockfile regen by default)
|
|
# ./local/scripts/sync-versions.sh --regen # Apply sync + regen Cargo.lock files
|
|
# ./local/scripts/sync-versions.sh --check # Check only, exit 1 if drift
|
|
# ./local/scripts/sync-versions.sh --dry-run # Preview: show what --regen WOULD change in lockfiles
|
|
# ./local/scripts/sync-versions.sh --no-regen # Same as default (for backward compat)
|
|
# ./local/scripts/sync-versions.sh --regen-only # Only regenerate Cargo.lock files
|
|
|
|
set -euo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
cd "$ROOT"
|
|
|
|
CHECK_ONLY=0
|
|
REGEN_LOCKFILES=0 # Phase 16.0: safe-by-default — lockfile regen is opt-in
|
|
DRY_RUN=0
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--check) CHECK_ONLY=1 ;;
|
|
--regen) REGEN_LOCKFILES=1 ;;
|
|
--no-regen) REGEN_LOCKFILES=0 ;;
|
|
--regen-only) REGEN_LOCKFILES=1; mode_regen_only=1 ;;
|
|
--dry-run) DRY_RUN=1; REGEN_LOCKFILES=1 ;; # dry-run implies --regen
|
|
-h|--help) sed -n '1,45p' "$0"; exit 0 ;;
|
|
*) echo "Unknown option: $arg" >&2; exit 1 ;;
|
|
esac
|
|
done
|
|
[[ "${mode_regen_only:-0}" == "1" ]] && CHECK_ONLY=1 # skip version sync
|
|
|
|
# ---- Determine target version from branch ----
|
|
|
|
BRANCH="$(git branch --show-current 2>/dev/null || echo "")"
|
|
if [[ -z "$BRANCH" ]]; then
|
|
echo "ERROR: cannot determine current git branch" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Extract semantic version from branch name (e.g. "0.2.5" from "0.2.5")
|
|
TARGET_VERSION="$(echo "$BRANCH" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || true)"
|
|
if [[ -z "$TARGET_VERSION" ]]; then
|
|
echo "ERROR: branch '$BRANCH' does not contain a semantic version" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "Target version: $TARGET_VERSION (from branch '$BRANCH')"
|
|
echo "Cat 2 suffix: +rb$TARGET_VERSION"
|
|
echo ""
|
|
|
|
# ---- Exclusions ----
|
|
|
|
# Crates that keep their own versioning (not branch-tracked)
|
|
EXCLUDE_NAMES=(
|
|
"zbus" # upstream zbus fork
|
|
"tlc" # established project with independent versioning
|
|
)
|
|
|
|
# Paths to exclude (substring match)
|
|
EXCLUDE_PATHS=(
|
|
"test cases" # meson test crates
|
|
"/target/" # build artifacts
|
|
)
|
|
|
|
should_exclude() {
|
|
local name="$1"
|
|
local path="$2"
|
|
|
|
for ex in "${EXCLUDE_NAMES[@]}"; do
|
|
[[ "$name" == "$ex" ]] && return 0
|
|
done
|
|
|
|
for ex in "${EXCLUDE_PATHS[@]}"; do
|
|
[[ "$path" == *"$ex"* ]] && return 0
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
# ---- Helper: extract upstream base from a Cat 2 version ----
|
|
# Strips any existing +rb<x.y.z> build-metadata suffix to get the upstream base version.
|
|
strip_rb_suffix() {
|
|
echo "$1" | sed -E 's/(\+rb[0-9]+(\.[0-9]+\.[0-9]+)?)+$//'
|
|
}
|
|
|
|
# ---- Phase 0: Cookbook root Cargo.toml ----
|
|
# Per Phase 0 audit: root Cargo.toml's [package] version was historically
|
|
# outside this script's scope and drifted to 0.2.5 on branch 0.3.1. The
|
|
# cookbook crate (redbear_cookbook) now sets its version to the current
|
|
# git branch — keep it in lock-step.
|
|
|
|
if [[ "${mode_regen_only:-0}" != "1" ]]; then
|
|
echo "=== Phase 0: Cookbook root Cargo.toml ==="
|
|
|
|
ROOT_DRIFT=0
|
|
ROOT_CTL="Cargo.toml"
|
|
if [ -f "$ROOT_CTL" ] && grep -qE '^\[package\]' "$ROOT_CTL" 2>/dev/null; then
|
|
root_name=$(grep '^name = ' "$ROOT_CTL" | head -1 | sed 's/name = "//;s/"//')
|
|
root_current=$(grep -E '^version\s*=' "$ROOT_CTL" | head -1 | sed 's/.*"\(.*\)".*/\1/' || true)
|
|
if [ -n "$root_current" ] && [ "$root_current" != "$TARGET_VERSION" ]; then
|
|
if [[ $CHECK_ONLY -eq 1 ]]; then
|
|
echo " DRIFT: $root_name $root_current -> $TARGET_VERSION ($ROOT_CTL)"
|
|
ROOT_DRIFT=1
|
|
else
|
|
sed -i "s/^version = \"${root_current}\"/version = \"${TARGET_VERSION}\"/" "$ROOT_CTL"
|
|
echo " UPDATED: $root_name $root_current -> $TARGET_VERSION ($ROOT_CTL)"
|
|
fi
|
|
fi
|
|
fi
|
|
echo ""
|
|
fi
|
|
|
|
# ---- Phase 1: Cat 1 — In-house crates (local/recipes/*/source/) ----
|
|
|
|
echo "=== Phase 1: Cat 1 — In-house crates ==="
|
|
|
|
CAT1_CHANGED=0
|
|
CAT1_CHECKED=0
|
|
CAT1_DRIFT=0
|
|
|
|
while IFS= read -r f; do
|
|
name="$(grep '^name = ' "$f" 2>/dev/null | head -1 | sed 's/name = "//;s/"//' || true)"
|
|
|
|
if should_exclude "$name" "$f"; then
|
|
continue
|
|
fi
|
|
|
|
CAT1_CHECKED=$((CAT1_CHECKED + 1))
|
|
|
|
# Case 1: [package] version = "x.y.z"
|
|
if grep -q '^version = "' "$f" 2>/dev/null; then
|
|
current="$(grep '^version = "' "$f" | head -1 | sed 's/version = "//;s/"//' || true)"
|
|
if [[ "$current" != "$TARGET_VERSION" ]]; then
|
|
if [[ $CHECK_ONLY -eq 1 ]]; then
|
|
echo " DRIFT: $name $current → $TARGET_VERSION ($f)"
|
|
CAT1_DRIFT=$((CAT1_DRIFT + 1))
|
|
else
|
|
sed -i "s/^version = \"$current\"/version = \"$TARGET_VERSION\"/" "$f"
|
|
echo " UPDATED: $name $current → $TARGET_VERSION ($f)"
|
|
CAT1_CHANGED=$((CAT1_CHANGED + 1))
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Case 2: [workspace.package] version = "x.y.z"
|
|
if grep -A1 '^\[workspace\.package\]' "$f" 2>/dev/null | grep -q '^version = "' 2>/dev/null; then
|
|
current="$(grep -A1 '^\[workspace\.package\]' "$f" | grep '^version = "' | sed 's/version = "//;s/"//' || true)"
|
|
if [[ "$current" != "$TARGET_VERSION" ]]; then
|
|
if [[ $CHECK_ONLY -eq 1 ]]; then
|
|
echo " DRIFT (workspace): $current → $TARGET_VERSION ($f)"
|
|
CAT1_DRIFT=$((CAT1_DRIFT + 1))
|
|
else
|
|
awk -v new="$TARGET_VERSION" '
|
|
/^\[workspace\.package\]/ { in_wp=1 }
|
|
/^\[/ && !/^\[workspace\.package\]/ { in_wp=0 }
|
|
in_ws==0 && in_wp==1 && /^version = / { print "version = \"" new "\""; next }
|
|
{ print }
|
|
' "$f" > "${f}.tmp" && mv "${f}.tmp" "$f"
|
|
echo " UPDATED (workspace): $current → $TARGET_VERSION ($f)"
|
|
CAT1_CHANGED=$((CAT1_CHANGED + 1))
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
done < <(find local/recipes/ -name "Cargo.toml" -not -path "*/target/*" -path "*/source/*" | sort)
|
|
|
|
echo ""
|
|
|
|
# ---- Phase 2: Cat 2 — Upstream forks (local/sources/*/) ----
|
|
|
|
echo "=== Phase 2: Cat 2 — Upstream forks ==="
|
|
|
|
CAT2_CHANGED=0
|
|
CAT2_CHECKED=0
|
|
CAT2_DRIFT=0
|
|
|
|
RB_SUFFIX="+rb${TARGET_VERSION}"
|
|
|
|
update_cat2_version() {
|
|
local f="$1"
|
|
local fork_name="$2"
|
|
|
|
if ! grep -qE '^[[:space:]]*\[(package|workspace\.package)\]' "$f" 2>/dev/null; then
|
|
return 0
|
|
fi
|
|
|
|
local current target base
|
|
current="$(grep -E '^version\s*=' "$f" | head -1 | sed 's/.*"\(.*\)".*/\1/' || true)"
|
|
[[ -n "$current" ]] || return 0
|
|
|
|
# Strip any existing +rb<x.y.z> build-metadata suffix to get upstream base
|
|
base="$(strip_rb_suffix "$current")"
|
|
target="${base}${RB_SUFFIX}"
|
|
|
|
if [[ "$current" == "$target" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
CAT2_CHECKED=$((CAT2_CHECKED + 1))
|
|
|
|
if [[ $CHECK_ONLY -eq 1 ]]; then
|
|
echo " DRIFT: $fork_name $current → $target ($f)"
|
|
CAT2_DRIFT=$((CAT2_DRIFT + 1))
|
|
else
|
|
sed -i "s|^version = \"${current}\"|version = \"${target}\"|" "$f"
|
|
echo " UPDATED: $fork_name $current → $target ($f)"
|
|
CAT2_CHANGED=$((CAT2_CHANGED + 1))
|
|
fi
|
|
}
|
|
|
|
for fork_dir in local/sources/*/; do
|
|
[ -d "$fork_dir" ] || continue
|
|
fork_name=$(basename "$fork_dir")
|
|
# Skip non-submodule dirs (ctrlc, libpciaccess, redox-drm, sysinfo — empty stubs)
|
|
[ -f "${fork_dir}Cargo.toml" ] || continue
|
|
|
|
# Try root Cargo.toml first
|
|
update_cat2_version "${fork_dir}Cargo.toml" "$fork_name"
|
|
done
|
|
|
|
echo ""
|
|
|
|
# ---- Summary ----
|
|
|
|
: ${ROOT_DRIFT:=0}
|
|
|
|
if [[ $CHECK_ONLY -eq 1 ]]; then
|
|
echo "Cat 0 (cookbook root): drift $ROOT_DRIFT"
|
|
echo "Cat 1: checked $CAT1_CHECKED, drift $CAT1_DRIFT"
|
|
echo "Cat 2: checked $CAT2_CHECKED, drift $CAT2_DRIFT"
|
|
TOTAL_DRIFT=$((CAT1_DRIFT + CAT2_DRIFT + ROOT_DRIFT))
|
|
if [[ $TOTAL_DRIFT -gt 0 ]]; then
|
|
echo "FAIL: $TOTAL_DRIFT version drift(s) found"
|
|
exit 1
|
|
fi
|
|
echo "All crates are at correct versions for branch '$BRANCH'"
|
|
else
|
|
echo "Cat 0 (cookbook root): updated if drifted"
|
|
echo "Cat 1: updated $CAT1_CHANGED (checked $CAT1_CHECKED)"
|
|
echo "Cat 2: updated $CAT2_CHANGED"
|
|
echo "All versions synced to branch '$BRANCH'"
|
|
fi
|
|
|
|
# ---- Phase 3: regenerate Cargo.lock files ----
|
|
# Per Phase 0 audit: Cargo.lock files accumulated drift after branch bumps.
|
|
# Each fork's Cargo.lock failed to refresh when its Cargo.toml's `+rb`
|
|
# suffix changed, producing version-resolution bugs.
|
|
#
|
|
# Run `cargo generate-lockfile` in the root cookbook and each Cat 2 fork
|
|
# that has a Cargo.lock. The path-dep + [patch.crates-io] config in each
|
|
# fork ensures the right crates are pulled in.
|
|
#
|
|
# Skip when --check (but NOT --regen-only: that opts IN to regen)
|
|
# Skip when --no-regen is active (REGEN_LOCKFILES=0)
|
|
|
|
if [[ $CHECK_ONLY -eq 1 && "${mode_regen_only:-0}" != "1" ]]; then
|
|
echo ""
|
|
echo "(lockfile regeneration skipped in --check mode)"
|
|
exit 0
|
|
fi
|
|
if [[ $REGEN_LOCKFILES -eq 0 ]]; then
|
|
echo ""
|
|
echo "(lockfile regeneration skipped — use --regen to enable)"
|
|
exit 0
|
|
fi
|
|
|
|
# ---- Phase 3: Lockfile regeneration utilities ----
|
|
|
|
REGEN_DONE=0
|
|
REGEN_FAILED=0
|
|
|
|
regen_lockfile_for() {
|
|
local dir="$1"
|
|
local label="$2"
|
|
if [ ! -d "$dir" ]; then return; fi
|
|
cd "$dir"
|
|
if [ ! -f "Cargo.toml" ]; then
|
|
cd "$ROOT"
|
|
return
|
|
fi
|
|
if [ -f "Cargo.lock" ]; then
|
|
rm -f Cargo.lock
|
|
fi
|
|
if cargo generate-lockfile >/dev/null 2>&1; then
|
|
echo " $label: regenerated Cargo.lock"
|
|
REGEN_DONE=$((REGEN_DONE + 1))
|
|
else
|
|
echo " $label: FAILED to regenerate Cargo.lock" >&2
|
|
REGEN_FAILED=$((REGEN_FAILED + 1))
|
|
fi
|
|
cd "$ROOT"
|
|
}
|
|
|
|
# ---- Phase 3: Dry-run preview (--dry-run flag) ----
|
|
# Snapshot each fork's Cargo.lock, regen, show diff, then revert.
|
|
# This lets operators preview lockfile changes before a real branch bump.
|
|
if [[ $DRY_RUN -eq 1 ]]; then
|
|
echo ""
|
|
echo "=== Phase 3: Dry-run lockfile regen preview ==="
|
|
echo "(existing Cargo.lock files will NOT be modified)"
|
|
echo ""
|
|
|
|
# Preserve all lockfiles first
|
|
declare -A LOCK_BACKUPS=()
|
|
for lock_dir in "$ROOT" local/sources/*/; do
|
|
[ -d "$lock_dir" ] || continue
|
|
[ -f "${lock_dir}/Cargo.toml" ] || continue
|
|
lock="${lock_dir}/Cargo.lock"
|
|
if [ -f "$lock" ]; then
|
|
LOCK_BACKUPS["$lock"]=$(cat "$lock")
|
|
fi
|
|
done
|
|
|
|
# Regenerate in-place (reverts after)
|
|
regen_lockfile_for "$ROOT" "root"
|
|
for fork_dir in local/sources/*/; do
|
|
[ -d "$fork_dir" ] || continue
|
|
fork_name=$(basename "$fork_dir")
|
|
[ -f "${fork_dir}Cargo.toml" ] || continue
|
|
regen_lockfile_for "${fork_dir%/}" "local/sources/${fork_name}"
|
|
done
|
|
|
|
# Show diffs
|
|
changed=0
|
|
echo ""
|
|
echo "--- Lockfile changes that would occur on a real bump ---"
|
|
for lock in "${!LOCK_BACKUPS[@]}"; do
|
|
backup="${LOCK_BACKUPS[$lock]}"
|
|
current=""
|
|
[ -f "$lock" ] && current=$(cat "$lock")
|
|
if [ "$backup" != "$current" ]; then
|
|
changed=$((changed + 1))
|
|
label="${lock#$ROOT/}"
|
|
echo ""
|
|
echo " === $label ==="
|
|
diff <(echo "$backup") <(echo "$current") | head -30
|
|
if [ $(diff <(echo "$backup") <(echo "$current") | wc -l) -gt 30 ]; then
|
|
echo " ... (truncated — $(( $(diff <(echo "$backup") <(echo "$current") | wc -l) - 30 )) more lines)"
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# Restore originals
|
|
for lock in "${!LOCK_BACKUPS[@]}"; do
|
|
echo "${LOCK_BACKUPS[$lock]}" > "$lock"
|
|
done
|
|
|
|
echo ""
|
|
echo "=== Dry-run summary ==="
|
|
echo " Lockfiles with changes: $changed"
|
|
if [ $changed -eq 0 ]; then
|
|
echo " ALL CLEAR — no lockfile changes would occur. Safe to --regen."
|
|
else
|
|
echo " Review the diffs above. If acceptable, run: $0 --regen"
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
echo ""
|
|
echo "=== Phase 3: Regenerate Cargo.lock files ==="
|
|
|
|
# Regenerate root cookbook lockfile
|
|
regen_lockfile_for "$ROOT" "root"
|
|
|
|
# Regenerate each Cat 2 fork's lockfile (skipped if not a Cargo crate)
|
|
for fork_dir in local/sources/*/; do
|
|
[ -d "$fork_dir" ] || continue
|
|
fork_name=$(basename "$fork_dir")
|
|
[ -f "${fork_dir}Cargo.toml" ] || continue
|
|
regen_lockfile_for "${fork_dir%/}" "local/sources/${fork_name}"
|
|
done
|
|
|
|
echo ""
|
|
echo "Lockfile regeneration: $REGEN_DONE ok, $REGEN_FAILED failed"
|
|
if [[ $REGEN_FAILED -gt 0 ]]; then
|
|
echo " Run ./local/scripts/sync-versions.sh --no-regen and inspect failure logs."
|
|
exit 1
|
|
fi
|