Harden apply-patches.sh and sync-upstream.sh against local/ data loss

apply-patches.sh: add --dry-run flag, make patch conflicts fail loudly instead of silently skipping, back up WIP directories instead of rm -rf, refuse to overwrite existing config files. sync-upstream.sh: add --force flag, abort on uncommitted local/ changes unless forced, stash with -u for untracked file protection, add pre-rebase overlay integrity check, improve nuclear option and stash pop guidance.
This commit is contained in:
2026-04-22 22:00:17 +01:00
parent 07c97ebaf8
commit 9820a860ac
2 changed files with 179 additions and 20 deletions
+86 -16
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# apply-patches.sh — Apply all Red Bear OS overlays on top of upstream Redox build system. # apply-patches.sh — Apply all Red Bear OS overlays on top of upstream Redox build system.
# #
# Usage: ./local/scripts/apply-patches.sh [--force] # Usage: ./local/scripts/apply-patches.sh [--force] [--dry-run]
# #
# This script: # This script:
# 1. Applies build-system patches (rebranding, cookbook fixes, config, docs) # 1. Applies build-system patches (rebranding, cookbook fixes, config, docs)
@@ -13,6 +13,7 @@
# instead. This script therefore treats the local overlay as the durable source of truth. # instead. This script therefore treats the local overlay as the durable source of truth.
# #
# With --force: reapplies even if patches appear already applied. # With --force: reapplies even if patches appear already applied.
# With --dry-run: shows what would change without making changes.
# #
# SAFE: does not touch local/ directory. Only modifies upstream files. # SAFE: does not touch local/ directory. Only modifies upstream files.
@@ -21,11 +22,37 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
PATCHES_DIR="$REPO_ROOT/local/patches" PATCHES_DIR="$REPO_ROOT/local/patches"
FORCE="${1:-}" FORCE=""
DRY_RUN=0
cd "$REPO_ROOT" cd "$REPO_ROOT"
# ── Helper ────────────────────────────────────────────────────────── # ── Helper ──────────────────────────────────────────────────────────
usage() {
cat <<'EOF'
Usage: ./local/scripts/apply-patches.sh [--force] [--dry-run]
Options:
--force attempt re-application even when checks fail
--dry-run show what would change without modifying files
EOF
}
for arg in "$@"; do
case "$arg" in
--force) FORCE="--force" ;;
--dry-run) DRY_RUN=1 ;;
-h|--help)
usage
exit 0
;;
*)
usage
exit 1
;;
esac
done
symlink() { symlink() {
local target="$1" link="$2" local target="$1" link="$2"
if [ -L "$link" ]; then if [ -L "$link" ]; then
@@ -34,32 +61,56 @@ symlink() {
return 0 # already correct return 0 # already correct
fi fi
fi fi
if [ "$DRY_RUN" = "1" ]; then
dry_run_msg "would remove and relink $link -> $target"
return 0
fi
rm -f "$link" rm -f "$link"
ln -s "$target" "$link" ln -s "$target" "$link"
echo " linked $link -> $target" echo " linked $link -> $target"
} }
dry_run_msg() {
if [ "$DRY_RUN" = "1" ]; then
echo " [dry-run] $*"
fi
}
# ── 1. Build-system patches ───────────────────────────────────────── # ── 1. Build-system patches ─────────────────────────────────────────
echo "==> Applying build-system patches..." echo "==> Applying build-system patches..."
for patch_file in "$PATCHES_DIR"/build-system/[0-9]*.patch; do for patch_file in "$PATCHES_DIR"/build-system/[0-9]*.patch; do
[ -f "$patch_file" ] || continue [ -f "$patch_file" ] || continue
patch_name="$(basename "$patch_file")" patch_name="$(basename "$patch_file")"
# Check if already applied (skip unless --force) # Check if patch applies cleanly
if [ "$FORCE" != "--force" ]; then if ! git apply --check "$patch_file" 2>/dev/null; then
if git apply --check "$patch_file" 2>/dev/null; then # Patch does NOT apply cleanly — check if it's already applied
: # patch applies cleanly, apply it if git apply --reverse --check "$patch_file" 2>/dev/null; then
else echo " SKIP $patch_name (already applied)"
echo " SKIP $patch_name (already applied or conflicts)"
echo " Use --force to attempt re-application"
continue continue
fi fi
# Patch conflicts and is NOT already applied — this is a real problem
echo " FAIL $patch_name — conflicts with current tree"
echo " This likely means upstream has changed the target files."
echo " Patch file: $patch_file"
echo " Options:"
echo " 1. Resolve conflicts: edit the patch file to match new upstream"
echo " 2. Use --force to attempt re-application (may fail)"
if [ "$FORCE" != "--force" ]; then
echo " ABORT: unresolved patch conflict (use --force to override)"
exit 1
fi
fi
if [ "$DRY_RUN" = "1" ]; then
dry_run_msg "would apply $patch_name"
continue
fi fi
if git apply --whitespace=nowarn "$patch_file"; then if git apply --whitespace=nowarn "$patch_file"; then
echo " OK $patch_name" echo " OK $patch_name"
else else
echo " FAIL $patch_nameresolve conflicts manually" echo " FAIL $patch_nameapply failed (check conflicts above)"
echo " Patch file: $patch_file" echo " Patch file: $patch_file"
exit 1 exit 1
fi fi
@@ -138,7 +189,13 @@ symlink "../../local/recipes/core/grub" "recipes/core/grub"
# so redirect the entire directory to our local overlay to ensure # so redirect the entire directory to our local overlay to ensure
# COOKBOOK_RECIPE resolves to a directory that contains grub.cfg # COOKBOOK_RECIPE resolves to a directory that contains grub.cfg
if [ -d "recipes/wip/services/grub" ] && [ ! -L "recipes/wip/services/grub" ]; then if [ -d "recipes/wip/services/grub" ] && [ ! -L "recipes/wip/services/grub" ]; then
rm -rf "recipes/wip/services/grub" backup_name="recipes/wip/services/grub.upstream-backup-$(date +%Y%m%d-%H%M%S)"
if [ "$DRY_RUN" = "1" ]; then
dry_run_msg "would backup recipes/wip/services/grub to $backup_name and replace with symlink"
else
echo " backing up upstream recipes/wip/services/grub -> $backup_name"
mv "recipes/wip/services/grub" "$backup_name"
fi
fi fi
if [ ! -e "recipes/wip/services/grub" ]; then if [ ! -e "recipes/wip/services/grub" ]; then
symlink "../../../local/recipes/core/grub" "recipes/wip/services/grub" symlink "../../../local/recipes/core/grub" "recipes/wip/services/grub"
@@ -200,24 +257,31 @@ echo "==> Ensuring Red Bear OS-specific files exist..."
# redbear.ipxe (network boot) # redbear.ipxe (network boot)
if [ ! -f redbear.ipxe ] && [ ! -L redbear.ipxe ]; then if [ ! -f redbear.ipxe ] && [ ! -L redbear.ipxe ]; then
cat > redbear.ipxe <<'IPXE' if [ "$DRY_RUN" = "1" ]; then
dry_run_msg "would create redbear.ipxe"
else
cat > redbear.ipxe <<'IPXE'
#!ipxe #!ipxe
kernel bootloader-live.efi kernel bootloader-live.efi
initrd http://${next-server}:8080/redbear-live.iso initrd http://${next-server}:8080/redbear-live.iso
boot boot
IPXE IPXE
echo " created redbear.ipxe" echo " created redbear.ipxe"
fi
fi fi
# redbear-full config (not in upstream) # redbear-full config (not in upstream)
if [ ! -f config/redbear-full.toml ] && [ ! -L config/redbear-full.toml ]; then if [ ! -f config/redbear-full.toml ] && [ ! -L config/redbear-full.toml ]; then
cat > config/redbear-full.toml <<'TOML' if [ "$DRY_RUN" = "1" ]; then
dry_run_msg "would create config/redbear-full.toml"
else
cat > config/redbear-full.toml <<'TOML'
# Red Bear OS Full Configuration # Red Bear OS Full Configuration
# Complete desktop + all Red Bear OS custom drivers and tools # Complete desktop + all Red Bear OS custom drivers and tools
# #
# Build: make all CONFIG_NAME=redbear-full # Build: make all CONFIG_NAME=redbear-full
# Live: make live CONFIG_NAME=redbear-full # Live: make live CONFIG_NAME=redbear-live-full
include = ["desktop.toml"] include = ["desktop.toml"]
@@ -249,9 +313,15 @@ amdgpu = {}
# Red Bear OS meta-package (dependencies, default config) # Red Bear OS meta-package (dependencies, default config)
redbear-meta = {} redbear-meta = {}
TOML TOML
echo " created config/redbear-full.toml" echo " created config/redbear-full.toml"
fi
else
echo " config/redbear-full.toml already exists — not overwriting"
fi fi
echo "" echo ""
echo "==> All Red Bear OS patches applied. Ready to build." echo "==> All Red Bear OS patches applied. Ready to build."
if [ "$DRY_RUN" = "1" ]; then
echo " [dry-run mode — no changes were made]"
fi
echo " make all CONFIG_NAME=redbear-full" echo " make all CONFIG_NAME=redbear-full"
+93 -4
View File
@@ -23,17 +23,20 @@ UPSTREAM_REMOTE="upstream-redox"
UPSTREAM_BRANCH="${UPSTREAM_BRANCH:-master}" UPSTREAM_BRANCH="${UPSTREAM_BRANCH:-master}"
DRY_RUN=0 DRY_RUN=0
NO_MERGE=0 NO_MERGE=0
FORCE=0
usage() { usage() {
echo "Usage: $0 [--dry-run] [--no-merge]" echo "Usage: $0 [--dry-run] [--no-merge] [--force]"
echo " --dry-run Show what would happen without making changes" echo " --dry-run Show what would happen without making changes"
echo " --no-merge Only fetch and check patch conflicts" echo " --no-merge Only fetch and check patch conflicts"
echo " --force Skip safety checks (uncommitted local/ changes)"
} }
for arg in "$@"; do for arg in "$@"; do
case "$arg" in case "$arg" in
--dry-run) DRY_RUN=1 ;; --dry-run) DRY_RUN=1 ;;
--no-merge) NO_MERGE=1 ;; --no-merge) NO_MERGE=1 ;;
--force) FORCE=1 ;;
--help|-h) --help|-h)
usage usage
exit 0 exit 0
@@ -112,16 +115,88 @@ if [ "$DRY_RUN" = "1" ]; then
exit 0 exit 0
fi fi
# ── 3.5. Check for uncommitted local/ changes ──────────────────────
if [ "$NO_MERGE" = "0" ] && [ "$DRY_RUN" = "0" ]; then
LOCAL_CHANGES=""
LOCAL_UNTRACKED=""
if [ -d "local" ]; then
LOCAL_CHANGES=$(cd local && git diff --name-only HEAD 2>/dev/null || true)
LOCAL_UNTRACKED=$(cd local && git ls-files --others --exclude-standard 2>/dev/null || true)
fi
# Also check for uncommitted changes to tracked local/ files from repo root
ROOT_LOCAL_CHANGES=$(git diff --name-only HEAD -- local/ 2>/dev/null || true)
if [ -n "$LOCAL_CHANGES" ] || [ -n "$LOCAL_UNTRACKED" ] || [ -n "$ROOT_LOCAL_CHANGES" ]; then
echo ""
echo "!! WARNING: Uncommitted changes detected in local/"
if [ -n "$ROOT_LOCAL_CHANGES" ]; then
echo " Modified files:"
echo "$ROOT_LOCAL_CHANGES" | head -10 | while read -r f; do echo " $f"; done
TOTAL=$(echo "$ROOT_LOCAL_CHANGES" | grep -c .)
[ "$TOTAL" -gt 10 ] && echo " ... and $((TOTAL - 10)) more"
fi
if [ -n "$LOCAL_UNTRACKED" ]; then
echo " Untracked files (NOT protected by stash):"
echo "$LOCAL_UNTRACKED" | head -5 | while read -r f; do echo " $f"; done
TOTAL=$(echo "$LOCAL_UNTRACKED" | grep -c .)
[ "$TOTAL" -gt 5 ] && echo " ... and $((TOTAL - 5)) more"
fi
echo ""
echo " git stash does NOT protect untracked files."
echo " Commit your local/ changes before syncing, or use --force to proceed anyway."
if [ "$FORCE" = "0" ]; then
echo ""
echo " ABORT: Uncommitted local/ changes detected. Use --force to override."
exit 1
else
echo " --force specified, proceeding anyway..."
fi
fi
fi
# ── 4. Stash uncommitted changes ──────────────────────────────────── # ── 4. Stash uncommitted changes ────────────────────────────────────
STASHED=0 STASHED=0
if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then
echo "==> Stashing uncommitted changes..." echo "==> Stashing uncommitted changes..."
git stash push -m "redbear-sync-$(date +%Y%m%d-%H%M%S)" git stash push -u -m "redbear-sync-$(date +%Y%m%d-%H%M%S)"
STASHED=1 STASHED=1
fi fi
PREV_HEAD=$(git rev-parse HEAD) PREV_HEAD=$(git rev-parse HEAD)
# ── 4.5. Verify overlay integrity before rebase ────────────────────
echo "==> Verifying Red Bear overlay integrity before rebase..."
BROKEN_SYMLINKS=""
while IFS= read -r link; do
if [ ! -e "$link" ]; then
BROKEN_SYMLINKS="$BROKEN_SYMLINKS
$link -> $(readlink "$link")"
fi
done < <(find recipes -maxdepth 3 -type l 2>/dev/null)
if [ -n "$BROKEN_SYMLINKS" ]; then
echo "!! WARNING: Broken symlinks detected in recipes/:"
echo "$BROKEN_SYMLINKS" | head -20
TOTAL=$(echo "$BROKEN_SYMLINKS" | grep -c .)
[ "$TOTAL" -gt 20 ] && echo " ... and $((TOTAL - 20)) more"
echo ""
echo " These symlinks may break further during rebase."
echo " Run ./local/scripts/apply-patches.sh after rebase to recreate them."
fi
# Check that key local/patches exist
for patch_file in local/patches/kernel/redox.patch local/patches/base/redox.patch local/patches/relibc/redox.patch; do
if [ ! -f "$patch_file" ]; then
echo "!! CRITICAL: Missing patch file: $patch_file"
echo " Cannot recover from rebase failure without this patch."
if [ "$FORCE" = "0" ]; then
exit 1
fi
fi
done
# ── 5. Rebase ─────────────────────────────────────────────────────── # ── 5. Rebase ───────────────────────────────────────────────────────
echo "" echo ""
echo "==> Rebasing Red Bear OS commits onto $UPSTREAM_REF..." echo "==> Rebasing Red Bear OS commits onto $UPSTREAM_REF..."
@@ -135,20 +210,34 @@ else
echo "!! Rebase conflict. Options:" echo "!! Rebase conflict. Options:"
echo " 1. Resolve conflicts: edit files, git add, git rebase --continue" echo " 1. Resolve conflicts: edit files, git add, git rebase --continue"
echo " 2. Abort: git rebase --abort" echo " 2. Abort: git rebase --abort"
echo " 3. Nuclear option:" echo " 3. Nuclear option (DESTRUCTIVE — loses uncommitted work):"
echo " git rebase --abort" echo " git rebase --abort"
echo " git reset --hard $UPSTREAM_REF" echo " git reset --hard $UPSTREAM_REF"
echo " ./local/scripts/apply-patches.sh --force" echo " ./local/scripts/apply-patches.sh --force"
echo "" echo ""
echo " Patches for recovery: local/patches/build-system/" echo " Patches for recovery: local/patches/build-system/"
echo " Previous HEAD: $PREV_HEAD" echo " Previous HEAD: $PREV_HEAD"
echo ""
echo " IMPORTANT: Before using the nuclear option, ensure all local/ changes"
echo " are committed. The nuclear option does NOT preserve uncommitted work."
echo " To recover to previous state: git reset --hard $PREV_HEAD"
exit 1 exit 1
fi fi
# ── 6. Restore stash ──────────────────────────────────────────────── # ── 6. Restore stash ────────────────────────────────────────────────
if [ "$STASHED" = 1 ]; then if [ "$STASHED" = 1 ]; then
echo "==> Restoring stashed changes..." echo "==> Restoring stashed changes..."
git stash pop || echo " (stash pop had conflicts — resolve manually)" if git stash pop; then
echo " Stash restored successfully."
else
echo "!! Stash pop had conflicts."
echo " Your changes are preserved in the stash."
echo " Options:"
echo " 1. Resolve conflicts in the working tree"
echo " 2. git checkout --theirs . && git stash drop"
echo " 3. git reset --hard && git stash pop (try again on clean tree)"
echo " List stashes: git stash list"
fi
fi fi
# ── 7. Verify symlinks ───────────────────────────────────────────── # ── 7. Verify symlinks ─────────────────────────────────────────────