From b31a326ca114cb7661e7c50b9b35ae7af3d0677c Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Wed, 24 Jun 2026 22:01:30 +0530 Subject: [PATCH] feat: Implement Rollbacks and Savepoints! --- bootstrap.sh | 5 +- lib/platform.sh | 70 ++++++++++++++++++++---- lib/rollback.sh | 130 ++++++++++++++++++++++++++++++++++++++++++++ lib/routes.sh | 25 +++++++++ lib/shell_config.sh | 8 +++ 5 files changed, 225 insertions(+), 13 deletions(-) create mode 100644 lib/rollback.sh diff --git a/bootstrap.sh b/bootstrap.sh index ede2687..c6f422c 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -41,7 +41,7 @@ else BOOTSTRAP_SOURCE_DIR="$BOOTSTRAP_TMP_DIR" _BASE_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master" - _LIBS=("lib/common.sh" "lib/platform.sh" "lib/shell_config.sh") + _LIBS=("lib/common.sh" "lib/rollback.sh" "lib/platform.sh" "lib/shell_config.sh") _curl_args=() for _lib in "${_LIBS[@]}"; do @@ -53,8 +53,10 @@ fi if [ -f "$BOOTSTRAP_SOURCE_DIR/lib/common.sh" ]; then . "$BOOTSTRAP_SOURCE_DIR/lib/common.sh" + . "$BOOTSTRAP_SOURCE_DIR/lib/rollback.sh" . "$BOOTSTRAP_SOURCE_DIR/lib/platform.sh" . "$BOOTSTRAP_SOURCE_DIR/lib/shell_config.sh" + init_rollback_system else echo "Error: Failed to locate or download bootstrap libraries." >&2 exit 1 @@ -77,6 +79,7 @@ install_bootstrap() { "lib/routes.sh" "lib/registry.sh" "lib/common.sh" + "lib/rollback.sh" "lib/platform.sh" "lib/shell_config.sh" "commands/help.sh" diff --git a/lib/platform.sh b/lib/platform.sh index 415b283..3bba570 100644 --- a/lib/platform.sh +++ b/lib/platform.sh @@ -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 } diff --git a/lib/rollback.sh b/lib/rollback.sh new file mode 100644 index 0000000..152cb4f --- /dev/null +++ b/lib/rollback.sh @@ -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 diff --git a/lib/routes.sh b/lib/routes.sh index 7401dd2..0018599 100755 --- a/lib/routes.sh +++ b/lib/routes.sh @@ -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 " + 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." diff --git a/lib/shell_config.sh b/lib/shell_config.sh index b810984..7f4bb8a 100644 --- a/lib/shell_config.sh +++ b/lib/shell_config.sh @@ -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/