diff --git a/scripts/patches/apply-override.sh b/scripts/patches/apply-override.sh index eb43f742..82db0d7a 100755 --- a/scripts/patches/apply-override.sh +++ b/scripts/patches/apply-override.sh @@ -163,11 +163,15 @@ if [[ ! -d "$PATCHED_SRC" ]]; then exit 1 fi -# Find the first series file for quilt env -SERIES_FILE=$(find "$PATCHES_DIR" -maxdepth 1 -name "*.series" | head -1) -if [[ -z "$SERIES_FILE" ]]; then - echo "Error: No series files found in patches/" >&2 - exit 1 +# Find the series file for quilt env (honor a pre-set QUILT_SERIES) +if [[ -n "${QUILT_SERIES:-}" && -f "${QUILT_SERIES}" ]]; then + SERIES_FILE="$QUILT_SERIES" +else + SERIES_FILE=$(find "$PATCHES_DIR" -maxdepth 1 -name "*.series" | head -1) + if [[ -z "$SERIES_FILE" ]]; then + echo "Error: No series files found in patches/" >&2 + exit 1 + fi fi export QUILT_PATCHES="$PATCHES_DIR" diff --git a/scripts/patches/regenerate-override-patches.sh b/scripts/patches/regenerate-override-patches.sh new file mode 100755 index 00000000..dd90c6dd --- /dev/null +++ b/scripts/patches/regenerate-override-patches.sh @@ -0,0 +1,261 @@ +#!/usr/bin/env bash +# +# regenerate-override-patches.sh — Regenerate @generated override patches and +# heal the rest of the series. +# +# Usage: +# scripts/patches/regenerate-override-patches.sh \ +# [--dry-run] [--target ] [--set 'PACKAGE=VERSION']... ... +# +# — an existing @generated patch, e.g. common/finding-override-undici.diff +# --set — rewrite the version for PACKAGE in the named patches' +# @generator specs before regenerating (repeatable) +# --target — target whose series to rebase (default: code-editor-sagemaker-server) +# --dry-run — print the commands that would run without executing them +# +# Each named patch is regenerated by re-executing the apply-override.sh command +# stored in its @generator metadata line. Because regenerating an early patch +# changes the context of later @generated patches, the script then rebases the +# series and automatically regenerates any @generated patch that conflicts, +# repeating until the series applies cleanly. +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" +PATCHED_SRC="${ROOT_DIR}/code-editor-src" +PATCHES_DIR="${ROOT_DIR}/patches" +MAX_ITERATIONS=25 + +# --------------------------------------------------------------------------- +# Parse CLI args +# --------------------------------------------------------------------------- +DRY_RUN=false +TARGET="code-editor-sagemaker-server" +SETS=() +PATCH_NAMES=() + +while [[ $# -gt 0 ]]; do + case "$1" in + --dry-run) DRY_RUN=true; shift ;; + --target) TARGET="$2"; shift 2 ;; + --set) SETS+=("$2"); shift 2 ;; + -*) echo "Unknown option: $1" >&2; exit 1 ;; + *) PATCH_NAMES+=("$1"); shift ;; + esac +done + +if [[ ${#PATCH_NAMES[@]} -eq 0 ]]; then + echo "Error: at least one patch name is required" >&2 + exit 1 +fi + +CONFIG_FILE="${ROOT_DIR}/configuration/${TARGET}.json" +if [[ ! -f "$CONFIG_FILE" ]]; then + echo "Error: Configuration file not found: $CONFIG_FILE" >&2 + exit 1 +fi + +for s in "${SETS[@]:-}"; do + if [[ -n "$s" && "$s" != *"="* ]]; then + echo "Error: --set expects PACKAGE=VERSION, got: $s" >&2 + exit 1 + fi +done + +export QUILT_PATCHES="$PATCHES_DIR" +export QUILT_SERIES="${ROOT_DIR}/$(jq -r '.patches.path' "$CONFIG_FILE")" + +# --------------------------------------------------------------------------- +# Read metadata from an @generated patch file +# --------------------------------------------------------------------------- +require_generated() { + local patch_file="$1" + if [[ ! -f "$patch_file" ]]; then + echo "Error: patch not found: $patch_file" >&2 + exit 1 + fi + if ! grep -q '^@generated$' "$patch_file"; then + echo "Error: not an @generated patch: $patch_file" >&2 + exit 1 + fi + if ! grep -q '^@generator: scripts/patches/apply-override.sh ' "$patch_file"; then + echo "Error: no usable @generator line in: $patch_file" >&2 + exit 1 + fi +} + +# Sets PATCH_HEADER to the free-text header (everything before @generated, +# without trailing blank lines). +read_header() { + local patch_file="$1" + PATCH_HEADER=$(sed -n '1,/^@generated$/p' "$patch_file" | sed '$d' \ + | sed -e :a -e '/^[[:space:]]*$/{$d;N;ba' -e '}') +} + +# Sets GEN_PATCH and GEN_OVERRIDES from the @generator line. +read_generator() { + local patch_file="$1" + local gen_args + gen_args=$(grep -m1 '^@generator: scripts/patches/apply-override.sh ' "$patch_file" \ + | sed 's|^@generator: scripts/patches/apply-override.sh ||') + + GEN_PATCH="" + GEN_OVERRIDES=() + eval "set -- $gen_args" + while [[ $# -gt 0 ]]; do + case "$1" in + --patch) GEN_PATCH="$2"; shift 2 ;; + --override) GEN_OVERRIDES+=("$2"); shift 2 ;; + *) echo "Error: unexpected @generator argument '$1' in $patch_file" >&2; exit 1 ;; + esac + done + + if [[ -z "$GEN_PATCH" || ${#GEN_OVERRIDES[@]} -eq 0 ]]; then + echo "Error: incomplete @generator line in $patch_file" >&2 + exit 1 + fi +} + +# --------------------------------------------------------------------------- +# Apply --set rewrites to GEN_OVERRIDES and PATCH_HEADER +# --------------------------------------------------------------------------- +apply_sets() { + local s pkg new_ver old_ver old_bare new_bare i spec spec_pkg + for s in "${SETS[@]:-}"; do + [[ -z "$s" ]] && continue + pkg="${s%%=*}" + new_ver="${s#*=}" + for i in "${!GEN_OVERRIDES[@]}"; do + spec="${GEN_OVERRIDES[$i]}" + spec_pkg="${spec##*:}" + spec_pkg="${spec_pkg%%=*}" + if [[ "$spec_pkg" == "$pkg" ]]; then + old_ver="${spec##*=}" + GEN_OVERRIDES[$i]="${spec%=*}=${new_ver}" + # Update version references in the free-text header too + old_bare="${old_ver#"${old_ver%%[0-9]*}"}" + new_bare="${new_ver#"${new_ver%%[0-9]*}"}" + if [[ -n "$old_bare" && -n "$new_bare" ]]; then + PATCH_HEADER="${PATCH_HEADER//$old_bare/$new_bare}" + fi + PATCH_HEADER="${PATCH_HEADER//$old_ver/$new_ver}" + fi + done + done +} + +# --------------------------------------------------------------------------- +# Command runners +# --------------------------------------------------------------------------- +run_cmd() { + if [[ "$DRY_RUN" == true ]]; then + printf '[dry-run] would run:' + printf ' %q' "$@" + printf '\n' + else + "$@" + fi +} + +regenerate_patch() { + local patch_name="$1"; shift + local header="$1"; shift + local cmd=("$SCRIPT_DIR/apply-override.sh" --patch "$patch_name" --header "$header") + local o + for o in "$@"; do + cmd+=(--override "$o") + done + run_cmd "${cmd[@]}" +} + +# --------------------------------------------------------------------------- +# Rebase the series, auto-regenerating conflicted @generated patches +# --------------------------------------------------------------------------- +ITERATIONS=0 + +rebase_and_heal() { + if [[ "$DRY_RUN" == true ]]; then + run_cmd ./scripts/prepare-src.sh --command rebase_patches "$TARGET" + echo "[dry-run] on conflict, would regenerate the conflicted @generated patch from its @generator line and rebase again (max $MAX_ITERATIONS iterations)" + return 0 + fi + + while true; do + ITERATIONS=$((ITERATIONS + 1)) + if [[ $ITERATIONS -gt $MAX_ITERATIONS ]]; then + echo "Error: exceeded $MAX_ITERATIONS rebase iterations, giving up" >&2 + exit 1 + fi + + echo "" + echo "=== Rebase iteration $ITERATIONS ===" + local output exit_code + set +e + output=$(cd "$ROOT_DIR" && ./scripts/prepare-src.sh --command rebase_patches "$TARGET" 2>&1) + exit_code=$? + set -e + echo "$output" + + if [[ $exit_code -eq 0 ]]; then + return 0 + fi + + if ! grep -q "Required actions:" <<< "$output"; then + echo "Error: rebase failed for a reason other than a patch conflict (see output above)" >&2 + exit 1 + fi + + local top rel + top=$(cd "$PATCHED_SRC" && quilt top) + rel="${top#"$PATCHES_DIR"/}" + local patch_file="${PATCHES_DIR}/${rel}" + + if ! grep -q '^@generated$' "$patch_file"; then + echo "" + echo "Conflicted patch $rel is not @generated; resolve it manually:" + echo "1. Edit the files to resolve the conflicts" + echo "2. Run 'quilt refresh' to update the patch" + echo "3. Re-run this script to continue" + exit 1 + fi + + echo "" + echo "Regenerating conflicted @generated patch: $rel" + read_header "$patch_file" + read_generator "$patch_file" + + # Drop the forced application and empty the stale patch so + # apply-override.sh can push it cleanly and refresh it from scratch. + (cd "$PATCHED_SRC" && quilt pop -f) + : > "$patch_file" + regenerate_patch "$GEN_PATCH" "$PATCH_HEADER" "${GEN_OVERRIDES[@]}" + done +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- +for name in "${PATCH_NAMES[@]}"; do + require_generated "${PATCHES_DIR}/${name}" +done + +echo "Target: $TARGET" +echo "Patches: ${PATCH_NAMES[*]}" + +# Ensure the series is fully applied before regenerating anything +rebase_and_heal + +for name in "${PATCH_NAMES[@]}"; do + echo "" + echo "=== Regenerating $name ===" + patch_file="${PATCHES_DIR}/${name}" + read_header "$patch_file" + read_generator "$patch_file" + apply_sets + regenerate_patch "$GEN_PATCH" "$PATCH_HEADER" "${GEN_OVERRIDES[@]}" + rebase_and_heal +done + +echo "" +echo "All patches regenerated and series applies cleanly."