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