feat: Implement Rollbacks and Savepoints!
This commit is contained in:
@@ -81,17 +81,40 @@ pkg_install() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Installing packages via $distro package manager: ${pkgs[*]}"
|
||||
local to_install=()
|
||||
for pkg in "${pkgs[@]}"; do
|
||||
if ! pkg_check "$pkg"; then
|
||||
to_install+=("$pkg")
|
||||
fi
|
||||
|
||||
# Reference counting logic
|
||||
if [ -n "${BOOTSTRAP_CURRENT_TOOL:-}" ] && [ -n "${BOOTSTRAP_PACKAGES_DIR:-}" ]; then
|
||||
local ref_file="$BOOTSTRAP_PACKAGES_DIR/$pkg"
|
||||
if ! grep -q "^${BOOTSTRAP_CURRENT_TOOL}$" "$ref_file" 2>/dev/null; then
|
||||
echo "$BOOTSTRAP_CURRENT_TOOL" >> "$ref_file"
|
||||
# Register rollback command
|
||||
if type add_rollback_cmd >/dev/null 2>&1; then
|
||||
add_rollback_cmd "pkg_remove $pkg"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#to_install[@]} -eq 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Installing packages via $distro package manager: ${to_install[*]}"
|
||||
case "$distro" in
|
||||
arch)
|
||||
sudo pacman -Sy --needed --noconfirm "${pkgs[@]}"
|
||||
sudo pacman -Sy --needed --noconfirm "${to_install[@]}"
|
||||
;;
|
||||
debian)
|
||||
sudo apt update
|
||||
sudo apt install -y "${pkgs[@]}"
|
||||
sudo apt install -y "${to_install[@]}"
|
||||
;;
|
||||
fedora)
|
||||
sudo dnf install -y "${pkgs[@]}"
|
||||
sudo dnf install -y "${to_install[@]}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
@@ -144,24 +167,47 @@ pkg_remove() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Removing packages via $distro package manager: ${pkgs[*]}"
|
||||
local to_remove=()
|
||||
for pkg in "${pkgs[@]}"; do
|
||||
if [ -n "${BOOTSTRAP_CURRENT_TOOL:-}" ] && [ -n "${BOOTSTRAP_PACKAGES_DIR:-}" ]; then
|
||||
local ref_file="$BOOTSTRAP_PACKAGES_DIR/$pkg"
|
||||
if [ -f "$ref_file" ]; then
|
||||
# Remove this tool from the reference file
|
||||
sed -i "/^${BOOTSTRAP_CURRENT_TOOL}$/d" "$ref_file"
|
||||
if [ -s "$ref_file" ]; then
|
||||
log_info "Skipping removal of '$pkg'; it is required by other tools."
|
||||
continue
|
||||
else
|
||||
rm -f "$ref_file"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
to_remove+=("$pkg")
|
||||
done
|
||||
|
||||
if [ ${#to_remove[@]} -eq 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Removing packages via $distro package manager: ${to_remove[*]}"
|
||||
case "$distro" in
|
||||
arch)
|
||||
local to_remove=()
|
||||
for pkg in "${pkgs[@]}"; do
|
||||
local pac_remove=()
|
||||
for pkg in "${to_remove[@]}"; do
|
||||
if pacman -Qq "$pkg" >/dev/null 2>&1; then
|
||||
to_remove+=("$pkg")
|
||||
pac_remove+=("$pkg")
|
||||
fi
|
||||
done
|
||||
if [ ${#to_remove[@]} -gt 0 ]; then
|
||||
sudo pacman -R --noconfirm "${to_remove[@]}"
|
||||
if [ ${#pac_remove[@]} -gt 0 ]; then
|
||||
sudo pacman -R --noconfirm "${pac_remove[@]}"
|
||||
fi
|
||||
;;
|
||||
debian)
|
||||
sudo apt remove -y "${pkgs[@]}"
|
||||
sudo apt remove -y "${to_remove[@]}"
|
||||
;;
|
||||
fedora)
|
||||
sudo dnf remove -y "${pkgs[@]}"
|
||||
sudo dnf remove -y "${to_remove[@]}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
130
lib/rollback.sh
Normal file
130
lib/rollback.sh
Normal file
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [ -n "${_LIB_ROLLBACK_SOURCED:-}" ]; then
|
||||
return 0
|
||||
fi
|
||||
_LIB_ROLLBACK_SOURCED=1
|
||||
|
||||
BOOTSTRAP_STATE_DIR="$HOME/.local/state/bootstrap"
|
||||
BOOTSTRAP_HISTORY_LOG="$BOOTSTRAP_STATE_DIR/history.log"
|
||||
BOOTSTRAP_UNINSTALLERS_DIR="$BOOTSTRAP_STATE_DIR/uninstallers"
|
||||
BOOTSTRAP_PACKAGES_DIR="$BOOTSTRAP_STATE_DIR/packages"
|
||||
|
||||
init_rollback_system() {
|
||||
mkdir -p "$BOOTSTRAP_UNINSTALLERS_DIR"
|
||||
mkdir -p "$BOOTSTRAP_PACKAGES_DIR"
|
||||
touch "$BOOTSTRAP_HISTORY_LOG"
|
||||
}
|
||||
|
||||
setup_uninstaller_context() {
|
||||
local tool="$1"
|
||||
export BOOTSTRAP_CURRENT_TOOL="$tool"
|
||||
export BOOTSTRAP_UNINSTALLER_CMDS="$BOOTSTRAP_UNINSTALLERS_DIR/${tool}.cmds"
|
||||
# Ensure fresh manifest for this run
|
||||
rm -f "$BOOTSTRAP_UNINSTALLER_CMDS"
|
||||
touch "$BOOTSTRAP_UNINSTALLER_CMDS"
|
||||
}
|
||||
|
||||
add_rollback_cmd() {
|
||||
local cmd="$1"
|
||||
if [ -n "${BOOTSTRAP_UNINSTALLER_CMDS:-}" ] && [ -f "$BOOTSTRAP_UNINSTALLER_CMDS" ]; then
|
||||
# Prepend to the top of the file
|
||||
sed -i "1i $cmd" "$BOOTSTRAP_UNINSTALLER_CMDS"
|
||||
fi
|
||||
}
|
||||
|
||||
track_file() {
|
||||
add_rollback_cmd "sudo rm -f '$1'"
|
||||
}
|
||||
|
||||
track_dir() {
|
||||
add_rollback_cmd "sudo rm -rf '$1'"
|
||||
}
|
||||
|
||||
create_savepoint() {
|
||||
local name="$1"
|
||||
echo "SAVEPOINT: $name" >> "$BOOTSTRAP_HISTORY_LOG"
|
||||
log_success "Savepoint '$name' created."
|
||||
}
|
||||
|
||||
mark_install_success() {
|
||||
local tool="$1"
|
||||
# Only record if we actually have an uninstaller
|
||||
if [ -f "$BOOTSTRAP_UNINSTALLERS_DIR/${tool}.cmds" ]; then
|
||||
echo "INSTALL: $tool" >> "$BOOTSTRAP_HISTORY_LOG"
|
||||
fi
|
||||
}
|
||||
|
||||
execute_rollback() {
|
||||
local tool="$1"
|
||||
local manifest="$BOOTSTRAP_UNINSTALLERS_DIR/${tool}.cmds"
|
||||
|
||||
if [ ! -f "$manifest" ]; then
|
||||
log_warn "No rollback manifest found for '$tool'."
|
||||
return 0
|
||||
fi
|
||||
|
||||
export BOOTSTRAP_CURRENT_TOOL="$tool"
|
||||
log_info "Rolling back '$tool'..."
|
||||
while IFS= read -r cmd; do
|
||||
[ -z "$cmd" ] && continue
|
||||
log_info "Executing: $cmd"
|
||||
eval "$cmd" || log_warn "Failed to execute: $cmd"
|
||||
done < "$manifest"
|
||||
|
||||
rm -f "$manifest"
|
||||
log_success "Rollback of '$tool' complete."
|
||||
}
|
||||
|
||||
rollback_bare() {
|
||||
if [ ! -s "$BOOTSTRAP_HISTORY_LOG" ]; then
|
||||
log_info "No history available to rollback."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local last_line
|
||||
last_line=$(tail -n 1 "$BOOTSTRAP_HISTORY_LOG")
|
||||
|
||||
if [[ "$last_line" == INSTALL:* ]]; then
|
||||
local tool="${last_line#INSTALL: }"
|
||||
execute_rollback "$tool"
|
||||
# Remove the last line efficiently
|
||||
sed -i '$ d' "$BOOTSTRAP_HISTORY_LOG"
|
||||
elif [[ "$last_line" == SAVEPOINT:* ]]; then
|
||||
local sp="${last_line#SAVEPOINT: }"
|
||||
log_warn "Last action was savepoint '$sp'. Cannot bare-rollback a savepoint."
|
||||
fi
|
||||
}
|
||||
|
||||
rollback_to_savepoint() {
|
||||
local target_sp="$1"
|
||||
|
||||
if ! grep -q "SAVEPOINT: $target_sp" "$BOOTSTRAP_HISTORY_LOG"; then
|
||||
log_error "Savepoint '$target_sp' not found in history."
|
||||
return 1
|
||||
fi
|
||||
|
||||
while [ -s "$BOOTSTRAP_HISTORY_LOG" ]; do
|
||||
local last_line
|
||||
last_line=$(tail -n 1 "$BOOTSTRAP_HISTORY_LOG")
|
||||
|
||||
if [[ "$last_line" == SAVEPOINT:\ $target_sp ]]; then
|
||||
log_success "Reached savepoint '$target_sp'."
|
||||
# Optionally remove the savepoint itself or keep it? Let's keep it.
|
||||
break
|
||||
elif [[ "$last_line" == INSTALL:* ]]; then
|
||||
local tool="${last_line#INSTALL: }"
|
||||
execute_rollback "$tool"
|
||||
sed -i '$ d' "$BOOTSTRAP_HISTORY_LOG"
|
||||
elif [[ "$last_line" == SAVEPOINT:* ]]; then
|
||||
local sp="${last_line#SAVEPOINT: }"
|
||||
log_info "Removing intermediate savepoint '$sp'..."
|
||||
sed -i '$ d' "$BOOTSTRAP_HISTORY_LOG"
|
||||
else
|
||||
# Unknown line format, just remove it
|
||||
sed -i '$ d' "$BOOTSTRAP_HISTORY_LOG"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
export -f init_rollback_system setup_uninstaller_context add_rollback_cmd track_file track_dir create_savepoint mark_install_success execute_rollback rollback_bare rollback_to_savepoint
|
||||
@@ -14,8 +14,10 @@ export BOOTSTRAP_DIR
|
||||
# Source libraries
|
||||
if [ -f "$BOOTSTRAP_DIR/lib/common.sh" ]; then
|
||||
. "$BOOTSTRAP_DIR/lib/common.sh"
|
||||
. "$BOOTSTRAP_DIR/lib/rollback.sh"
|
||||
. "$BOOTSTRAP_DIR/lib/platform.sh"
|
||||
. "$BOOTSTRAP_DIR/lib/shell_config.sh"
|
||||
init_rollback_system
|
||||
else
|
||||
echo "Error: Bootstrap libraries not found at $BOOTSTRAP_DIR/lib/" >&2
|
||||
exit 1
|
||||
@@ -111,9 +113,14 @@ run_ware() {
|
||||
|
||||
# Run the script (edited or unchanged)
|
||||
log_info "Running ${display_name} installer..."
|
||||
setup_uninstaller_context "$tool"
|
||||
bash "$temp_script" "${cmd_args[@]}"
|
||||
local run_status=$?
|
||||
|
||||
if [ "$run_status" -eq 0 ]; then
|
||||
mark_install_success "$tool"
|
||||
fi
|
||||
|
||||
# Cleanup
|
||||
rm -f "$temp_script"
|
||||
return "$run_status"
|
||||
@@ -193,6 +200,24 @@ for script in "${SCRIPTS[@]}"; do
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
fall)
|
||||
local savepoint_name="${1:-}"
|
||||
if [ -z "$savepoint_name" ]; then
|
||||
log_error "Usage: b fall <savepoint_name>"
|
||||
exit 1
|
||||
fi
|
||||
create_savepoint "$savepoint_name"
|
||||
exit 0
|
||||
;;
|
||||
rb)
|
||||
local target="${1:-}"
|
||||
if [ -z "$target" ]; then
|
||||
rollback_bare
|
||||
else
|
||||
rollback_to_savepoint "$target"
|
||||
fi
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown command '$script'."
|
||||
log_info "Run 'b all' to list all available commands."
|
||||
|
||||
@@ -112,6 +112,10 @@ write_env_snippet() {
|
||||
mkdir -p "$dir"
|
||||
log_info "Writing environment snippet '$name' to $dir/${name}.sh"
|
||||
echo "$content" > "$dir/${name}.sh"
|
||||
|
||||
if type add_rollback_cmd >/dev/null 2>&1; then
|
||||
add_rollback_cmd "rm -f \"$dir/${name}.sh\""
|
||||
fi
|
||||
}
|
||||
|
||||
# Write alias snippet to aliases.d/
|
||||
@@ -124,6 +128,10 @@ write_alias_snippet() {
|
||||
mkdir -p "$dir"
|
||||
log_info "Writing alias snippet '$name' to $dir/${name}.sh"
|
||||
echo "$content" > "$dir/${name}.sh"
|
||||
|
||||
if type add_rollback_cmd >/dev/null 2>&1; then
|
||||
add_rollback_cmd "rm -f \"$dir/${name}.sh\""
|
||||
fi
|
||||
}
|
||||
|
||||
# Remove environment snippet from env.d/
|
||||
|
||||
Reference in New Issue
Block a user