From 7f4e6376f24ff6b9fd8d4b650119989ff4eef463 Mon Sep 17 00:00:00 2001 From: "LINUXexpert.org" <51408202+LINUXexpert-org@users.noreply.github.com> Date: Thu, 22 Jan 2026 11:13:22 -0700 Subject: [PATCH] Initial commit --- ubuntu-to-mint-convert-v3.sh | 647 +++++++++++++++++++++++++++++++++++ 1 file changed, 647 insertions(+) create mode 100644 ubuntu-to-mint-convert-v3.sh diff --git a/ubuntu-to-mint-convert-v3.sh b/ubuntu-to-mint-convert-v3.sh new file mode 100644 index 0000000..b1db44c --- /dev/null +++ b/ubuntu-to-mint-convert-v3.sh @@ -0,0 +1,647 @@ +#!/usr/bin/env bash +# ubuntu-to-mint-convert-v3.sh +# +# Copyright (C) 2026 LINUXexpert.org +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation, version 3 of the License. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program. If not, see . +# +# Keep Ubuntu as the base OS and add Linux Mint repos + Mint desktop/tooling. +# This is NOT an officially supported migration path. Use at your own risk. +# +# Supported paths (current supported Mint bases): +# - Ubuntu 24.04 (noble) -> Linux Mint 22.x (default: 22.3 "zena") +# - Ubuntu 22.04 (jammy) -> Linux Mint 21.x (default: 21.3 "virginia") +# +# Modes: +# doctor : run preflight checks only +# plan [opts] : dry-run apt simulation using temporary apt dirs (no system changes) +# convert --i-accept-the-risk [opts] : apply changes, install Mint packages +# rollback : restore /etc/apt + sources from a backup created by this script +# +# Common options: +# --edition cinnamon|mate|xfce (default: cinnamon) +# --target zena|zara|xia|wilma|virginia|victoria|vera|vanessa (default depends on Ubuntu base) +# --mint-mirror URL (default: http://packages.linuxmint.com) +# --keep-ppas (default: PPAs/3rd-party sources are disabled during convert) +# --preserve-snap (default: yes; removes Mint's "nosnap" pin if it appears) +# --with-recommends (default: no; safer for corporate systems) +# --yes (skip interactive confirmation) +# +set -Eeuo pipefail +IFS=$'\n\t' + +SCRIPT_VERSION="3.0" +LOG_DIR="/var/log/ubuntu-to-mint" +mkdir -p "$LOG_DIR" +LOG_FILE="$LOG_DIR/ubuntu-to-mint-$(date +%Y%m%d-%H%M%S).log" +exec > >(tee -a "$LOG_FILE") 2>&1 + +RED=$'\033[31m'; YEL=$'\033[33m'; GRN=$'\033[32m'; BLU=$'\033[34m'; NC=$'\033[0m' + +die() { echo "${RED}ERROR:${NC} $*" >&2; exit 1; } +warn(){ echo "${YEL}WARN:${NC} $*" >&2; } +info(){ echo "${BLU}INFO:${NC} $*"; } +ok() { echo "${GRN}OK:${NC} $*"; } + +ON_ERROR_BACKUP_HINT="" +on_err() { + local line="${1:-?}" code="${2:-?}" + echo "${RED}FAILED${NC} at line ${line} (exit ${code})." >&2 + if [[ -n "${ON_ERROR_BACKUP_HINT}" ]]; then + echo "Rollback hint: ${ON_ERROR_BACKUP_HINT}" >&2 + fi + echo "Log: ${LOG_FILE}" >&2 + exit "$code" +} +trap 'on_err "$LINENO" "$?"' ERR + +need_root() { [[ ${EUID:-$(id -u)} -eq 0 ]] || die "Run as root (use sudo)."; } + +have_cmd() { command -v "$1" >/dev/null 2>&1; } + +usage() { + cat <<'EOF' +Usage: + sudo bash ubuntu-to-mint-convert-v3.sh doctor + sudo bash ubuntu-to-mint-convert-v3.sh plan [--edition cinnamon|mate|xfce] [--target ] [--mint-mirror ] [--with-recommends] + sudo bash ubuntu-to-mint-convert-v3.sh convert --i-accept-the-risk [options...] + sudo bash ubuntu-to-mint-convert-v3.sh rollback /root/ubuntu-to-mint-backup-YYYYMMDD-HHMMSS + +Options: + --edition cinnamon|mate|xfce + --target zena|zara|xia|wilma|virginia|victoria|vera|vanessa + --mint-mirror URL + --keep-ppas + --preserve-snap + --with-recommends + --yes +EOF +} + +# --- Defaults --- +MODE="${1:-}" +EDITION="cinnamon" +TARGET_MINT="" +MINT_MIRROR="http://packages.linuxmint.com" +KEEP_PPAS="no" +PRESERVE_SNAP="yes" +WITH_RECOMMENDS="no" +ASSUME_YES="no" + +# --- Parse args (simple) --- +shift || true +ACCEPT_RISK="no" +while [[ $# -gt 0 ]]; do + case "$1" in + --i-accept-the-risk) ACCEPT_RISK="yes"; shift;; + --edition) EDITION="${2:-}"; shift 2;; + --target) TARGET_MINT="${2:-}"; shift 2;; + --mint-mirror) MINT_MIRROR="${2:-}"; shift 2;; + --keep-ppas) KEEP_PPAS="yes"; shift;; + --preserve-snap) PRESERVE_SNAP="yes"; shift;; + --with-recommends) WITH_RECOMMENDS="yes"; shift;; + --yes) ASSUME_YES="yes"; shift;; + -h|--help) usage; exit 0;; + *) die "Unknown arg: $1 (use --help)";; + esac +done + +validate_choice() { + case "$1" in + cinnamon|mate|xfce) ;; + *) die "Invalid --edition '$1' (use cinnamon|mate|xfce)";; + esac +} + +read_os_release() { + [[ -r /etc/os-release ]] || die "/etc/os-release missing" + # shellcheck disable=SC1091 + . /etc/os-release + OS_ID="${ID:-}" + OS_NAME="${NAME:-}" + OS_VERSION_CODENAME="${VERSION_CODENAME:-}" + OS_VERSION_ID="${VERSION_ID:-}" +} + +detect_ubuntu_codename() { + local c="" + if have_cmd lsb_release; then + c="$(lsb_release -cs 2>/dev/null || true)" + fi + if [[ -z "$c" ]]; then + c="${OS_VERSION_CODENAME:-}" + fi + [[ -n "$c" ]] || die "Could not determine Ubuntu codename." + echo "$c" +} + +set_targets_from_ubuntu() { + UBUNTU_CODENAME="$(detect_ubuntu_codename)" + case "$UBUNTU_CODENAME" in + noble) + UBUNTU_BASE="noble" + DEFAULT_MINT="zena" # Mint 22.3 recommended on Ubuntu Noble base + ALLOWED_TARGETS="zena zara xia wilma" + ;; + jammy) + UBUNTU_BASE="jammy" + DEFAULT_MINT="virginia" # Mint 21.3 on Ubuntu Jammy base + ALLOWED_TARGETS="virginia victoria vera vanessa" + ;; + *) + die "Unsupported Ubuntu codename '$UBUNTU_CODENAME'. This script supports Ubuntu noble (24.04) or jammy (22.04) only." + ;; + esac + + if [[ -z "$TARGET_MINT" ]]; then + TARGET_MINT="$DEFAULT_MINT" + fi + + # Validate target is allowed for this base + local ok_target="no" + for t in $ALLOWED_TARGETS; do + [[ "$TARGET_MINT" == "$t" ]] && ok_target="yes" + done + [[ "$ok_target" == "yes" ]] || die "--target '$TARGET_MINT' not allowed for Ubuntu '$UBUNTU_CODENAME'. Allowed: $ALLOWED_TARGETS" +} + +preflight_common() { + need_root + validate_choice "$EDITION" + read_os_release + + info "Script v${SCRIPT_VERSION}" + info "Detected OS: ${OS_NAME} (ID=${OS_ID}, VERSION_ID=${OS_VERSION_ID}, CODENAME=${OS_VERSION_CODENAME})" + + [[ "$OS_ID" == "ubuntu" ]] || die "This script is intended for Ubuntu (ID=ubuntu). Detected ID=${OS_ID}." + set_targets_from_ubuntu + ok "Ubuntu base: ${UBUNTU_BASE} | Target Mint codename: ${TARGET_MINT} | Edition: ${EDITION}" + + # Refuse containers (too many surprises) + if have_cmd systemd-detect-virt; then + if systemd-detect-virt --container >/dev/null 2>&1; then + die "Detected container environment. Aborting." + fi + fi + + # Architecture check + local arch + arch="$(dpkg --print-architecture)" + [[ "$arch" == "amd64" ]] || warn "Architecture is '$arch'. Mint is typically amd64 on desktops. Proceeding may fail." + + # APT locks + if fuser /var/lib/dpkg/lock >/dev/null 2>&1 || fuser /var/lib/apt/lists/lock >/dev/null 2>&1 || fuser /var/cache/apt/archives/lock >/dev/null 2>&1; then + die "APT/DPKG lock is held (updates running?). Close Software Updater/apt and try again." + fi + + # dpkg health + if dpkg --audit | grep -q .; then + warn "dpkg reports issues (dpkg --audit not empty). Attempting to fix..." + DEBIAN_FRONTEND=noninteractive dpkg --configure -a + fi + DEBIAN_FRONTEND=noninteractive apt-get -y -f install + + # Network reachability (lightweight) + have_cmd curl || DEBIAN_FRONTEND=noninteractive apt-get update -y && DEBIAN_FRONTEND=noninteractive apt-get install -y curl ca-certificates + curl -fsS --max-time 10 "http://archive.ubuntu.com/ubuntu/" >/dev/null || warn "Cannot reach archive.ubuntu.com (may be corporate mirror/proxy setup)." + curl -fsS --max-time 10 "${MINT_MIRROR%/}/" >/dev/null || warn "Cannot reach Mint mirror ${MINT_MIRROR} (may be intermittent)." + + # Disk space + local root_free + root_free="$(df -Pm / | awk 'NR==2{print $4}')" + if [[ "${root_free:-0}" -lt 6144 ]]; then + warn "Low free space on / (${root_free} MB). Recommend >= 6GB free." + fi + + # Battery check (if laptop) + if [[ -d /sys/class/power_supply ]]; then + local on_batt="no" + for ps in /sys/class/power_supply/*; do + [[ -r "$ps/type" ]] || continue + if grep -qi "battery" "$ps/type"; then + if [[ -r "$ps/status" ]] && grep -qi "discharging" "$ps/status"; then + on_batt="yes" + fi + fi + done + [[ "$on_batt" == "yes" ]] && warn "System appears to be on battery. Plug into AC before convert." + fi + + # Snap inventory (for corporate safety) + if have_cmd snap; then + info "Snap detected. Installed snaps:" + snap list || true + else + info "snap not installed." + fi + + ok "Preflight completed." +} + +# Try to detect Ubuntu mirrors from existing config so we don't break corporate proxy/mirrors. +detect_ubuntu_mirrors() { + local sources_txt="" + # collect possible deb lines from .list and .sources + if [[ -r /etc/apt/sources.list ]]; then sources_txt+="$(grep -E '^[[:space:]]*deb ' /etc/apt/sources.list || true)"$'\n'; fi + if compgen -G "/etc/apt/sources.list.d/*.list" >/dev/null; then + sources_txt+="$(grep -RhsE '^[[:space:]]*deb ' /etc/apt/sources.list.d/*.list || true)"$'\n' + fi + if compgen -G "/etc/apt/sources.list.d/*.sources" >/dev/null; then + # very rough extraction of URIs from deb822 sources + sources_txt+="$(grep -RhsE '^[[:space:]]*URIs:[[:space:]]*' /etc/apt/sources.list.d/*.sources | awk '{print $2}' || true)"$'\n' + fi + + # defaults + UBUNTU_ARCHIVE_MIRROR="http://archive.ubuntu.com/ubuntu" + UBUNTU_SECURITY_MIRROR="http://security.ubuntu.com/ubuntu" + + # try to find an existing archive mirror + local m + m="$(echo "$sources_txt" | grep -Eo 'https?://[^ ]+/ubuntu' | head -n1 || true)" + [[ -n "$m" ]] && UBUNTU_ARCHIVE_MIRROR="$m" + + # try to find an existing security mirror + local s + s="$(echo "$sources_txt" | grep -Eo 'https?://[^ ]+/ubuntu' | grep -E 'security\.ubuntu\.com|/ubuntu-security' | head -n1 || true)" + [[ -n "$s" ]] && UBUNTU_SECURITY_MIRROR="$s" + + info "Ubuntu archive mirror: ${UBUNTU_ARCHIVE_MIRROR}" + info "Ubuntu security mirror: ${UBUNTU_SECURITY_MIRROR}" +} + +mint_repo_key_install() { + # Linux Mint repository signing key (short ID commonly referenced as A6616109451BBBF2) + # We fetch via hkps and store as a dedicated keyring file for signed-by usage. + local keyid="A6616109451BBBF2" + local keyring="/usr/share/keyrings/linuxmint-repo.gpg" + + info "Installing Linux Mint repo signing key into ${keyring}" + DEBIAN_FRONTEND=noninteractive apt-get update -y + DEBIAN_FRONTEND=noninteractive apt-get install -y gnupg dirmngr ca-certificates + + local gnupghome + gnupghome="$(mktemp -d)" + chmod 700 "$gnupghome" + gpg --homedir "$gnupghome" --batch --keyserver hkps://keyserver.ubuntu.com --recv-keys "$keyid" + gpg --homedir "$gnupghome" --batch --export "$keyid" | gpg --dearmor -o "$keyring" + chmod 644 "$keyring" + rm -rf "$gnupghome" + ok "Key installed." +} + +write_mint_sources_system() { + local list="/etc/apt/sources.list.d/official-package-repositories.list" + local keyring="/usr/share/keyrings/linuxmint-repo.gpg" + + detect_ubuntu_mirrors + + info "Writing Mint+Ubuntu sources to ${list}" + cat > "$list" < "$pref" <<'EOF' +# Conservative pinning for Ubuntu->Mint Option B conversion. +# Keep Ubuntu as default; allow Mint only for Mint desktop/tooling packages. +# +# Default: low priority for Mint origin so it won't replace base packages automatically. +Package: * +Pin: origin "packages.linuxmint.com" +Pin-Priority: 100 + +# Mint tooling / desktop stack: prefer Mint packages. +Package: mint* mintsources* mintupdate* mintsystem* mintstick* mintmenu* mintdrivers* mintreport* mintwelcome* mintlocale* +Pin: origin "packages.linuxmint.com" +Pin-Priority: 700 + +Package: cinnamon* nemo* muffin* cjs* xapp* slick-greeter* lightdm* pix* xviewer* mint-themes* mint-y-icons* mint-x-icons* +Pin: origin "packages.linuxmint.com" +Pin-Priority: 700 +EOF + + ok "Pinning written." +} + +disable_thirdparty_sources_system() { + # Moves third-party sources out of the way to reduce conflicts during install. + # We back them up first and leave a marker so rollback can restore. + local backup_dir="$1" + local disabled_dir="${backup_dir}/disabled-sources" + mkdir -p "$disabled_dir" + + info "Disabling 3rd-party sources (PPAs, vendor repos) into: ${disabled_dir}" + shopt -s nullglob + + for f in /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; do + # keep our generated official list intact + [[ "$(basename "$f")" == "official-package-repositories.list" ]] && continue + # disable everything else unless user opted to keep + mv -v "$f" "${disabled_dir}/" || true + done + + shopt -u nullglob + ok "Third-party sources disabled (restorable via rollback)." +} + +backup_system_state() { + local backup_dir="/root/ubuntu-to-mint-backup-$(date +%Y%m%d-%H%M%S)" + mkdir -p "$backup_dir" + ON_ERROR_BACKUP_HINT="sudo bash $0 rollback ${backup_dir}" + + info "Creating backup at: ${backup_dir}" + mkdir -p "${backup_dir}/etc" + cp -a /etc/apt "${backup_dir}/etc/" || true + cp -a /etc/os-release /etc/lsb-release 2>/dev/null "${backup_dir}/etc/" || true + cp -a /etc/fstab /etc/hostname /etc/hosts 2>/dev/null "${backup_dir}/etc/" || true + + dpkg-query -W -f='${Package}\t${Version}\n' > "${backup_dir}/dpkg-packages.tsv" || true + apt-mark showmanual > "${backup_dir}/apt-manual.txt" || true + apt-mark showhold > "${backup_dir}/apt-holds.txt" || true + systemctl list-unit-files --state=enabled > "${backup_dir}/enabled-services.txt" || true + + if have_cmd snap; then + snap list > "${backup_dir}/snap-list.txt" || true + fi + if have_cmd flatpak; then + flatpak list > "${backup_dir}/flatpak-list.txt" || true + fi + + ok "Backup complete." + echo "$backup_dir" +} + +timeshift_snapshot_best_effort() { + if have_cmd timeshift; then + info "Timeshift detected. Attempting pre-change snapshot (best-effort)..." + # Timeshift might not be configured; don't fail conversion if it can't snapshot. + timeshift --create --comments "pre ubuntu->mint option-b $(date -Is)" --tags D || warn "Timeshift snapshot failed (may not be configured)." + else + warn "Timeshift not installed. Strongly recommended to snapshot/backup before converting." + fi +} + +apt_simulate_with_temp_sources() { + # Use a temporary APT environment for plan mode: no system changes. + local tmp + tmp="$(mktemp -d)" + mkdir -p "$tmp/etc/apt/sources.list.d" "$tmp/var/lib/apt/lists/partial" "$tmp/var/cache/apt/archives/partial" + + # Keyring: use system keyring file if present; otherwise, we can't verify Mint repo. + local keyring="/usr/share/keyrings/linuxmint-repo.gpg" + [[ -r "$keyring" ]] || die "Mint keyring not found at ${keyring}. Run convert once (it installs key) or install key manually." + + detect_ubuntu_mirrors + cat > "$tmp/etc/apt/sources.list" </dev/null 2>&1 || true + cp -a /etc/apt/preferences.d/50-linuxmint-conversion.pref "$tmp/etc/apt/preferences.d/" 2>/dev/null || true + + info "Plan mode: apt update (temporary dirs)..." + apt-get \ + -o Dir::Etc="$tmp/etc/apt" \ + -o Dir::State="$tmp/var/lib/apt" \ + -o Dir::Cache="$tmp/var/cache/apt" \ + -o Dir::State::status="/var/lib/dpkg/status" \ + -o Acquire::Retries=3 \ + update + + local recommends="--no-install-recommends" + [[ "$WITH_RECOMMENDS" == "yes" ]] && recommends="" + + local pkgs=() + case "$EDITION" in + cinnamon) pkgs+=(mint-meta-cinnamon) ;; + mate) pkgs+=(mint-meta-mate) ;; + xfce) pkgs+=(mint-meta-xfce) ;; + esac + pkgs+=(mint-meta-core mint-meta-codecs mintsystem mintupdate mintsources) + + info "Plan mode: simulated install: ${pkgs[*]}" + apt-get \ + -o Dir::Etc="$tmp/etc/apt" \ + -o Dir::State="$tmp/var/lib/apt" \ + -o Dir::Cache="$tmp/var/cache/apt" \ + -o Dir::State::status="/var/lib/dpkg/status" \ + -o Acquire::Retries=3 \ + -s install $recommends "${pkgs[@]}" | tee "$LOG_DIR/plan-$(date +%Y%m%d-%H%M%S).txt" >/dev/null + + rm -rf "$tmp" + ok "Plan completed. Review the plan log in ${LOG_DIR}." +} + +parse_and_guard_apt_actions() { + # Guard rails: abort if apt wants to remove critical packages. + local sim_output_file="$1" + [[ -r "$sim_output_file" ]] || die "Missing simulation output: $sim_output_file" + + local removed + removed="$(grep -E '^Remv ' "$sim_output_file" | awk '{print $2}' || true)" + local removed_count + removed_count="$(echo "$removed" | grep -c . || true)" + + info "APT simulation: packages marked for removal: ${removed_count}" + + # Critical packages we refuse to remove (extend as needed) + local critical_re='^(sudo|openssh-server|ssh|network-manager|systemd|systemd-sysv|dbus|polkit|linux-image|linux-generic|linux-modules|grub|grub2|initramfs-tools|libc6|libstdc\+\+6|snapd)$' + local bad="" + while IFS= read -r p; do + [[ -z "$p" ]] && continue + if [[ "$p" =~ $critical_re ]]; then + bad+="$p"$'\n' + fi + done <<< "$removed" + + if [[ -n "$bad" ]]; then + echo "$bad" | sed 's/^/ - /' + die "Refusing to proceed: simulation removes critical packages above." + fi + + if [[ "$removed_count" -gt 20 ]]; then + die "Refusing to proceed: too many removals (${removed_count}). Re-run with --with-recommends off (default), or inspect conflicts." + fi + + ok "Guard rails passed (no critical removals; removals <= 20)." +} + +convert_apply() { + [[ "$ACCEPT_RISK" == "yes" ]] || die "You must pass --i-accept-the-risk to run convert." + + preflight_common + + if [[ "$ASSUME_YES" != "yes" ]]; then + echo + warn "This can break a corporate-managed machine. Ensure you have approval + a rollback plan (snapshot/backup)." + echo "Target: Ubuntu ${UBUNTU_BASE} -> Mint ${TARGET_MINT} (${EDITION})" + read -r -p "Type 'I UNDERSTAND' to continue: " ans + [[ "$ans" == "I UNDERSTAND" ]] || die "Aborted by user." + fi + + local backup_dir + backup_dir="$(backup_system_state)" + timeshift_snapshot_best_effort + + # Quiet apt timers that can race us + systemctl stop apt-daily.service apt-daily-upgrade.service 2>/dev/null || true + systemctl kill --kill-who=all apt-daily.service apt-daily-upgrade.service 2>/dev/null || true + + if [[ "$KEEP_PPAS" != "yes" ]]; then + disable_thirdparty_sources_system "$backup_dir" + else + warn "--keep-ppas set. Proceeding with third-party sources enabled may increase conflict risk." + fi + + mint_repo_key_install + write_mint_sources_system + write_mint_pinning_system + + # Update + simulate real install on the real system (but still simulation first) + info "APT update..." + DEBIAN_FRONTEND=noninteractive apt-get -o Acquire::Retries=3 update + + local recommends="--no-install-recommends" + [[ "$WITH_RECOMMENDS" == "yes" ]] && recommends="" + + local pkgs=() + case "$EDITION" in + cinnamon) pkgs+=(mint-meta-cinnamon) ;; + mate) pkgs+=(mint-meta-mate) ;; + xfce) pkgs+=(mint-meta-xfce) ;; + esac + # keep this minimal; meta packages can be large – avoid recommends by default + pkgs+=(mint-meta-core mintsystem mintupdate mintsources) + + # Optional codecs (often desired, but can pull extra) – keep it but still no recommends + pkgs+=(mint-meta-codecs) + + info "Simulation (safety check) of install: ${pkgs[*]}" + local sim_out="${backup_dir}/apt-sim-install.txt" + DEBIAN_FRONTEND=noninteractive apt-get -s install $recommends "${pkgs[@]}" | tee "$sim_out" >/dev/null + parse_and_guard_apt_actions "$sim_out" + + # Execute install + info "Installing Mint packages..." + export DEBIAN_FRONTEND=noninteractive + export NEEDRESTART_MODE=a + apt-get -y install \ + -o Dpkg::Options::=--force-confdef \ + -o Dpkg::Options::=--force-confold \ + $recommends \ + "${pkgs[@]}" + + # Preserve Snap (Mint often pins snapd; we undo that if it appears and user asked to preserve) + if [[ "$PRESERVE_SNAP" == "yes" ]]; then + local nosnap="/etc/apt/preferences.d/nosnap.pref" + if [[ -f "$nosnap" ]]; then + warn "Mint 'nosnap' preference detected at ${nosnap}. Removing to preserve snap functionality." + rm -f "$nosnap" + apt-get -o Acquire::Retries=3 update + fi + fi + + # Post-checks + info "Post-checks:" + apt-cache policy mintsystem | sed 's/^/ /' || true + if have_cmd cinnamon; then cinnamon --version | sed 's/^/ /' || true; fi + if have_cmd nemo; then nemo --version | sed 's/^/ /' || true; fi + + ok "Conversion complete (Option B)." + info "Backup dir: ${backup_dir}" + info "Log file: ${LOG_FILE}" + echo + echo "Next steps:" + echo " 1) Reboot." + echo " 2) At the login screen, choose the '${EDITION^}' session." + echo " 3) Validate corp software: VPN, EDR/agent, SSO, MDM posture, printers, smartcard, etc." +} + +rollback_apply() { + need_root + local backup_dir="${2:-}" + [[ -n "$backup_dir" ]] || die "rollback requires a backup dir argument." + [[ -d "$backup_dir" ]] || die "No such backup dir: $backup_dir" + [[ -d "${backup_dir}/etc/apt" ]] || die "Backup dir missing etc/apt: $backup_dir" + + info "Restoring /etc/apt from backup: ${backup_dir}/etc/apt" + rm -rf /etc/apt + cp -a "${backup_dir}/etc/apt" /etc/apt + + # Restore disabled sources if present + if [[ -d "${backup_dir}/disabled-sources" ]]; then + info "Restoring disabled sources from ${backup_dir}/disabled-sources" + mkdir -p /etc/apt/sources.list.d + cp -a "${backup_dir}/disabled-sources/." /etc/apt/sources.list.d/ || true + fi + + info "APT update after rollback..." + DEBIAN_FRONTEND=noninteractive apt-get -o Acquire::Retries=3 update || true + ok "Rollback of APT configuration completed." + info "Note: This does NOT automatically remove Mint-installed packages. Use Timeshift/snapshot to fully revert the system state." +} + +main() { + case "$MODE" in + doctor) + preflight_common + ;; + plan) + preflight_common + # plan requires keyring already present (or run convert once to install it) + apt_simulate_with_temp_sources + ;; + convert) + convert_apply + ;; + rollback) + rollback_apply "$MODE" "$2" + ;; + *) + usage + exit 1 + ;; + esac +} + +main