@@ -70,6 +70,11 @@ SELF="$(realpath "$0")"
7070BUILD_SCRIPT=" /usr/local/sbin/uki-build.sh"
7171INSTALL_PLUGIN=" /usr/lib/kernel/install.d/90-uki-ukify.install"
7272BACKUP_ROOT=" /var/backups/uki-setup"
73+ RUN_TIMESTAMP=" $( date -u +%Y%m%dT%H%M%SZ) "
74+ RUN_BACKUP_DIR=" ${BACKUP_ROOT} /${RUN_TIMESTAMP} "
75+ RUN_MANIFEST_DIR=" ${BACKUP_ROOT} /manifests"
76+ RUN_MANIFEST_PATH=" ${RUN_MANIFEST_DIR} /${RUN_TIMESTAMP} .manifest"
77+ ROLLBACK_SCRIPT=" /usr/local/sbin/uki-rollback.sh"
7378PKG_MGR=" "
7479PKG_INSTALL_CMD=()
7580DRY_RUN=0
@@ -83,6 +88,46 @@ die() { echo "${RED}${BLD}[err]${RST} $*" >&2; exit 1; }
8388hr () { echo " ──────────────────────────────────────────────────────────────" ; }
8489require_cmd () { command -v " $1 " & > /dev/null || die " Required command missing: $1 " ; }
8590
91+ refresh_run_paths () {
92+ RUN_BACKUP_DIR=" ${BACKUP_ROOT} /${RUN_TIMESTAMP} "
93+ RUN_MANIFEST_DIR=" ${BACKUP_ROOT} /manifests"
94+ RUN_MANIFEST_PATH=" ${RUN_MANIFEST_DIR} /${RUN_TIMESTAMP} .manifest"
95+ }
96+
97+ init_run_manifest () {
98+ refresh_run_paths
99+ mkdir -p " $RUN_MANIFEST_DIR " " $RUN_BACKUP_DIR "
100+ cat > " $RUN_MANIFEST_PATH " << EOF
101+ # uki-setup manifest v1
102+ timestamp=${RUN_TIMESTAMP}
103+ script=uki-setup.sh
104+ backup_dir=${RUN_BACKUP_DIR}
105+ EOF
106+ }
107+
108+ ensure_manifest_ready () {
109+ refresh_run_paths
110+ if [[ ! -f " $RUN_MANIFEST_PATH " ]]; then
111+ init_run_manifest
112+ fi
113+ }
114+
115+ append_manifest_entry () {
116+ local op=" $1 " path=" $2 " backup_ref=" ${3:- } " extra=" ${4:- } "
117+ ensure_manifest_ready
118+ printf ' %s|%s|%s|%s\n' " $op " " $path " " $backup_ref " " $extra " >> " $RUN_MANIFEST_PATH "
119+ }
120+
121+ record_created_path () {
122+ local path=" $1 "
123+ append_manifest_entry " created" " $path " " "
124+ }
125+
126+ record_deleted_path () {
127+ local path=" $1 " backup_ref=" ${2:- } "
128+ append_manifest_entry " deleted" " $path " " $backup_ref "
129+ }
130+
86131list_installed_kernels () {
87132 local pkg kernel_ver
88133
@@ -442,12 +487,13 @@ ensure_esp_mounted() {
442487backup_path () {
443488 local src=" $1 "
444489 [[ -e " $src " || -L " $src " ]] || return 0
445- local ts backup_dir backup
446- ts= " $( date +%Y%m%d-%H%M%S ) "
447- backup_dir=" ${BACKUP_ROOT} / ${ts }"
490+ local backup_dir backup
491+ refresh_run_paths
492+ backup_dir=" ${RUN_BACKUP_DIR } "
448493 backup=" ${backup_dir}${src} "
449494 mkdir -p " $( dirname " $backup " ) "
450495 cp -a " $src " " $backup "
496+ append_manifest_entry " modified" " $src " " $backup "
451497 info " Backed up ${src} -> ${backup} "
452498}
453499
@@ -607,6 +653,10 @@ BOOT_SUCCESS_DIR="/var/lib/uki-ukify/boot-success"
607653BACKUP_ROOT="/var/backups/uki-setup"
608654EFI_AUDIT_LOG="/var/log/uki-efi-audit.log"
609655DRY_RUN=0
656+ RUN_TIMESTAMP="$(date -u +%Y%m%dT%H%M%SZ)"
657+ RUN_BACKUP_DIR="${BACKUP_ROOT}/${RUN_TIMESTAMP}"
658+ RUN_MANIFEST_DIR="${BACKUP_ROOT}/manifests"
659+ RUN_MANIFEST_PATH="${RUN_MANIFEST_DIR}/${RUN_TIMESTAMP}.manifest"
610660# ─────────────────────────────────────────────────────────────────────────────
611661
612662RED='\e[31;1m'; GRN='\e[32;1m'; YLW='\e[33;1m'; RST='\e[0m'
@@ -615,6 +665,46 @@ warn() { echo -e "${YLW}[uki-build]${RST} $*" >&2; }
615665die() { echo -e "${RED}[uki-build]${RST} $*" >&2; exit 1; }
616666require_cmd() { command -v "$1" &>/dev/null || die "Required command missing: $1"; }
617667
668+ refresh_run_paths() {
669+ RUN_BACKUP_DIR="${BACKUP_ROOT}/${RUN_TIMESTAMP}"
670+ RUN_MANIFEST_DIR="${BACKUP_ROOT}/manifests"
671+ RUN_MANIFEST_PATH="${RUN_MANIFEST_DIR}/${RUN_TIMESTAMP}.manifest"
672+ }
673+
674+ init_run_manifest() {
675+ refresh_run_paths
676+ mkdir -p "$RUN_MANIFEST_DIR" "$RUN_BACKUP_DIR"
677+ cat > "$RUN_MANIFEST_PATH" <<EOF
678+ # uki-setup manifest v1
679+ timestamp=${RUN_TIMESTAMP}
680+ script=uki-build.sh
681+ backup_dir=${RUN_BACKUP_DIR}
682+ EOF
683+ }
684+
685+ ensure_manifest_ready() {
686+ refresh_run_paths
687+ if [[ ! -f "$RUN_MANIFEST_PATH" ]]; then
688+ init_run_manifest
689+ fi
690+ }
691+
692+ append_manifest_entry() {
693+ local op="$1" path="$2" backup_ref="${3:-}" extra="${4:-}"
694+ ensure_manifest_ready
695+ printf '%s|%s|%s|%s\n' "$op" "$path" "$backup_ref" "$extra" >> "$RUN_MANIFEST_PATH"
696+ }
697+
698+ backup_path_for_manifest() {
699+ refresh_run_paths
700+ local src="$1"
701+ [[ -e "$src" || -L "$src" ]] || return 1
702+ local backup="${RUN_BACKUP_DIR}${src}"
703+ mkdir -p "$(dirname "$backup")"
704+ cp -a "$src" "$backup"
705+ echo "$backup"
706+ }
707+
618708list_installed_kernels() {
619709 local pkg kernel_ver
620710
@@ -662,6 +752,7 @@ backup_efibootmgr_snapshot() {
662752 fi
663753
664754 efibootmgr -v > "$snapshot_file"
755+ append_manifest_entry "efi_snapshot" "efibootmgr" "$snapshot_file" "$reason"
665756 info "Saved efibootmgr snapshot: ${snapshot_file}"
666757 audit_efi_event "Saved efibootmgr snapshot ${snapshot_file}"
667758}
@@ -714,6 +805,10 @@ delete_uki_and_entry() {
714805
715806 uki="${EFI_DIR}/linux-${kernel_ver}.efi"
716807 if [[ -f "$uki" ]]; then
808+ local deleted_backup=""
809+ if deleted_backup=$(backup_path_for_manifest "$uki"); then
810+ append_manifest_entry "deleted" "$uki" "$deleted_backup"
811+ fi
717812 log_or_run "Remove stale UKI ${uki}" rm -f "$uki"
718813 audit_efi_event "Removed stale UKI ${uki}"
719814 fi
@@ -1221,9 +1316,15 @@ ESP_MOUNT_CHECK=$(find_mounted_esp_target || true)
12211316[[ -n "$ESP_MOUNT_CHECK" ]] || die "ESP mount verification failed after mount attempts. Checked candidates: ${ESP_MOUNT_CANDIDATES[*]}. Fix: mount your ESP manually (typically /boot/efi) and re-run."
12221317validate_esp_free_space "$ESP_MOUNT_CHECK"
12231318
1319+ init_run_manifest
12241320EFFECTIVE_CMDLINE=$(get_effective_cmdline)
12251321
12261322cleanup_items=("$INITRD_OUT")
1323+ if [[ -e "$UKI_OUT" || -L "$UKI_OUT" ]]; then
1324+ if prev_backup=$(backup_path_for_manifest "$UKI_OUT"); then
1325+ append_manifest_entry "modified" "$UKI_OUT" "$prev_backup"
1326+ fi
1327+ fi
12271328
12281329info "Stage 1/2: Building standalone initramfs via dracut: ${INITRD_OUT}"
12291330if [[ "$DRY_RUN" -eq 1 ]]; then
@@ -1275,6 +1376,9 @@ if [[ "$DRY_RUN" -eq 1 ]]; then
12751376 info "[dry-run] Would run: ukify ${UKIFY_ARGS[*]}"
12761377else
12771378 ukify "${UKIFY_ARGS[@]}"
1379+ if [[ -f "$UKI_OUT" ]]; then
1380+ append_manifest_entry "created" "$UKI_OUT" "" "kernel=${KERNEL_VER}"
1381+ fi
12781382fi
12791383
12801384extract_uki_section_text() {
@@ -1420,9 +1524,175 @@ BUILDBODY
14201524 " $BUILD_SCRIPT "
14211525
14221526 chmod 0755 " $BUILD_SCRIPT "
1527+ record_created_path " $BUILD_SCRIPT "
14231528 info " Build script written."
14241529}
14251530
1531+
1532+ phase_write_rollback_script () {
1533+ hr
1534+ info " Phase 4: Writing rollback script → ${ROLLBACK_SCRIPT} "
1535+
1536+ mkdir -p " $( dirname " $ROLLBACK_SCRIPT " ) "
1537+ backup_path " $ROLLBACK_SCRIPT "
1538+
1539+ cat > " $ROLLBACK_SCRIPT " << 'ROLLBACKBODY '
1540+ #!/bin/bash
1541+ set -euo pipefail
1542+
1543+ BACKUP_ROOT="/var/backups/uki-setup"
1544+ MANIFEST_DIR="${BACKUP_ROOT}/manifests"
1545+ DRY_RUN=0
1546+ TARGET_TS=""
1547+
1548+ RED='\e[31;1m'; GRN='\e[32;1m'; YLW='\e[33;1m'; RST='\e[0m'
1549+ info() { echo -e "${GRN}[uki-rollback]${RST} $*"; }
1550+ warn() { echo -e "${YLW}[uki-rollback]${RST} $*" >&2; }
1551+ die() { echo -e "${RED}[uki-rollback]${RST} $*" >&2; exit 1; }
1552+
1553+ usage() {
1554+ cat <<'EOF'
1555+ Usage: uki-rollback.sh [--list] [--timestamp <UTC_TIMESTAMP>] [--dry-run]
1556+
1557+ Options:
1558+ --list List available backup timestamps.
1559+ --timestamp <timestamp> Explicit backup timestamp to restore (required unless --list).
1560+ --dry-run Preview actions without modifying system state.
1561+ -h, --help Show this help output.
1562+ EOF
1563+ }
1564+
1565+ require_cmd() { command -v "$1" >/dev/null 2>&1 || die "Required command missing: $1"; }
1566+
1567+ run_or_echo() {
1568+ if [[ "$DRY_RUN" -eq 1 ]]; then
1569+ info "[dry-run] $*"
1570+ else
1571+ "$@"
1572+ fi
1573+ }
1574+
1575+ list_timestamps() {
1576+ [[ -d "$MANIFEST_DIR" ]] || { warn "No manifest directory found: ${MANIFEST_DIR}"; return 0; }
1577+ find "$MANIFEST_DIR" -maxdepth 1 -type f -name '*.manifest' -printf '%f\n' \
1578+ | sed 's/\.manifest$//' | sort
1579+ }
1580+
1581+ validate_manifest() {
1582+ local manifest="$1"
1583+ [[ -s "$manifest" ]] || die "Manifest is missing or empty: $manifest"
1584+ head -1 "$manifest" | grep -q '^# uki-setup manifest v1$' || die "Malformed manifest header in: $manifest"
1585+ }
1586+
1587+ restore_efi_from_snapshot() {
1588+ local snapshot="$1" line bootnum disk part label loader
1589+ [[ -f "$snapshot" ]] || { warn "EFI snapshot not found: $snapshot"; return 0; }
1590+ require_cmd efibootmgr
1591+
1592+ while IFS= read -r line; do
1593+ [[ "$line" =~ ^Boot([0-9A-Fa-f]{4})\*?[[:space:]]+([^[:space:]].*?)[[:space:]]+HD\(([0-9]+),GPT, ]] || continue
1594+ bootnum="${BASH_REMATCH[1]}"
1595+ label="${BASH_REMATCH[2]}"
1596+ part="${BASH_REMATCH[3]}"
1597+ loader=$(echo "$line" | sed -n 's/.*File(\\\(.*\))/\1/p')
1598+ [[ -n "$loader" ]] || continue
1599+
1600+ if efibootmgr | grep -q "^Boot${bootnum}"; then
1601+ info "EFI entry Boot${bootnum} already exists; skipping recreate."
1602+ continue
1603+ fi
1604+
1605+ disk=$(findmnt -n -o SOURCE /boot/efi 2>/dev/null || true)
1606+ [[ -b "$disk" ]] || { warn "Cannot determine ESP disk for EFI restore; skipping Boot${bootnum}."; continue; }
1607+ disk="/dev/$(lsblk -no PKNAME "$disk" 2>/dev/null | head -1)"
1608+ [[ -b "$disk" ]] || { warn "Cannot resolve parent disk for EFI restore; skipping Boot${bootnum}."; continue; }
1609+
1610+ if [[ "$DRY_RUN" -eq 1 ]]; then
1611+ info "[dry-run] efibootmgr --create --disk ${disk} --part ${part} --label ${label} --loader \\${loader}"
1612+ else
1613+ efibootmgr --quiet --create --disk "$disk" --part "$part" --label "$label" --loader "\\${loader}" || warn "Failed to recreate EFI entry Boot${bootnum}"
1614+ fi
1615+ done < "$snapshot"
1616+ }
1617+
1618+ restore_from_manifest() {
1619+ local manifest="$1" line op path backup extra
1620+ validate_manifest "$manifest"
1621+
1622+ while IFS= read -r line; do
1623+ [[ -z "$line" || "$line" =~ ^# || "$line" =~ ^timestamp= || "$line" =~ ^script= || "$line" =~ ^backup_dir= ]] && continue
1624+ IFS='|' read -r op path backup extra <<< "$line"
1625+
1626+ case "$op" in
1627+ modified)
1628+ [[ -n "$backup" && -e "$backup" ]] || { warn "Missing backup for modified path: $path"; continue; }
1629+ run_or_echo mkdir -p "$(dirname "$path")"
1630+ run_or_echo cp -a "$backup" "$path"
1631+ ;;
1632+ created)
1633+ if [[ -e "$path" || -L "$path" ]]; then
1634+ run_or_echo rm -rf "$path"
1635+ fi
1636+ ;;
1637+ deleted)
1638+ if [[ -n "$backup" && ( -e "$backup" || -L "$backup" ) ]]; then
1639+ run_or_echo mkdir -p "$(dirname "$path")"
1640+ run_or_echo cp -a "$backup" "$path"
1641+ else
1642+ warn "Cannot recreate deleted path without backup: $path"
1643+ fi
1644+ ;;
1645+ efi_snapshot)
1646+ restore_efi_from_snapshot "$backup"
1647+ ;;
1648+ *)
1649+ warn "Skipping malformed/unknown manifest entry: $line"
1650+ ;;
1651+ esac
1652+ done < "$manifest"
1653+ }
1654+
1655+ while [[ $# -gt 0 ]]; do
1656+ case "$1" in
1657+ --list)
1658+ list_timestamps
1659+ exit 0
1660+ ;;
1661+ --timestamp)
1662+ shift
1663+ [[ $# -gt 0 ]] || die "--timestamp requires a value"
1664+ TARGET_TS="$1"
1665+ ;;
1666+ --dry-run)
1667+ DRY_RUN=1
1668+ ;;
1669+ -h|--help)
1670+ usage
1671+ exit 0
1672+ ;;
1673+ *)
1674+ die "Unknown option: $1"
1675+ ;;
1676+ esac
1677+ shift
1678+ done
1679+
1680+ [[ $EUID -eq 0 ]] || die "Must run as root."
1681+ [[ -n "$TARGET_TS" ]] || die "Missing required --timestamp. Use --list to view available timestamps."
1682+ MANIFEST_PATH="${MANIFEST_DIR}/${TARGET_TS}.manifest"
1683+ [[ -f "$MANIFEST_PATH" ]] || die "Backup manifest not found: ${MANIFEST_PATH}"
1684+
1685+ info "Restoring backup timestamp: ${TARGET_TS}"
1686+ [[ "$DRY_RUN" -eq 1 ]] && info "Dry-run mode enabled."
1687+ restore_from_manifest "$MANIFEST_PATH"
1688+ info "Rollback complete."
1689+ ROLLBACKBODY
1690+
1691+ chmod 0755 " $ROLLBACK_SCRIPT "
1692+ record_created_path " $ROLLBACK_SCRIPT "
1693+ info " Rollback script written."
1694+ }
1695+
14261696# =============================================================================
14271697# PHASE 4 — Write kernel-install(8) plugin
14281698# =============================================================================
@@ -1476,6 +1746,7 @@ exit 0
14761746PLUGINBODY
14771747
14781748 chmod 0755 " $INSTALL_PLUGIN "
1749+ record_created_path " $INSTALL_PLUGIN "
14791750 info " Plugin written."
14801751}
14811752
@@ -1507,6 +1778,7 @@ phase_disable_bls_plugins() {
15071778 backup_path " $target "
15081779 if [[ ! -e " $target " ]]; then
15091780 ln -s /dev/null " $target "
1781+ record_created_path " $target "
15101782 info " Disabled: ${p} "
15111783 else
15121784 info " Already overridden: ${p} — skipping."
@@ -1556,6 +1828,7 @@ phase_summary() {
15561828 echo " Files installed:"
15571829 echo " ${BUILD_SCRIPT} ← rebuild script (edit CMDLINE here)"
15581830 echo " ${INSTALL_PLUGIN} ← auto-trigger on kernel installs"
1831+ echo " ${ROLLBACK_SCRIPT} ← rollback helper"
15591832 echo " "
15601833 echo " UKIs are stored in: ${EFI_DIR} /"
15611834 find " ${EFI_DIR} " -maxdepth 1 -type f -name " *.efi" -exec ls -lh {} + 2> /dev/null | sed ' s/^/ /' || true
@@ -1618,9 +1891,11 @@ EOF
16181891if [[ " ${UKI_SETUP_SKIP_MAIN:- 0} " -ne 1 ]]; then
16191892 parse_cli_args " $@ "
16201893 [[ " $DRY_RUN " -eq 1 ]] && info " Dry-run mode enabled. Build script invocations will use --dry-run."
1894+ init_run_manifest
16211895 phase_preflight
16221896 phase_deps
16231897 phase_write_build_script
1898+ phase_write_rollback_script
16241899 phase_write_plugin
16251900 phase_disable_bls_plugins
16261901 phase_initial_build
0 commit comments