#!/usr/bin/env bash # Red Bear OS — C-7 direct-sed migration # # For KF6 recipes whose [build].script uses inline `sed -i` # chains, this script: # 1. Restores the pristine source from `source-pristine/` # 2. Applies the same sed chain manually via `bash -c` # 3. Diffs pristine vs post-sed source # 4. Saves the diff as `local/patches//01-initial-migration.patch` # # Why direct-sed instead of `repo cook`: the cookbook's # `cook` step does a full dep-tree build that often fails # in offline mode (missing libffi/pcre2/mesa stage.pkgar # etc.). The sed chain we care about is the FIRST thing # in the recipe's [build].script and runs BEFORE cmake # configure. We can run it standalone with a simple # `bash -c ""` to capture the post-sed state. # # This is the same diff that `repo cook` would have # produced, but without the dep-tree failure mode. set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")/../.." PATCHES_DIR="${REDBEAR_MIGRATE_PATCHES_DIR:-/home/kellito/Builds/RedBear-OS/local/patches}" # All 17 KF6 recipes that previously had inline `sed -i` # chains (all those whose upstream 6.26.0 still has the # ecm_install_po_files_as_qm call). The 24 NO-OP recipes # were cleaned separately by cleanup-kf6-noop-seds.sh. recipe_dirs=( "local/recipes/kde/kf6-karchive" "local/recipes/kde/kf6-kauth" "local/recipes/kde/kf6-kbookmarks" "local/recipes/kde/kf6-kcodecs" "local/recipes/kde/kf6-kcompletion" "local/recipes/kde/kf6-kconfig" "local/recipes/kde/kf6-kcoreaddons" "local/recipes/kde/kf6-kdbusaddons" "local/recipes/kde/kf6-kglobalaccel" "local/recipes/kde/kf6-kitemviews" "local/recipes/kde/kf6-kjobwidgets" "local/recipes/kde/kf6-knotifications" "local/recipes/kde/kf6-kwidgetsaddons" "local/recipes/kde/kf6-kwindowsystem" "local/recipes/kde/kf6-solid" "local/recipes/kde/kf6-sonnet" "local/recipes/kde/kf6-syntaxhighlighting" "local/recipes/kde/sddm" # 7 unclassified (git-sourced or kde/plasma). Their # pristine state was snapshotted after a successful # `repo fetch`. If the pristine doesn't exist, the # script reports NO-OP and moves on. "local/recipes/kde/breeze" "local/recipes/kde/kde-cli-tools" "local/recipes/kde/kdecoration" "local/recipes/kde/kf6-kcmutils" "local/recipes/kde/kf6-kdeclarative" "local/recipes/kde/kf6-kded6" "local/recipes/kde/kf6-kwayland" "local/recipes/kde/kf6-notifyconfig" "local/recipes/kde/kglobalacceld" "local/recipes/kde/kirigami" "local/recipes/kde/konsole" "local/recipes/kde/kwin" "local/recipes/kde/plasma-desktop" "local/recipes/kde/plasma-workspace" ) migrated=0 skipped=0 failed=0 no_op=0 for recipe_dir in "${recipe_dirs[@]}"; do name=$(basename "$recipe_dir") recipe="$recipe_dir/recipe.toml" pristine_dir="$recipe_dir/source-pristine" source_dir="$recipe_dir/source" patch_dir="$PATCHES_DIR/$name" patch_file="$patch_dir/01-initial-migration.patch" if [ -e "$patch_file" ]; then echo "SKIP: $name (patch already exists)" skipped=$((skipped+1)) continue fi if [ ! -e "$pristine_dir" ] || [ ! -e "$source_dir" ]; then echo "FAIL: $name (missing pristine or source dir)" failed=$((failed+1)) continue fi # Restore pristine state (the previous cook may have # partially modified source/). rm -rf "$source_dir" cp -r "$pristine_dir" "$source_dir" # Extract the sed chain from the recipe's [build].script. # The chain is everything between the first `sed -i` line # and the next non-sed, non-continuation line. Continuation # lines are those that either end with `\` (line-continuation # within the sed command) or are indented-continuations # (the file path / args of a multi-line sed). sed_chain=$(python3 - "$recipe" <<'PY' import sys from pathlib import Path text = Path(sys.argv[1]).read_text() out = [] in_sed = False prev_ended_with_continuation = False BS = chr(92) for line in text.splitlines(): if "sed -i" in line and not in_sed: in_sed = True out.append(line) prev_ended_with_continuation = line.rstrip().endswith(BS) elif in_sed: ends_with_bs = line.rstrip().endswith(BS) is_indented = line.startswith(" ") or line.startswith("\t") if "sed -i" in line: out.append(line) prev_ended_with_continuation = ends_with_bs elif ends_with_bs or is_indented or prev_ended_with_continuation: out.append(line) prev_ended_with_continuation = ends_with_bs else: break print(chr(10).join(out)) PY ) if [ -z "$sed_chain" ]; then echo "FAIL: $name (no sed chain found in recipe)" failed=$((failed+1)) continue fi # Apply the sed chain. Bash's `$(...)` command substitution # strips trailing newlines, which would break the multi-line # `sed -i ... \` continuation. Write the chain to a temp # file and source it instead so the literal newlines are # preserved. sed_script=$(mktemp /tmp/sed-chain.XXXXXX.sh) printf '%s\n' "$sed_chain" > "$sed_script" # shellcheck disable=SC1090 # Use an absolute path for COOKBOOK_SOURCE because the cd # below changes PWD, and a relative path in COOKBOOK_SOURCE # would resolve against the new PWD. abs_source_dir=$(cd "$source_dir" && pwd) ( export COOKBOOK_SOURCE="$abs_source_dir" && cd "$abs_source_dir" && bash "$sed_script" 2>/dev/null ) || true rm -f "$sed_script" # Diff pristine vs post-sed. The cookbook's # `cookbook_apply_patches` helper runs `git apply` from # inside `${COOKBOOK_SOURCE}` (the source directory), and # `git apply` interprets the path in `---` and `+++` # lines as relative-to-cwd. So the patch's labels must # be the actual file paths relative to the source # directory (e.g. `CMakeLists.txt` or `src/CMakeLists.txt`). # # Build a per-file diff so each file gets the right # label. Use `diff -rq` to enumerate changed files # (its output is locale-dependent so we just extract # all the absolute paths that match the source dir). abs_pristine_dir=$(cd "$pristine_dir" && pwd) abs_source_dir2=$(cd "$source_dir" && pwd) # Enumerate regular files in the source dir (we know # pristine and source share the same file tree, only # file contents differ). diff_out="" while IFS= read -r -d '' rel_file; do # Skip ECM-generated noise. case "$rel_file" in .clang-format|.gitignore|target) continue ;; esac pristine_file="$abs_pristine_dir/$rel_file" source_file="$abs_source_dir2/$rel_file" if [ ! -f "$pristine_file" ] || [ ! -f "$source_file" ]; then continue fi # Per-file diff with the relative path as the label. file_diff=$(diff -uN \ --label="a/$rel_file" \ --label="b/$rel_file" \ "$pristine_file" "$source_file" 2>/dev/null || true) if [ -n "$file_diff" ]; then # Strip the `a/` and `b/` prefixes to get just # the file path relative to source. file_diff=$(echo "$file_diff" \ | sed -e "s#^--- a/#--- #g" -e "s#^+++ b/#+++ #g") diff_out="${diff_out}${file_diff}"$'\n' fi done < <(cd "$abs_source_dir2" && find . -type f -not -path './.git/*' -not -path './target/*' -not -name '.clang-format' -not -name '.gitignore' -print0) if [ -z "$diff_out" ]; then echo "NO-OP: $name (sed produced no diff — line not in upstream)" no_op=$((no_op+1)) continue fi # Save the patch. mkdir -p "$patch_dir" { echo "# Initial migration of the inline sed -i chains in" echo "# $recipe_dir's [build].script to a durable external" echo "# patch. Captured by local/scripts/migrate-kf6-seds-direct.sh" echo "# on $(date -Iseconds)." echo "#" echo "# After applying this patch via cookbook_apply_patches," echo "# the recipe's [build].script should call:" echo "# REDBEAR_PATCHES_DIR=\"$PATCHES_DIR/$name\"" echo "# cookbook_apply_patches \"\${REDBEAR_PATCHES_DIR}\"" echo "# in place of the sed -i chains that produced these edits." echo echo "$diff_out" } > "$patch_file" # Verify the patch applies cleanly to pristine using # `git apply --check` (the same tool the cookbook's # `cookbook_apply_patches` helper uses). `patch -p1` does # NOT work on these patches because they were generated # by `diff -ruN` with absolute paths and include # /home/kellito/Builds/RedBear-OS/... prefixes that # `patch -p1` cannot strip. if (cd "$pristine_dir" && git apply --check "$patch_file" >/dev/null 2>&1); then echo "MIGRATED: $name" migrated=$((migrated+1)) else echo "FAIL: $name (patch does not apply cleanly)" rm -f "$patch_file" failed=$((failed+1)) fi done echo echo "=== Summary ===" echo "Migrated: $migrated" echo "No-op: $no_op" echo "Skipped: $skipped" echo "Failed: $failed"