From af1102b5fd9413a01d96bac199abeb3db232b192 Mon Sep 17 00:00:00 2001
From: "LINUXexpert.org" <51408202+LINUXexpert-org@users.noreply.github.com>
Date: Mon, 26 Jan 2026 10:11:10 -0700
Subject: [PATCH] Update print statement from 'Hello' to 'Goodbye'
---
ubuntu-to-mint-convert-v3.sh | 2157 ++++++++++++++++------------------
1 file changed, 1019 insertions(+), 1138 deletions(-)
diff --git a/ubuntu-to-mint-convert-v3.sh b/ubuntu-to-mint-convert-v3.sh
index 3e03e65..dc076fe 100644
--- a/ubuntu-to-mint-convert-v3.sh
+++ b/ubuntu-to-mint-convert-v3.sh
@@ -1,4 +1,5 @@
#!/usr/bin/env bash
+
# ubuntu-to-mint-convert-v3.sh
#
# Copyright (C) 2026 LINUXexpert.org
@@ -14,574 +15,583 @@
#
# You should have received a copy of the GNU General Public License along
# with this program. If not, see .
-#
-# ----------------------------------------------------------------------
-# Purpose:
-# Keep Ubuntu as the base OS and add Linux Mint repositories + install a
-# Mint desktop environment, while preserving corporate tooling as much
-# as possible.
-#
-# Supported:
-# Ubuntu 24.04 (noble) -> Linux Mint 22.x (default target: zena)
-# Ubuntu 22.04 (jammy) -> Linux Mint 21.x (default target: virginia)
-#
-# Modes:
-# doctor
-# plan
-# convert --i-accept-the-risk
-# rollback
-#
-# X11/Wayland note:
-# This script defaults to an X11 session (safer for conversions/corp tooling).
-# You may pass --prefer-wayland, but since this script standardizes on LightDM
-# (Mint default), it will only select a Wayland session if it is actually
-# available as a LightDM-compatible Xsession. Otherwise it will warn and use X11.
-# ----------------------------------------------------------------------
set -Eeuo pipefail
IFS=$'\n\t'
-umask 022
-SCRIPT_VERSION="4.3"
+###############################################################################
+# Version / globals
+###############################################################################
+SCRIPT_NAME="$(basename "$0")"
+SCRIPT_VERSION="4.4"
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
+DEFAULT_MINT_MIRROR="http://packages.linuxmint.com"
+KEYRING_OUT="/usr/share/keyrings/linuxmint-repo.gpg"
+SOURCES_OUT="/etc/apt/sources.list.d/official-package-repositories.list"
+PIN_OUT="/etc/apt/preferences.d/50-linuxmint-conversion.pref"
+PIN_STACK_OUT="/etc/apt/preferences.d/51-linuxmint-desktop-stack.pref"
-export LC_ALL=C
+# Safety thresholds
+MAX_ALLOWED_REMOVALS_DEFAULT=40
-RED=$'\033[31m'; YEL=$'\033[33m'; GRN=$'\033[32m'; BLU=$'\033[34m'; NC=$'\033[0m'
-BOLD=$'\033[1m'
-
-die() { echo "${RED}ERROR:${NC} $*" >&2; exit 1; }
-warn(){ echo "${YEL}WARN:${NC} $*" >&2; }
-info(){ echo "${BLU}INFO:${NC} $*"; }
-ok() { echo "${GRN}OK:${NC} $*"; }
-
-# Optional: set to full fingerprint to hard-fail if it doesn't match.
-# Example format: "0123456789ABCDEF0123456789ABCDEF01234567"
-MINT_KEY_FPR_EXPECT="${MINT_KEY_FPR_EXPECT:-}"
-
-ON_ERROR_BACKUP_HINT=""
-on_err() {
- local line="${1:-?}" code="${2:-?}"
- echo "${RED}FAILED${NC} at line ${line} (exit ${code})." >&2
- echo "Command: ${BASH_COMMAND}" >&2
- [[ -n "${ON_ERROR_BACKUP_HINT}" ]] && echo "Rollback hint: ${ON_ERROR_BACKUP_HINT}" >&2
- 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; }
-
-# -------------------------
-# UNSUPPORTED DISCLAIMER GATE (CONVERT ONLY)
-# -------------------------
-require_unsupported_disclaimer() {
- local phrase="I UNDERSTAND THIS IS UNSUPPORTED"
- echo
- echo -e "${RED}${BOLD}======================================================================${NC}"
- echo -e "${RED}${BOLD} UNSUPPORTED MIGRATION METHOD — PROBABLY A REALLY DUMB IDEA${NC}"
- echo -e "${RED}${BOLD}======================================================================${NC}"
- echo -e "${RED}${BOLD}This script attempts to graft Linux Mint repositories/packages onto Ubuntu.${NC}"
- echo -e "${RED}${BOLD}It is NOT supported by Linux Mint, Canonical, your IT department, or your employer.${NC}"
- echo -e "${RED}${BOLD}It can break APT, boot/login, device management, VPN/EDR, and leave the system unrecoverable.${NC}"
- echo -e "${RED}${BOLD}${NC}"
- echo -e "${RED}${BOLD}Recommended approach: do a CLEAN Linux Mint install and restore your data/apps from backup.${NC}"
- echo -e "${RED}${BOLD}If this is a corporate-managed device: STOP and get approval first.${NC}"
- echo -e "${RED}${BOLD}======================================================================${NC}"
- echo
- echo "To continue anyway, type exactly:"
- echo " ${phrase}"
- echo "Anything else will abort."
- echo
-
- [[ -r /dev/tty ]] || die "No interactive TTY available (/dev/tty not readable). Refusing to proceed."
- local ans=""
- read -r -p "> " ans < /dev/tty || die "Unable to read confirmation from TTY."
- [[ "$ans" == "$phrase" ]] || die "Aborted."
- ok "Disclaimer acknowledged."
-}
-
-usage() {
- cat <<'EOF'
-Usage:
- sudo bash ubuntu-to-mint-convert-v3.sh doctor [--auto-fix]
- sudo bash ubuntu-to-mint-convert-v3.sh plan [--auto-fix] [--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:
- --i-accept-the-risk (required for convert)
- --edition cinnamon|mate|xfce (default: cinnamon)
- --target zena|zara|xia|wilma|virginia|victoria|vera|vanessa
- --mint-mirror URL (default: http://packages.linuxmint.com)
- --keep-ppas (do not disable 3rd-party repos; higher conflict risk)
- --allow-unhold (temporarily unhold packages during convert; risky)
- --preserve-snap (default: yes)
- --no-preserve-snap (disable snap-preservation behavior)
- --with-recommends (default: no; safer for corp systems)
- --overwrite-keyring (always overwrite Mint repo keyring file in-place)
- --recreate-keyring (move existing keyring aside and recreate from scratch)
- --auto-fix (doctor/plan only: allow dpkg/apt repairs and tool installs)
- --prefer-wayland (attempt to prefer a Wayland session if LightDM-compatible; otherwise warn and use X11)
- --yes (skip interactive confirmation inside convert; does NOT bypass disclaimer)
-EOF
-}
-
-# -------------------------
-# Defaults / arg parsing
-# -------------------------
-MODE="${1:-}"
-[[ -n "$MODE" ]] || { usage; exit 1; }
+# Runtime state
+SUBCMD="${1:-}"
shift || true
-ROLLBACK_DIR=""
-if [[ "$MODE" == "rollback" ]]; then
- ROLLBACK_DIR="${1:-}"
- [[ -n "$ROLLBACK_DIR" ]] || die "rollback requires a backup dir argument."
- shift || true
-fi
-
-EDITION="cinnamon"
+UBUNTU_BASE=""
+DEFAULT_MINT=""
+ALLOWED_TARGETS=""
TARGET_MINT=""
-MINT_MIRROR="http://packages.linuxmint.com"
+EDITION="cinnamon"
+MINT_MIRROR="$DEFAULT_MINT_MIRROR"
KEEP_PPAS="no"
-ALLOW_UNHOLD="no"
PRESERVE_SNAP="yes"
WITH_RECOMMENDS="no"
ASSUME_YES="no"
-ACCEPT_RISK="no"
-AUTO_FIX="no"
-KEYRING_MODE="auto" # auto|overwrite|recreate
-PREFER_WAYLAND="no"
+RISK_ACK_FLAG="no"
+PREFER_WAYLAND="no" # With LightDM we will force X11 anyway.
+OVERWRITE_KEYRING="no" # overwrite keyring file if exists
+RECREATE_KEYRING="no" # delete+recreate keyring if exists (back it up)
+AUTO_FIX="yes" # attempt basic dpkg/apt repair pre-flight
+PURGE_CONFLICTING_FLAVORS="yes" # purge ubuntucinnamon / flavor meta packages that break Mint DE
+MAX_ALLOWED_REMOVALS="$MAX_ALLOWED_REMOVALS_DEFAULT"
-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;;
- --allow-unhold) ALLOW_UNHOLD="yes"; shift;;
- --preserve-snap) PRESERVE_SNAP="yes"; shift;;
- --no-preserve-snap) PRESERVE_SNAP="no"; shift;;
- --with-recommends) WITH_RECOMMENDS="yes"; shift;;
- --overwrite-keyring) KEYRING_MODE="overwrite"; shift;;
- --recreate-keyring) KEYRING_MODE="recreate"; shift;;
- --auto-fix) AUTO_FIX="yes"; shift;;
- --prefer-wayland) PREFER_WAYLAND="yes"; shift;;
- --yes) ASSUME_YES="yes"; shift;;
- -h|--help) usage; exit 0;;
- *) die "Unknown arg: $1 (use --help)";;
- esac
-done
+BACKUP_DIR=""
+ON_ERROR_BACKUP_HINT=""
-validate_choice() {
- case "$1" in
- cinnamon|mate|xfce) ;;
- *) die "Invalid --edition '$1' (use cinnamon|mate|xfce)";;
- esac
+###############################################################################
+# Pretty output helpers
+###############################################################################
+is_tty() { [[ -t 1 ]]; }
+
+c_reset=""; c_red=""; c_grn=""; c_ylw=""; c_blu=""; c_bold=""
+if is_tty; then
+ c_reset="$(printf '\033[0m')"
+ c_red="$(printf '\033[31m')"
+ c_grn="$(printf '\033[32m')"
+ c_ylw="$(printf '\033[33m')"
+ c_blu="$(printf '\033[34m')"
+ c_bold="$(printf '\033[1m')"
+fi
+
+ts() { date -Is; }
+info() { echo "${c_blu}INFO:${c_reset} $*"; }
+ok() { echo "${c_grn}OK:${c_reset} $*"; }
+warn() { echo "${c_ylw}WARN:${c_reset} $*"; }
+err() { echo "${c_red}ERROR:${c_reset} $*"; }
+
+die() {
+ err "$*"
+ [[ -n "${ON_ERROR_BACKUP_HINT:-}" ]] && echo -e "\nRollback hint: ${ON_ERROR_BACKUP_HINT}"
+ exit 1
}
-validate_mirror() {
- [[ "$MINT_MIRROR" =~ ^https?:// ]] || die "--mint-mirror must start with http:// or https://"
+have_cmd() { command -v "$1" >/dev/null 2>&1; }
+
+###############################################################################
+# Logging + traps
+###############################################################################
+LOG_FILE=""
+setup_logging() {
+ mkdir -p "$LOG_DIR"
+ LOG_FILE="${LOG_DIR}/ubuntu-to-mint-$(date +%Y%m%d-%H%M%S).log"
+ # tee while preserving stderr
+ exec > >(tee -a "$LOG_FILE") 2>&1
+ info "Script v${SCRIPT_VERSION}"
+ info "Log: ${LOG_FILE}"
}
-read_os_release() {
- [[ -r /etc/os-release ]] || die "/etc/os-release missing"
+on_err() {
+ local exit_code=$?
+ local line_no=${BASH_LINENO[0]:-?}
+ local cmd=${BASH_COMMAND:-?}
+ err "FAILED at line ${line_no} (exit ${exit_code})."
+ err "Command: ${cmd}"
+ [[ -n "${LOG_FILE:-}" ]] && err "Log: ${LOG_FILE}"
+ [[ -n "${ON_ERROR_BACKUP_HINT:-}" ]] && echo -e "\nRollback hint: ${ON_ERROR_BACKUP_HINT}"
+ exit "$exit_code"
+}
+trap on_err ERR
+
+###############################################################################
+# Usage
+###############################################################################
+usage() {
+ cat < Mint desktop/tooling" graft.
+Ubuntu remains the base OS; Mint repos+packages are added with pinning.
+
+Usage:
+ sudo bash ${SCRIPT_NAME} doctor
+ sudo bash ${SCRIPT_NAME} plan [options]
+ sudo bash ${SCRIPT_NAME} convert --i-accept-the-risk [options]
+ sudo bash ${SCRIPT_NAME} rollback /root/ubuntu-to-mint-backup-YYYYMMDD-HHMMSS
+
+Options:
+ --edition cinnamon|mate|xfce (default: cinnamon)
+ --target Override Mint target codename (validated per Ubuntu base)
+ --mint-mirror (default: ${DEFAULT_MINT_MIRROR})
+
+ --keep-ppas Do NOT disable third-party APT sources (not recommended)
+ --preserve-snap / --no-preserve-snap (default: preserve snap)
+ --with-recommends Allow recommended packages during install (default: off)
+ --yes Skip most interactive prompts (convert still requires disclaimer + --i-accept-the-risk)
+ --max-removals N Abort if APT simulation removes more than N packages (default: ${MAX_ALLOWED_REMOVALS_DEFAULT})
+
+ --overwrite-keyring If ${KEYRING_OUT} exists, overwrite it
+ --recreate-keyring Backup+delete ${KEYRING_OUT} then recreate it
+
+ --no-auto-fix Do not attempt dpkg/apt repair pre-flight
+ --no-purge-flavor Do not purge conflicting Ubuntu-flavor packages (ubuntucinnamon*, etc.)
+
+Notes:
+ * "convert" will force LightDM + X11 session by default (Mint-style). Wayland is not used by LightDM.
+EOF
+}
+
+###############################################################################
+# Argument parsing
+###############################################################################
+parse_args() {
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --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 ;;
+ --no-preserve-snap)
+ PRESERVE_SNAP="no"; shift ;;
+ --with-recommends)
+ WITH_RECOMMENDS="yes"; shift ;;
+ --yes)
+ ASSUME_YES="yes"; shift ;;
+ --i-accept-the-risk)
+ RISK_ACK_FLAG="yes"; shift ;;
+ --overwrite-keyring)
+ OVERWRITE_KEYRING="yes"; shift ;;
+ --recreate-keyring)
+ RECREATE_KEYRING="yes"; shift ;;
+ --no-auto-fix)
+ AUTO_FIX="no"; shift ;;
+ --no-purge-flavor)
+ PURGE_CONFLICTING_FLAVORS="no"; shift ;;
+ --max-removals)
+ MAX_ALLOWED_REMOVALS="${2:-}"; shift 2 ;;
+ -h|--help|help)
+ usage; exit 0 ;;
+ *)
+ die "Unknown argument: $1 (use --help)" ;;
+ esac
+ done
+
+ case "$EDITION" in
+ cinnamon|mate|xfce) : ;;
+ *) die "--edition must be cinnamon|mate|xfce" ;;
+ esac
+
+ [[ "$MAX_ALLOWED_REMOVALS" =~ ^[0-9]+$ ]] || die "--max-removals must be an integer"
+}
+
+###############################################################################
+# System checks
+###############################################################################
+require_root() {
+ [[ "${EUID}" -eq 0 ]] || die "Run as root (use sudo)."
+}
+
+detect_os() {
+ [[ -r /etc/os-release ]] || die "Missing /etc/os-release"
# shellcheck disable=SC1091
. /etc/os-release
- OS_ID="${ID:-}"
- OS_NAME="${NAME:-}"
- OS_VERSION_CODENAME="${VERSION_CODENAME:-}"
- OS_VERSION_ID="${VERSION_ID:-}"
-}
-have_regular_file_or_die() {
- local p="$1"
- if [[ -e "$p" && ! -f "$p" ]]; then
- die "Expected regular file at '$p' but found non-regular (dir/symlink/device). Fix it before proceeding."
- fi
-}
+ local id="${ID:-}"
+ local ver="${VERSION_ID:-}"
+ local codename="${VERSION_CODENAME:-${UBUNTU_CODENAME:-}}"
-detect_ubuntu_codename() {
- local c=""
- if have_cmd lsb_release; then c="$(lsb_release -cs 2>/dev/null || true)"; fi
- [[ -n "$c" ]] || c="${OS_VERSION_CODENAME:-}"
- [[ -n "$c" ]] || die "Could not determine Ubuntu codename."
- echo "$c"
-}
+ info "Detected OS: ${NAME:-unknown} (ID=${id}, VERSION_ID=${ver}, CODENAME=${codename})"
-set_targets_from_ubuntu() {
- UBUNTU_CODENAME="$(detect_ubuntu_codename)"
- local -a ALLOWED_TARGETS=()
- case "$UBUNTU_CODENAME" in
+ [[ "$id" == "ubuntu" ]] || die "This script only supports Ubuntu as the base (ID=ubuntu)."
+
+ case "$codename" in
noble)
UBUNTU_BASE="noble"
DEFAULT_MINT="zena"
- ALLOWED_TARGETS=(zena zara xia wilma)
+ ALLOWED_TARGETS="zena zara xia wilma"
;;
jammy)
UBUNTU_BASE="jammy"
DEFAULT_MINT="virginia"
- ALLOWED_TARGETS=(virginia victoria vera vanessa)
+ ALLOWED_TARGETS="virginia victoria vera vanessa"
;;
*)
- die "Unsupported Ubuntu codename '$UBUNTU_CODENAME'. Supports Ubuntu noble (24.04) or jammy (22.04) only."
+ die "Unsupported Ubuntu codename '${codename}'. Supported: noble (24.04), jammy (22.04)."
;;
esac
- [[ -n "$TARGET_MINT" ]] || TARGET_MINT="$DEFAULT_MINT"
+ if [[ -z "$TARGET_MINT" ]]; then
+ TARGET_MINT="$DEFAULT_MINT"
+ fi
local ok_target="no"
- for t in "${ALLOWED_TARGETS[@]}"; do
- [[ "$TARGET_MINT" == "$t" ]] && ok_target="yes" && break
+ for t in $ALLOWED_TARGETS; do
+ [[ "$TARGET_MINT" == "$t" ]] && ok_target="yes"
done
- if [[ "$ok_target" != "yes" ]]; then
- local allowed_str
- printf -v allowed_str '%s ' "${ALLOWED_TARGETS[@]}"
- allowed_str="${allowed_str% }"
- die "--target '$TARGET_MINT' not allowed for Ubuntu '$UBUNTU_CODENAME'. Allowed: $allowed_str"
- fi
+ [[ "$ok_target" == "yes" ]] || die "--target '${TARGET_MINT}' not allowed for Ubuntu '${UBUNTU_BASE}'. Allowed: ${ALLOWED_TARGETS}"
+
+ info "Ubuntu base: ${UBUNTU_BASE} | Target Mint codename: ${TARGET_MINT} | Edition: ${EDITION}"
}
-detect_ubuntu_mirrors() {
- local sources_txt=""
- 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
- sources_txt+="$(grep -RhsE '^[[:space:]]*URIs:[[:space:]]*' /etc/apt/sources.list.d/*.sources | awk '{print $2}' || true)"$'\n'
- fi
-
- UBUNTU_ARCHIVE_MIRROR="http://archive.ubuntu.com/ubuntu"
- UBUNTU_SECURITY_MIRROR="http://security.ubuntu.com/ubuntu"
-
- local m
- m="$(echo "$sources_txt" | grep -Eo 'https?://[^ ]+/ubuntu' | head -n1 || true)"
- [[ -n "$m" ]] && UBUNTU_ARCHIVE_MIRROR="$m"
-
- info "Ubuntu archive mirror: ${UBUNTU_ARCHIVE_MIRROR}"
- info "Ubuntu security mirror: ${UBUNTU_SECURITY_MIRROR}"
-}
-
-apt_fix_broken_overwrite() {
- DEBIAN_FRONTEND=noninteractive apt-get -y -f install \
- -o Dpkg::Options::=--force-overwrite \
- -o Dpkg::Options::=--force-confdef \
- -o Dpkg::Options::=--force-confold || true
- DEBIAN_FRONTEND=noninteractive dpkg --configure -a || true
- DEBIAN_FRONTEND=noninteractive apt-get -y -f install \
- -o Dpkg::Options::=--force-overwrite \
- -o Dpkg::Options::=--force-confdef \
- -o Dpkg::Options::=--force-confold || true
-}
-
-ensure_tools() {
- # ensure_tools ...
- local allow="$1"; shift
- local missing=()
- local t
- for t in "$@"; do
- have_cmd "$t" || missing+=("$t")
- done
- if [[ ${#missing[@]} -eq 0 ]]; then
- return 0
- fi
-
- if [[ "$allow" == "yes" ]]; then
- info "Installing required tools: ${missing[*]}"
- DEBIAN_FRONTEND=noninteractive apt-get update -o Acquire::Retries=3
- DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates curl gnupg dirmngr
- for t in "$@"; do
- have_cmd "$t" || die "Tool install failed; still missing: $t"
- done
- return 0
- fi
-
- die "Missing required tools (${missing[*]}). Install them first or re-run with --auto-fix."
-}
-
-preflight_common() {
- # preflight_common
- local allow_changes="$1"
-
- need_root
- validate_choice "$EDITION"
- validate_mirror
- 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} | Prefer Wayland: ${PREFER_WAYLAND}"
-
- for l in /var/lib/dpkg/lock /var/lib/dpkg/lock-frontend /var/lib/apt/lists/lock /var/cache/apt/archives/lock; do
+check_apt_locks() {
+ local locks=(
+ "/var/lib/dpkg/lock"
+ "/var/lib/dpkg/lock-frontend"
+ "/var/lib/apt/lists/lock"
+ "/var/cache/apt/archives/lock"
+ )
+ for l in "${locks[@]}"; do
if [[ -e "$l" ]] && fuser "$l" >/dev/null 2>&1; then
- die "APT/DPKG lock is held ($l). Close updaters and try again."
+ die "APT/dpkg lock active on ${l}. Close Software Updater/apt/dpkg and retry."
fi
done
+}
+
+apt_fix_basic() {
+ info "Attempting basic dpkg/apt repair (best-effort)..."
+ export DEBIAN_FRONTEND=noninteractive
+ dpkg --configure -a || true
+ apt-get -y -f install || true
+ apt-get -y --fix-broken install || true
+ apt-get -y update || true
+ ok "Basic repair attempt complete."
+}
+
+doctor_report() {
+ info "Doctor checks:"
+ check_apt_locks
+
+ local holds
+ holds="$(apt-mark showhold 2>/dev/null || true)"
+ if [[ -n "$holds" ]]; then
+ warn "Held packages detected:"
+ echo "$holds"
+ else
+ ok "No held packages detected."
+ fi
if dpkg --audit | grep -q .; then
- if [[ "$allow_changes" == "yes" ]]; then
- warn "dpkg reports issues. Attempting to fix..."
- DEBIAN_FRONTEND=noninteractive dpkg --configure -a || true
- apt_fix_broken_overwrite
- else
- warn "dpkg reports issues (doctor/plan won't modify). Run: sudo dpkg --configure -a && sudo apt-get -f install"
- fi
- fi
-
- if ! apt-get check >/dev/null 2>&1; then
- if [[ "$allow_changes" == "yes" ]]; then
- warn "apt-get check failed. Attempting best-effort repair..."
- apt_fix_broken_overwrite
- apt-get check >/dev/null 2>&1 || warn "apt-get check still failing; conversion may fail."
- else
- warn "apt-get check failed (doctor/plan won't modify). Fix before converting."
- fi
- fi
-
- local root_free
- root_free="$(df -Pm / | awk 'NR==2{print $4}')"
- [[ "${root_free:-0}" -ge 6144 ]] || warn "Low free space on / (${root_free} MB). Recommend >= 6GB free."
-
- if [[ "$MODE" == "plan" || "$MODE" == "convert" ]]; then
- ensure_tools "$allow_changes" curl gpg
+ warn "dpkg reports issues:"
+ dpkg --audit || true
else
- have_cmd curl || warn "curl not found (recommended)."
- have_cmd gpg || warn "gpg not found (recommended)."
+ ok "dpkg --audit clean."
fi
- if have_cmd curl; then
- curl -fsS --connect-timeout 10 --max-time 30 "${MINT_MIRROR%/}/" >/dev/null || warn "Cannot reach Mint mirror ${MINT_MIRROR} (may be blocked/proxy)."
- fi
-
- ok "Preflight completed."
-}
-
-# -------------------------
-# Keyring helpers
-# -------------------------
-keyring_contains_keyid() {
- local keyring="$1"
- local keyid="${2^^}"
-
- [[ -s "$keyring" ]] || return 1
- have_regular_file_or_die "$keyring"
- have_cmd gpg || return 1
-
- local gh
- gh="$(mktemp -d)"
- chmod 700 "$gh"
-
- local found="no"
- if gpg --homedir "$gh" --batch --no-default-keyring --keyring "$keyring" --with-colons --list-keys 2>/dev/null \
- | awk -F: '$1=="pub"||$1=="sub"{print toupper($5)}' \
- | grep -q "${keyid}"; then
- found="yes"
- fi
-
- rm -rf "$gh"
- [[ "$found" == "yes" ]]
-}
-
-get_key_fingerprint_from_keyring() {
- local keyring="$1"
- have_cmd gpg || return 1
- [[ -s "$keyring" ]] || return 1
-
- local gh
- gh="$(mktemp -d)"
- chmod 700 "$gh"
- local fpr
- fpr="$(gpg --homedir "$gh" --batch --no-default-keyring --keyring "$keyring" --with-colons --list-keys 2>/dev/null \
- | awk -F: '$1=="fpr"{print toupper($10); exit}' || true)"
- rm -rf "$gh"
- [[ -n "$fpr" ]] || return 1
- echo "$fpr"
-}
-
-backup_existing_keyring() {
- local keyring="$1"
- local bdir="${LOG_DIR}/keyring-backups"
- mkdir -p "$bdir"
- local ts
- ts="$(date +%Y%m%d-%H%M%S)"
- local dest="${bdir}/$(basename "$keyring").${ts}.bak"
- cp -a "$keyring" "$dest"
- ok "Backed up existing keyring to: $dest"
-}
-
-# -------------------------
-# Key handling (HKPS -> HKP:80 -> HTTPS fallback) + atomic write (same dir)
-# FIXED: do not dearmor to an already-existing file; validate OpenPGP data
-# -------------------------
-mint_repo_key_write_to() {
- # mint_repo_key_write_to
- local out_keyring="$1"
- local allow_changes="${2:-no}"
- local keyid="A6616109451BBBF2"
-
- [[ -n "$out_keyring" ]] || die "mint_repo_key_write_to requires an output path"
- ensure_tools "$allow_changes" curl gpg
-
- local out_dir
- out_dir="$(dirname "$out_keyring")"
- mkdir -p "$out_dir"
-
- # Temp dir on same filesystem for atomic mv; output file does NOT exist yet.
- local tmp_dir tmp_out
- tmp_dir="$(mktemp -d -p "$out_dir" ".linuxmint-repo.gpg.tmp.XXXXXX")"
- tmp_out="${tmp_dir}/linuxmint-repo.gpg"
-
- local gnupghome
- gnupghome="$(mktemp -d)"
- chmod 700 "$gnupghome"
-
- local -a ks_opts=()
- if [[ -n "${http_proxy:-}" ]]; then
- ks_opts+=(--keyserver-options "http-proxy=${http_proxy}")
- elif [[ -n "${https_proxy:-}" ]]; then
- ks_opts+=(--keyserver-options "http-proxy=${https_proxy}")
- fi
-
- local got="no"
- if gpg --homedir "$gnupghome" --batch "${ks_opts[@]}" --keyserver hkps://keyserver.ubuntu.com --recv-keys "$keyid" >/dev/null 2>&1; then
- got="yes"
- elif gpg --homedir "$gnupghome" --batch "${ks_opts[@]}" --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys "$keyid" >/dev/null 2>&1; then
- got="yes"
- fi
-
- if [[ "$got" == "yes" ]]; then
- gpg --homedir "$gnupghome" --batch --export "$keyid" \
- | gpg --batch --yes --dearmor -o "$tmp_out" \
- || { rm -rf "$gnupghome" "$tmp_dir"; die "Failed to export+dearmor Mint repo key from keyserver."; }
+ if apt-get -s check >/dev/null 2>&1; then
+ ok "apt-get check: OK"
else
- info "Keyserver blocked; fetching key over HTTPS from Ubuntu keyserver (exact match)..."
- local armored="$gnupghome/linuxmint-repo.asc"
+ warn "apt-get check reports problems."
+ fi
- if ! curl -fsSL --connect-timeout 10 --max-time 30 \
- -H "Accept: application/pgp-keys" \
- "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x${keyid}&exact=on" -o "$armored"; then
- curl -fsSL --connect-timeout 10 --max-time 30 \
- -H "Accept: application/pgp-keys" \
- "http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x${keyid}&exact=on" -o "$armored" \
- || { rm -rf "$gnupghome" "$tmp_dir"; die "Unable to fetch Mint repo key via keyserver or HTTPS fallback."; }
+ ok "Doctor complete."
+}
+
+###############################################################################
+# Backup / rollback
+###############################################################################
+backup_system_state() {
+ local dir="/root/ubuntu-to-mint-backup-$(date +%Y%m%d-%H%M%S)"
+ mkdir -p "$dir"
+ info "Creating backup at: ${dir}"
+
+ mkdir -p "${dir}/etc"
+ cp -a /etc/apt "${dir}/etc/" || true
+ cp -a /etc/os-release /etc/lsb-release 2>/dev/null "${dir}/etc/" || true
+ cp -a /etc/fstab /etc/hostname /etc/hosts 2>/dev/null "${dir}/etc/" || true
+
+ dpkg-query -W -f='${Package}\t${Version}\n' > "${dir}/dpkg-packages.tsv" || true
+ apt-mark showmanual > "${dir}/apt-manual.txt" || true
+ apt-mark showhold > "${dir}/apt-holds.txt" || true
+ systemctl list-unit-files --state=enabled > "${dir}/enabled-services.txt" || true
+
+ if have_cmd snap; then snap list > "${dir}/snap-list.txt" || true; fi
+ if have_cmd flatpak; then flatpak list > "${dir}/flatpak-list.txt" || true; fi
+
+ ok "Backup complete."
+ echo "$dir"
+}
+
+rollback_from_backup() {
+ local dir="${1:-}"
+ [[ -n "$dir" ]] || die "rollback requires a backup directory path."
+ [[ -d "$dir" ]] || die "Backup directory not found: $dir"
+ [[ -d "${dir}/etc/apt" ]] || die "Backup missing ${dir}/etc/apt"
+
+ info "Restoring /etc/apt from backup: ${dir}"
+ rm -rf /etc/apt
+ cp -a "${dir}/etc/apt" /etc/apt
+ ok "APT config restored."
+
+ info "Running apt-get update + fix-broken (best-effort)..."
+ export DEBIAN_FRONTEND=noninteractive
+ apt-get -y update || true
+ apt-get -y -f install || true
+ ok "Rollback complete. Reboot recommended."
+}
+
+timeshift_snapshot_best_effort() {
+ if have_cmd timeshift; then
+ info "Timeshift detected. Attempting pre-change snapshot (best-effort)..."
+ timeshift --create --comments "pre ubuntu->mint ${EDITION} $(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
+}
+
+###############################################################################
+# Third-party sources handling
+###############################################################################
+is_enterprise_allowlisted_source() {
+ # Avoid disabling common corporate agents repos
+ # (CrowdStrike, Palo Alto GlobalProtect, etc.)
+ local f="$1"
+ grep -Eqi '(crowdstrike|falcon|paloaltonetworks|globalprotect|pan-gp|pan-globalprotect|zscaler|netskope|sentinelone)' "$f" 2>/dev/null
+}
+
+disable_thirdparty_sources_system() {
+ local backup_dir="$1"
+ local disabled_dir="${backup_dir}/disabled-sources"
+ mkdir -p "$disabled_dir"
+
+ info "Disabling 3rd-party sources into: ${disabled_dir}"
+ shopt -s nullglob
+ for f in /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; do
+ [[ "$(basename "$f")" == "$(basename "$SOURCES_OUT")" ]] && continue
+ if [[ "$KEEP_PPAS" == "yes" ]]; then
+ continue
fi
-
- grep -q "BEGIN PGP PUBLIC KEY BLOCK" "$armored" \
- || { rm -rf "$gnupghome" "$tmp_dir"; die "Downloaded content is not a PGP public key block (proxy portal/HTML?)."; }
-
- local keyinfo
- if ! keyinfo="$(gpg --batch --with-colons --show-keys "$armored" 2>/dev/null)"; then
- rm -rf "$gnupghome" "$tmp_dir"
- die "Downloaded key block is not valid OpenPGP data (gpg cannot parse it)."
+ if is_enterprise_allowlisted_source "$f"; then
+ warn "Keeping allowlisted corporate source: $f"
+ continue
fi
+ mv -v "$f" "${disabled_dir}/" || true
+ done
+ shopt -u nullglob
- if ! awk -F: '$1=="pub"||$1=="sub"{print toupper($5)}' <<<"$keyinfo" | grep -q "${keyid^^}"; then
- rm -rf "$gnupghome" "$tmp_dir"
- die "Fetched key does not contain expected keyid ${keyid^^}"
- fi
+ ok "Third-party sources disabled (restorable via rollback)."
+}
- gpg --batch --yes --dearmor -o "$tmp_out" "$armored" \
- || { rm -rf "$gnupghome" "$tmp_dir"; die "Failed to dearmor downloaded key block."; }
+###############################################################################
+# Mint keyring retrieval (NO keyserver dependency by default)
+###############################################################################
+ensure_deps_for_keyring() {
+ export DEBIAN_FRONTEND=noninteractive
+ apt-get -y update
+ apt-get -y install ca-certificates curl gnupg dirmngr dpkg-dev >/dev/null
+}
+
+keyring_from_installed_package() {
+ # If linuxmint-keyring is installed, copy the keyring file(s) from it.
+ if ! dpkg -s linuxmint-keyring >/dev/null 2>&1; then
+ return 1
+ fi
+
+ local files
+ files="$(dpkg -L linuxmint-keyring 2>/dev/null | grep -E '/usr/share/keyrings/.*\.(gpg|asc)$' || true)"
+ [[ -n "$files" ]] || return 1
+
+ local src
+ src="$(echo "$files" | grep -E '\.gpg$' | head -n 1 || true)"
+ if [[ -z "$src" ]]; then
+ src="$(echo "$files" | head -n 1)"
+ fi
+ [[ -r "$src" ]] || return 1
+
+ echo "$src"
+ return 0
+}
+
+fetch_latest_linuxmint_keyring_deb() {
+ local pool_base="${MINT_MIRROR%/}/pool/main/l/linuxmint-keyring/"
+ local tmpdir="$1"
+ mkdir -p "$tmpdir"
+
+ # Try HTTPS first, then HTTP.
+ local index=""
+ if index="$(curl -fsSL "https://$(echo "$pool_base" | sed 's|^http://||')" 2>/dev/null)"; then
+ :
+ elif index="$(curl -fsSL "$pool_base" 2>/dev/null)"; then
+ :
+ else
+ return 1
+ fi
+
+ # Extract deb names
+ local debs
+ debs="$(echo "$index" | grep -oE 'linuxmint-keyring_[0-9A-Za-z.+:~_-]+_all\.deb' | sort -Vu | uniq || true)"
+ [[ -n "$debs" ]] || return 1
+
+ local deb
+ deb="$(echo "$debs" | tail -n 1)"
+ local url="${pool_base}${deb}"
+
+ info "Downloading linuxmint-keyring package: ${deb}"
+ if ! curl -fsSL "$url" -o "${tmpdir}/${deb}"; then
+ # attempt https variant if pool_base was http
+ local https_url="https://$(echo "$url" | sed 's|^http://||')"
+ curl -fsSL "$https_url" -o "${tmpdir}/${deb}" || return 1
+ fi
+
+ echo "${tmpdir}/${deb}"
+ return 0
+}
+
+extract_keyring_from_deb_to() {
+ local deb_path="$1"
+ local out_keyring="$2"
+
+ [[ -r "$deb_path" ]] || die "Keyring deb not readable: $deb_path"
+ [[ -n "$out_keyring" ]] || die "extract_keyring_from_deb_to requires output path"
+
+ local tmpdir
+ tmpdir="$(mktemp -d)"
+ chmod 700 "$tmpdir"
+
+ dpkg-deb -x "$deb_path" "$tmpdir"
+
+ local candidate=""
+ if [[ -d "$tmpdir/usr/share/keyrings" ]]; then
+ candidate="$(find "$tmpdir/usr/share/keyrings" -maxdepth 1 -type f -name '*.gpg' | head -n 1 || true)"
+ fi
+
+ if [[ -z "$candidate" ]]; then
+ # maybe shipped as .asc
+ candidate="$(find "$tmpdir/usr/share/keyrings" -maxdepth 1 -type f -name '*.asc' | head -n 1 || true)"
+ fi
+
+ [[ -n "$candidate" ]] || die "Could not find Mint keyring inside linuxmint-keyring deb."
+
+ mkdir -p "$(dirname "$out_keyring")"
+
+ local tmp_out
+ tmp_out="$(mktemp)"
+ rm -f "$tmp_out" 2>/dev/null || true
+
+ if [[ "$candidate" == *.gpg ]]; then
+ cp -f "$candidate" "$tmp_out"
+ else
+ # .asc -> dearmor
+ grep -q "BEGIN PGP PUBLIC KEY BLOCK" "$candidate" \
+ || die "Extracted key file does not look like an armored PGP public key."
+ gpg --batch --dearmor -o "$tmp_out" "$candidate"
fi
chmod 644 "$tmp_out"
mv -f "$tmp_out" "$out_keyring"
chmod 644 "$out_keyring"
- rm -rf "$gnupghome" "$tmp_dir"
- local fpr
- fpr="$(get_key_fingerprint_from_keyring "$out_keyring" || true)"
- [[ -n "$fpr" ]] || die "Unable to read fingerprint from written keyring: $out_keyring"
- info "Mint repo key fingerprint installed: $fpr"
-
- if [[ -n "$MINT_KEY_FPR_EXPECT" ]]; then
- [[ "${fpr^^}" == "${MINT_KEY_FPR_EXPECT^^}" ]] || die "Mint key fingerprint mismatch. Expected ${MINT_KEY_FPR_EXPECT^^}, got ${fpr^^}"
- fi
+ rm -rf "$tmpdir"
}
-mint_repo_key_install() {
- local keyring="/usr/share/keyrings/linuxmint-repo.gpg"
- local keyid="A6616109451BBBF2"
+mint_repo_key_write_to() {
+ local out_keyring="$1"
+ [[ -n "$out_keyring" ]] || die "mint_repo_key_write_to requires an output path"
- have_regular_file_or_die "$keyring"
+ ensure_deps_for_keyring
- if [[ -e "$keyring" ]]; then
- case "$KEYRING_MODE" in
- recreate)
- warn "Mint keyring already exists at ${keyring}; recreating as requested."
- [[ -s "$keyring" ]] && backup_existing_keyring "$keyring" || true
- rm -f "$keyring"
- ;;
- overwrite)
- warn "Mint keyring already exists at ${keyring}; overwriting in-place as requested."
- ;;
- auto)
- if [[ -s "$keyring" ]] && keyring_contains_keyid "$keyring" "$keyid"; then
- ok "Mint keyring already present and contains expected key (${keyid})."
- return 0
- fi
- warn "Mint keyring exists but is missing/invalid or does not contain expected key (${keyid}); recreating safely."
- [[ -s "$keyring" ]] && backup_existing_keyring "$keyring" || true
- rm -f "$keyring"
- ;;
- *)
- die "Internal error: unknown KEYRING_MODE=${KEYRING_MODE}"
- ;;
- esac
+ # Handle existing file behavior
+ if [[ -e "$out_keyring" ]]; then
+ if [[ "$RECREATE_KEYRING" == "yes" ]]; then
+ local bak="${out_keyring}.$(date +%Y%m%d-%H%M%S).bak"
+ warn "Recreating keyring: backing up existing to ${bak}"
+ cp -a "$out_keyring" "$bak" || true
+ rm -f "$out_keyring"
+ elif [[ "$OVERWRITE_KEYRING" == "yes" ]]; then
+ warn "Overwriting existing keyring at ${out_keyring}"
+ :
+ else
+ # Validate it looks like a keyring; if yes, keep it.
+ if gpg --batch --quiet --show-keys "$out_keyring" >/dev/null 2>&1; then
+ ok "Existing Mint keyring looks valid; keeping ${out_keyring} (use --overwrite-keyring or --recreate-keyring to replace)."
+ return 0
+ fi
+ die "Existing keyring at ${out_keyring} is not readable by gpg. Use --recreate-keyring to rebuild it."
+ fi
fi
- info "Installing Linux Mint repo signing key into ${keyring}"
- mint_repo_key_write_to "$keyring" "yes"
- [[ -s "$keyring" ]] || die "Mint keyring did not get created at ${keyring}"
-
- if ! keyring_contains_keyid "$keyring" "$keyid"; then
- backup_existing_keyring "$keyring" || true
- die "Mint keyring created but does not contain expected key (${keyid})."
+ # Preferred: use linuxmint-keyring (installed) OR download its .deb and extract keyring.
+ local src=""
+ if src="$(keyring_from_installed_package 2>/dev/null)"; then
+ info "Using keyring from installed linuxmint-keyring: ${src}"
+ mkdir -p "$(dirname "$out_keyring")"
+ cp -f "$src" "$out_keyring"
+ chmod 644 "$out_keyring"
+ ok "Key installed."
+ return 0
fi
- ok "Key installed."
+ local tmpdir
+ tmpdir="$(mktemp -d)"
+ chmod 700 "$tmpdir"
+
+ local deb_path=""
+ if deb_path="$(fetch_latest_linuxmint_keyring_deb "$tmpdir")"; then
+ extract_keyring_from_deb_to "$deb_path" "$out_keyring"
+ rm -rf "$tmpdir"
+ ok "Key installed."
+ return 0
+ fi
+
+ rm -rf "$tmpdir"
+ die "Unable to obtain Mint keyring package from ${MINT_MIRROR}. Check proxy/firewall connectivity."
}
-# -------------------------
-# APT sources/pinning
-# -------------------------
-disable_ubuntu_sources_if_present() {
- if [[ -f /etc/apt/sources.list.d/ubuntu.sources ]]; then
- local new="/etc/apt/sources.list.d/ubuntu.sources.disabled.ubuntu-to-mint"
- info "Disabling existing ubuntu.sources to avoid duplicate entries: ${new}"
- mv -f /etc/apt/sources.list.d/ubuntu.sources "$new" || true
+mint_repo_key_install_system() {
+ info "Installing Linux Mint repo signing key into ${KEYRING_OUT}"
+ mint_repo_key_write_to "$KEYRING_OUT"
+}
+
+###############################################################################
+# Sources + pinning
+###############################################################################
+detect_ubuntu_mirrors() {
+ # Keep it simple and stable; prefer existing mirrors if they look sane.
+ UBUNTU_ARCHIVE_MIRROR="http://archive.ubuntu.com/ubuntu"
+ UBUNTU_SECURITY_MIRROR="http://security.ubuntu.com/ubuntu"
+
+ # If system already uses a mirror, re-use it.
+ local any
+ any="$(grep -RhoE '^deb\s+http[^ ]+\s+' /etc/apt/sources.list /etc/apt/sources.list.d/*.list 2>/dev/null | head -n 1 || true)"
+ if [[ -n "$any" ]]; then
+ local m
+ m="$(echo "$any" | awk '{print $2}')"
+ if [[ "$m" == http*ubuntu.com/ubuntu* || "$m" == http* ]]; then
+ UBUNTU_ARCHIVE_MIRROR="$m"
+ fi
fi
+ info "Ubuntu archive mirror: ${UBUNTU_ARCHIVE_MIRROR}"
+ info "Ubuntu security mirror: ${UBUNTU_SECURITY_MIRROR}"
}
write_mint_sources_system() {
- local list="/etc/apt/sources.list.d/official-package-repositories.list"
- local keyring="/usr/share/keyrings/linuxmint-repo.gpg"
-
- [[ -s "$keyring" ]] || die "Missing keyring ${keyring}. Run key install first."
-
detect_ubuntu_mirrors
- disable_ubuntu_sources_if_present
+ info "Writing Mint+Ubuntu sources to ${SOURCES_OUT}"
- info "Writing Mint+Ubuntu sources to ${list}"
- cat > "$list" < "$SOURCES_OUT" < "$pref" <<'EOF'
+ info "Writing conservative APT pinning to ${PIN_OUT}"
+ cat > "$PIN_OUT" <<'EOF'
+# Keep Ubuntu as the default source for overlapping packages.
Package: *
-Pin: release o=LinuxMint
+Pin: origin "packages.linuxmint.com"
Pin-Priority: 100
-Package: mint* mintsources* mintupdate* mintsystem* mintstick* mintmenu* mintlocale* mintdrivers* mintreport* mintwelcome*
-Pin: release o=LinuxMint
-Pin-Priority: 700
-
-Package: mint* mintsources* mintupdate* mintsystem* mintstick* mintmenu* mintlocale* mintdrivers* mintreport* mintwelcome*
+# Prefer Mint tooling / meta packages.
+Package: mint* mintsources* mintupdate* mintsystem* mintstick* mintmenu* mintlocale* mintdrivers* mintreport* mintwelcome* mintinstall*
Pin: origin "packages.linuxmint.com"
Pin-Priority: 700
+EOF
+ ok "Base pinning written."
-Package: cinnamon* nemo* muffin* cjs* slick-greeter* lightdm* pix* xviewer* mint-themes* mint-y-icons* mint-x-icons*
-Pin: release o=LinuxMint
-Pin-Priority: 900
-
-Package: cinnamon* nemo* muffin* cjs* slick-greeter* lightdm* pix* xviewer* mint-themes* mint-y-icons* mint-x-icons*
-Pin: origin "packages.linuxmint.com"
-Pin-Priority: 900
-
-Package: python3-xapp* python-xapp* libxapp* gir1.2-xapp* xapps* xapps-common xapp-symbolic-icons xapp-status-icon
-Pin: release o=LinuxMint
-Pin-Priority: 1001
-
-Package: python3-xapp* python-xapp* libxapp* gir1.2-xapp* xapps* xapps-common xapp-symbolic-icons xapp-status-icon
-Pin: origin "packages.linuxmint.com"
-Pin-Priority: 1001
-
-Package: libnemo-extension1* nemo-data*
-Pin: release o=LinuxMint
-Pin-Priority: 1001
-
-Package: libnemo-extension1* nemo-data*
-Pin: origin "packages.linuxmint.com"
-Pin-Priority: 1001
-
-Package: libcinnamon-control-center1* libcinnamon-menu-3-0* libcinnamon-desktop4* cinnamon-desktop-data* cinnamon-control-center-data* cinnamon-l10n*
-Pin: release o=LinuxMint
-Pin-Priority: 1001
-
-Package: libcinnamon-control-center1* libcinnamon-menu-3-0* libcinnamon-desktop4* cinnamon-desktop-data* cinnamon-control-center-data* cinnamon-l10n*
+ info "Writing high-priority desktop stack pinning to ${PIN_STACK_OUT}"
+ cat > "$PIN_STACK_OUT" <<'EOF'
+# Desktop stacks MUST come from Mint repo to avoid Ubuntu/Mint version skew.
+# (This prevents the "not installable" / mixed-version dependency issues.)
+Package: cinnamon* nemo* muffin* cjs* libcjs* libmuffin* libnemo-extension* nemo-* xapp* xapps-* slick-greeter* lightdm* mint-themes* mint-y-icons* mint-x-icons* pix* xviewer* cinnamon-settings-daemon* cinnamon-control-center* cinnamon-session* cinnamon-desktop-data* cinnamon-l10n* libcinnamon* gir1.2-cmenu-3.0 libcinnamon-menu-3-0*
Pin: origin "packages.linuxmint.com"
Pin-Priority: 1001
EOF
-
- ok "Pinning written."
+ ok "Desktop stack pinning written."
}
-# -------------------------
-# 3rd-party repo handling (preserve Falcon/GlobalProtect/etc)
-# -------------------------
-disable_thirdparty_sources_system() {
- local backup_dir="$1"
- local disabled_dir="${backup_dir}/disabled-sources"
- mkdir -p "$disabled_dir"
-
- info "Disabling 3rd-party sources into: ${disabled_dir}"
- shopt -s nullglob
-
- local allow_re='(crowdstrike|falcon|globalprotect|paloalto|pan(gp)?|cortex|prisma)'
-
- for f in /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; do
- [[ "$(basename "$f")" == "official-package-repositories.list" ]] && continue
- if echo "$(basename "$f")" | grep -Eiq "$allow_re" || grep -Eiq "$allow_re" "$f"; then
- info "Preserving vendor repo: $f"
- continue
- fi
- mv -v "$f" "${disabled_dir}/" || true
- done
-
- shopt -u nullglob
- ls -1 "${disabled_dir}" 2>/dev/null | sed 's/^/ disabled: /' | tee -a "${backup_dir}/disabled-sources.txt" >/dev/null || true
- ok "Third-party sources disabled (restorable via rollback)."
-}
-
-# -------------------------
-# dpkg-divert for known conflicts
-# -------------------------
-apply_known_diversions() {
- local f="/usr/share/icons/hicolor/16x16/apps/software-properties.png"
- if dpkg-query -W -f='${Status}' software-properties-gtk 2>/dev/null | grep -q "installed"; then
- if dpkg -S "$f" 2>/dev/null | grep -q "^software-properties-gtk:"; then
- info "Applying dpkg-divert for known conflict: $f"
- dpkg-divert --package ubuntu-to-mint-convert --rename --add "$f" || true
- fi
+remove_nosnap_pref_if_needed() {
+ if [[ "$PRESERVE_SNAP" == "yes" ]] && [[ -f /etc/apt/preferences.d/nosnap.pref ]]; then
+ warn "Mint 'nosnap' preference detected at /etc/apt/preferences.d/nosnap.pref. Removing to preserve snap functionality."
+ rm -f /etc/apt/preferences.d/nosnap.pref || true
fi
}
-# -------------------------
-# Backup / snapshot
-# -------------------------
-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
-
- mkdir -p "${backup_dir}/etc/X11" "${backup_dir}/etc/lightdm"
- cp -a /etc/X11/default-display-manager 2>/dev/null "${backup_dir}/etc/X11/" || true
- cp -a /etc/lightdm 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
-
- 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 --create --comments "pre ubuntu->mint $(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-get with tee but correct exit status
-# -------------------------
-run_apt_tee() {
- local outfile="$1"; shift
- local old_trap
- old_trap="$(trap -p ERR || true)"
- trap - ERR
- set +e
-
- DEBIAN_FRONTEND=noninteractive apt-get "$@" 2>&1 | tee "$outfile"
- local rc="${PIPESTATUS[0]}"
-
- set -e
- if [[ -n "$old_trap" ]]; then eval "$old_trap"; else trap 'on_err "$LINENO" "$?"' ERR; fi
- return "$rc"
-}
-
-parse_and_guard_apt_actions() {
- local sim_output_file="$1"
- [[ -r "$sim_output_file" ]] || die "Missing simulation output: $sim_output_file"
-
- local removed removed_count
- removed="$(grep -E '^Remv ' "$sim_output_file" | awk '{print $2}' || true)"
- removed_count="$(echo "$removed" | grep -c . || true)"
-
- info "APT simulation: packages marked for removal: ${removed_count}"
-
- 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|netplan\.io|systemd-resolved)$'
- local bad=""
- while IFS= read -r p; do
- [[ -z "$p" ]] && continue
- [[ "$p" =~ $critical_re ]] && bad+="$p"$'\n'
- done <<< "$removed"
-
- if [[ -n "$bad" ]]; then
- echo "$bad" | sed 's/^/ - /'
- die "Refusing to proceed: simulation removes critical packages above."
- fi
-
- if [[ "$removed_count" -gt 25 ]]; then
- die "Refusing to proceed: too many removals (${removed_count}). Inspect conflicts."
- fi
-
- ok "Guard rails passed."
-}
-
-# -------------------------
-# Desktop / Display Manager defaults (prefer X11 by default)
-# -------------------------
-find_session_name_for_edition() {
- local desired="$1"
- local prefer_wayland="$2" # yes|no
- local xs="/usr/share/xsessions"
- local ws="/usr/share/wayland-sessions"
-
- [[ -d "$xs" ]] || die "Missing $xs (no X sessions installed?)."
-
- local sess=""
-
- if [[ "$prefer_wayland" == "yes" ]]; then
- local -a way_candidates=()
- case "$desired" in
- cinnamon) way_candidates=(cinnamon-wayland) ;;
- mate) way_candidates=() ;;
- xfce) way_candidates=() ;;
- esac
-
- local c
- for c in "${way_candidates[@]}"; do
- if [[ -f "$xs/${c}.desktop" ]]; then
- sess="$c"
- echo "$sess"
- return 0
- fi
- if [[ -d "$ws" && -f "$ws/${c}.desktop" ]]; then
- warn "Wayland session '${c}' exists in ${ws}, but LightDM may not support it. Falling back to X11."
- fi
- done
- fi
-
- local -a candidates=()
- case "$desired" in
- cinnamon) candidates=(cinnamon cinnamon2d) ;;
- mate) candidates=(mate) ;;
- xfce) candidates=(xfce xfce4) ;;
- *) candidates=("$desired") ;;
+###############################################################################
+# Packages / simulation / install
+###############################################################################
+edition_meta_pkg() {
+ case "$EDITION" in
+ cinnamon) echo "mint-meta-cinnamon" ;;
+ mate) echo "mint-meta-mate" ;;
+ xfce) echo "mint-meta-xfce" ;;
esac
-
- local c
- for c in "${candidates[@]}"; do
- [[ -f "$xs/${c}.desktop" ]] && sess="$c" && break
- done
-
- [[ -n "$sess" ]] || sess="$desired"
- echo "$sess"
}
-set_mint_defaults_display_manager_and_session() {
- local dm_pkg="lightdm"
- local greeter="slick-greeter"
- local session
- session="$(find_session_name_for_edition "$EDITION" "$PREFER_WAYLAND")"
+edition_session_name() {
+ case "$EDITION" in
+ cinnamon) echo "cinnamon" ;;
+ mate) echo "mate" ;;
+ xfce) echo "xfce" ;;
+ esac
+}
- if [[ "$PREFER_WAYLAND" == "yes" && "$session" != *wayland* ]]; then
- warn "--prefer-wayland was set, but no LightDM-compatible Wayland session was found; using X11 session '${session}'."
+required_packages_for_convert() {
+ local meta
+ meta="$(edition_meta_pkg)"
+ cat </dev/null
+
+ # Check removals count
+ local removals
+ removals="$(grep -E '^[[:space:]]*Remv[[:space:]]' "$sim_out" | wc -l | awk '{print $1}')"
+ info "Simulation removals detected: ${removals} (max allowed: ${MAX_ALLOWED_REMOVALS})"
+ if [[ "$removals" -gt "$MAX_ALLOWED_REMOVALS" ]]; then
+ die "APT simulation wants to remove too many packages (${removals}). Aborting. Review: ${sim_out}"
fi
- info "Staging defaults (apply fully after reboot): DM=${dm_pkg}, greeter=${greeter}, session=${session}"
+ # Critical packages must never be removed
+ local critical=(sudo systemd systemd-sysv network-manager linux-image-generic)
+ for c in "${critical[@]}"; do
+ if grep -E "^[[:space:]]*Remv[[:space:]]+${c}([[:space:]]|:)" "$sim_out" >/dev/null; then
+ die "APT simulation wants to remove critical package '${c}'. Aborting. Review: ${sim_out}"
+ fi
+ done
- DEBIAN_FRONTEND=noninteractive apt-get -y install "${dm_pkg}" "${greeter}"
+ ok "Simulation looks within safety thresholds. Review log: ${sim_out}"
+}
- local dm_path
- dm_path="$(command -v lightdm 2>/dev/null || true)"
- [[ -n "$dm_path" ]] || dm_path="/usr/sbin/lightdm"
- [[ -x "$dm_path" ]] || die "LightDM binary not found/executable at: $dm_path"
+apt_install_mint_stack() {
+ export DEBIAN_FRONTEND=noninteractive
- mkdir -p /etc/X11
- echo "${dm_path}" > /etc/X11/default-display-manager
+ # Known dpkg overwrite conflict: mintupdate vs software-properties-gtk icon.
+ # We force overwrite ONLY during the conversion install to avoid halting.
+ local dpkg_force_overwrite=(
+ "-o" "Dpkg::Options::=--force-overwrite"
+ "-o" "Dpkg::Options::=--force-confnew"
+ )
- if have_cmd debconf-set-selections; then
- for owner in gdm3 lightdm sddm; do
- dpkg-query -W -f='${Status}' "$owner" 2>/dev/null | grep -q "installed" || continue
- echo "${owner} shared/default-x-display-manager select lightdm" | debconf-set-selections || true
+ local opts=()
+ # shellcheck disable=SC2207
+ opts+=($(apt_get_opts_common))
+ info "Installing Mint stack (with dpkg overwrite guard for known file conflicts)..."
+
+ apt-get "${opts[@]}" install "${dpkg_force_overwrite[@]}" "$@"
+}
+
+###############################################################################
+# Display manager + session defaults
+###############################################################################
+disable_conflicting_lightdm_overrides() {
+ local desired_session
+ desired_session="$(edition_session_name)"
+
+ if [[ -d /etc/lightdm/lightdm.conf.d ]]; then
+ shopt -s nullglob
+ for f in /etc/lightdm/lightdm.conf.d/*.conf; do
+ # Keep our final file
+ [[ "$(basename "$f")" == "99-ubuntu2mint.conf" ]] && continue
+
+ # If a file forces a different user-session, disable it.
+ if grep -Eq '^[[:space:]]*user-session=' "$f"; then
+ local sess
+ sess="$(grep -E '^[[:space:]]*user-session=' "$f" | tail -n1 | cut -d= -f2- | tr -d '[:space:]' || true)"
+ if [[ -n "$sess" && "$sess" != "$desired_session" ]]; then
+ warn "Disabling conflicting LightDM override (forces user-session=${sess}): ${f}"
+ mv -f "$f" "${f}.u2m-disabled" || true
+ fi
+ fi
done
+ shopt -u nullglob
+ fi
+}
+
+ensure_xsession_desktop_exists() {
+ local sess
+ sess="$(edition_session_name)"
+ case "$sess" in
+ cinnamon)
+ if [[ ! -f /usr/share/xsessions/cinnamon.desktop ]]; then
+ warn "Missing /usr/share/xsessions/cinnamon.desktop; creating a minimal session file."
+ cat > /usr/share/xsessions/cinnamon.desktop <<'EOF'
+[Desktop Entry]
+Name=Cinnamon
+Comment=This session logs you into Cinnamon
+Exec=cinnamon-session-cinnamon
+TryExec=cinnamon-session-cinnamon
+Type=Application
+DesktopNames=X-Cinnamon
+EOF
+ fi
+ ;;
+ mate)
+ # mate.desktop typically provided; we won't fabricate unless missing
+ if ! ls /usr/share/xsessions/*.desktop 2>/dev/null | grep -qi mate; then
+ warn "MATE session desktop file not found under /usr/share/xsessions. Ensure mate-session is installed."
+ fi
+ ;;
+ xfce)
+ # xfce.desktop is typical
+ if ! ls /usr/share/xsessions/*.desktop 2>/dev/null | grep -qi xfce; then
+ warn "XFCE session desktop file not found under /usr/share/xsessions. Ensure xfce4-session is installed."
+ fi
+ ;;
+ esac
+}
+
+set_display_manager_and_session_defaults() {
+ local sess
+ sess="$(edition_session_name)"
+
+ info "Staging defaults: DM=lightdm, greeter=slick-greeter, session=${sess}"
+ export DEBIAN_FRONTEND=noninteractive
+
+ # Force LightDM as default display manager
+ if have_cmd debconf-set-selections; then
echo "lightdm shared/default-x-display-manager select lightdm" | debconf-set-selections || true
fi
+ # Ensure lightdm + slick-greeter are present (already in stack)
+ apt-get -y install lightdm slick-greeter || true
+
mkdir -p /etc/lightdm/lightdm.conf.d
- cat > /etc/lightdm/lightdm.conf.d/60-mint-defaults.conf < /etc/lightdm/lightdm.conf.d/99-ubuntu2mint.conf </dev/null || true
- systemctl disable sddm 2>/dev/null || true
- systemctl unmask lightdm.service 2>/dev/null || true
- systemctl enable lightdm.service 2>/dev/null || systemctl enable lightdm 2>/dev/null || true
+ ensure_xsession_desktop_exists
- ok "Defaults staged: lightdm + ${session}"
+ # LightDM implies X11; make sure we don't end up attempting Wayland sessions.
+ # (No explicit Wayland config is needed, but we also ensure x11-common is sane.)
+ apt-get -y install x11-common dbus-x11 || true
+
+ systemctl enable lightdm >/dev/null 2>&1 || true
+
+ ok "Defaults staged: lightdm + ${sess}"
}
-# -------------------------
-# Post-conversion validation
-# -------------------------
-is_enabled_any() { systemctl is-enabled "$1" >/dev/null 2>&1; }
-is_active_any() { systemctl is-active "$1" >/dev/null 2>&1; }
+###############################################################################
+# Cinnamon runtime sanity fixes (prevents login loop causes we observed)
+###############################################################################
+purge_conflicting_ubuntu_flavor_packages() {
+ [[ "$PURGE_CONFLICTING_FLAVORS" == "yes" ]] || return 0
-detect_first_matching_unit() {
- local re="$1"
- systemctl list-unit-files --no-legend 2>/dev/null | awk '{print $1}' | grep -Ei "$re" | head -n1 || true
+ export DEBIAN_FRONTEND=noninteractive
+ local to_purge=()
+
+ # ubuntucinnamon-environment (and friends) has been observed to inject gschema overrides
+ # that don't match Mint Cinnamon, leading to crashes/login loops.
+ if dpkg -l | awk '{print $2}' | grep -q '^ubuntucinnamon-'; then
+ to_purge+=( $(dpkg -l | awk '{print $2}' | grep '^ubuntucinnamon-' || true) )
+ fi
+
+ # Remove ubuntu desktop metapackages only if present and edition is cinnamon,
+ # since they can pull GNOME sessions and change defaults.
+ if [[ "$EDITION" == "cinnamon" ]]; then
+ for p in ubuntu-desktop gdm3; do
+ if dpkg -s "$p" >/dev/null 2>&1; then
+ warn "Detected package '${p}' which may keep GNOME as the default. It will NOT be purged automatically."
+ fi
+ done
+ fi
+
+ if [[ "${#to_purge[@]}" -gt 0 ]]; then
+ warn "Purging conflicting Ubuntu flavor packages: ${to_purge[*]}"
+ apt-get -y purge "${to_purge[@]}" || true
+ apt-get -y autoremove || true
+ ok "Conflicting flavor packages purged (best-effort)."
+ fi
}
-post_convert_validate_or_die() {
- local backup_dir="$1"
- local report="${backup_dir}/post-convert-validation.txt"
- : > "$report"
+fix_cinnamon_symbol_rr_best_effort() {
+ # If csd-* binaries exist, verify they can run (prevents LightDM login loop).
+ # We observed: csd-power/csd-color undefined symbol gnome_rr_screen_new_async
+ # Most often resolved by ensuring gnome-desktop / gnome-rr runtime libs are present and not partially removed.
+ export DEBIAN_FRONTEND=noninteractive
- v() { echo "$*" | tee -a "$report" >/dev/null; }
+ local any="no"
+ [[ -x /usr/bin/csd-power ]] && any="yes"
+ [[ -x /usr/bin/csd-color ]] && any="yes"
+ [[ "$any" == "yes" ]] || return 0
- local failed=0
- local session
- session="$(find_session_name_for_edition "$EDITION" "$PREFER_WAYLAND")"
+ info "Validating cinnamon-settings-daemon helpers (csd-power/csd-color) best-effort..."
- info "Running post-conversion validation (report: ${report})"
-
- v "=== Post-Conversion Validation ==="
- v "Timestamp: $(date -Is)"
- v "Edition: ${EDITION}"
- v "Session: ${session}"
- v "PreferWL: ${PREFER_WAYLAND}"
- v "---------------------------------"
-
- if [[ -f "/usr/share/xsessions/${session}.desktop" ]]; then
- v "OK: session desktop file exists: /usr/share/xsessions/${session}.desktop"
+ local broken="no"
+ if /usr/bin/csd-power --help >/dev/null 2>&1; then
+ :
else
- v "FAIL: missing session desktop file: /usr/share/xsessions/${session}.desktop"
- failed=1
+ broken="yes"
+ fi
+ if [[ -x /usr/bin/csd-color ]] && ! /usr/bin/csd-color --help >/dev/null 2>&1; then
+ broken="yes"
fi
- if [[ "$PREFER_WAYLAND" != "yes" && "$session" == *wayland* ]]; then
- v "WARN: selected session appears to be Wayland despite X11 preference: ${session}"
+ if [[ "$broken" == "no" ]]; then
+ ok "csd-power/csd-color appear runnable."
+ return 0
fi
- if [[ -r /etc/X11/default-display-manager ]]; then
- local dm_path
- dm_path="$(cat /etc/X11/default-display-manager 2>/dev/null || true)"
- if [[ "$dm_path" =~ lightdm ]] && [[ -x "$dm_path" ]]; then
- v "OK: default display manager set to: ${dm_path}"
- else
- v "FAIL: /etc/X11/default-display-manager is '${dm_path}' (expected executable lightdm path)"
- failed=1
- fi
+ warn "csd-* helpers appear broken; attempting library remediation (best-effort)."
+
+ # Ensure core Xsession bits exist (has_option errors typically indicate a broken x11-common installation)
+ apt-get -y install --reinstall x11-common || true
+
+ # Ensure GNOME desktop compatibility libs exist (Noble provides t64 packages)
+ apt-get -y install --reinstall libgnome-desktop-3-20t64 libgnome-desktop-3-common || true
+
+ # libgnome-rr-4 is a distinct runtime in Noble; install if available
+ if apt-cache show libgnome-rr-4-2t64 >/dev/null 2>&1; then
+ apt-get -y install --reinstall libgnome-rr-4-2t64 || true
+ fi
+
+ # Reinstall cinnamon-settings-daemon from Mint repo (pinning should prefer Mint)
+ if dpkg -s cinnamon-settings-daemon >/dev/null 2>&1; then
+ apt-get -y install --reinstall cinnamon-settings-daemon || true
+ fi
+
+ # Re-check
+ if /usr/bin/csd-power --help >/dev/null 2>&1; then
+ ok "csd-power is runnable after remediation."
else
- v "FAIL: missing /etc/X11/default-display-manager"
- failed=1
+ warn "csd-power still appears broken. Cinnamon may login-loop. Check: ldd /usr/bin/csd-power; journalctl -b _UID=\$UID"
fi
-
- if is_enabled_any lightdm.service || is_enabled_any lightdm; then
- v "OK: lightdm is enabled for next boot"
- else
- v "FAIL: lightdm is NOT enabled"
- failed=1
- fi
-
- if is_active_any NetworkManager.service || is_active_any network-manager.service; then
- v "OK: NetworkManager is active"
- else
- v "FAIL: NetworkManager is NOT active"
- failed=1
- fi
-
- if apt-get check >/dev/null 2>&1; then
- v "OK: apt-get check passed"
- else
- v "FAIL: apt-get check failed"
- failed=1
- fi
-
- local snapd_installed="no"
- if dpkg-query -W -f='${Status}' snapd 2>/dev/null | grep -q "installed"; then
- snapd_installed="yes"
- fi
-
- if [[ "$PRESERVE_SNAP" == "yes" && "$snapd_installed" == "yes" ]]; then
- if is_enabled_any snapd.service || is_enabled_any snapd.socket; then
- v "OK: snapd is enabled (service or socket)"
- else
- v "FAIL: snapd is installed but not enabled (service/socket)"
- failed=1
- fi
- else
- v "INFO: snapd check skipped (preserve-snap=${PRESERVE_SNAP}, snapd_installed=${snapd_installed})"
- fi
-
- local falcon_detect="no"
- [[ -d /opt/CrowdStrike ]] && falcon_detect="yes"
- [[ -x /opt/CrowdStrike/falconctl ]] && falcon_detect="yes"
-
- local falcon_unit
- falcon_unit="$(detect_first_matching_unit 'falcon(-sensor)?\.service|crowdstrike|falcon')"
-
- if [[ "$falcon_detect" == "yes" || -n "$falcon_unit" ]]; then
- v "INFO: CrowdStrike detected (unit=${falcon_unit:-none})"
- if [[ -n "$falcon_unit" ]]; then
- if is_enabled_any "$falcon_unit"; then v "OK: ${falcon_unit} is enabled"; else v "FAIL: ${falcon_unit} is NOT enabled"; failed=1; fi
- if is_active_any "$falcon_unit"; then v "OK: ${falcon_unit} is active"; else v "FAIL: ${falcon_unit} is NOT active"; failed=1; fi
- else
- if pgrep -fa 'falcon' >/dev/null 2>&1; then
- v "OK: CrowdStrike process detected (no unit found, but processes are running)"
- else
- v "FAIL: CrowdStrike files detected but no unit found and no falcon process running"
- failed=1
- fi
- fi
- else
- v "INFO: CrowdStrike not detected (skipping)"
- fi
-
- local gp_detect="no"
- [[ -d /opt/paloaltonetworks/globalprotect ]] && gp_detect="yes"
- [[ -x /opt/paloaltonetworks/globalprotect/PanGPS ]] && gp_detect="yes"
-
- local gp_unit
- gp_unit="$(detect_first_matching_unit 'gpd\.service|pangps\.service|globalprotect|pan(gp|gps)')"
-
- if [[ "$gp_detect" == "yes" || -n "$gp_unit" ]]; then
- v "INFO: GlobalProtect detected (unit=${gp_unit:-none})"
- if [[ -x /opt/paloaltonetworks/globalprotect/PanGPS ]]; then
- v "OK: PanGPS binary present"
- else
- v "FAIL: PanGPS binary missing at /opt/paloaltonetworks/globalprotect/PanGPS"
- failed=1
- fi
-
- if [[ -n "$gp_unit" ]]; then
- if is_enabled_any "$gp_unit"; then
- v "OK: ${gp_unit} is enabled"
- else
- v "FAIL: ${gp_unit} is NOT enabled"
- failed=1
- fi
- if is_active_any "$gp_unit"; then
- v "OK: ${gp_unit} is active"
- else
- v "FAIL: ${gp_unit} is NOT active (VPN may fail to connect after reboot)"
- failed=1
- fi
- else
- if pgrep -x PanGPS >/dev/null 2>&1 || pgrep -x PanGPA >/dev/null 2>&1; then
- v "OK: GlobalProtect processes detected (no unit found)"
- else
- v "WARN: No GlobalProtect unit found and no PanGPS/PanGPA process running (may be normal if not connected)"
- fi
- fi
- else
- v "INFO: GlobalProtect not detected (skipping)"
- fi
-
- if [[ -r /etc/lightdm/lightdm.conf.d/60-mint-defaults.conf ]]; then
- if grep -q "user-session=${session}" /etc/lightdm/lightdm.conf.d/60-mint-defaults.conf; then
- v "OK: LightDM default session configured to ${session}"
- else
- v "FAIL: LightDM defaults file does not set user-session=${session}"
- failed=1
- fi
- if grep -q "greeter-session=slick-greeter" /etc/lightdm/lightdm.conf.d/60-mint-defaults.conf; then
- v "OK: LightDM greeter configured to slick-greeter"
- else
- v "FAIL: LightDM defaults file does not set greeter-session=slick-greeter"
- failed=1
- fi
- else
- v "FAIL: missing /etc/lightdm/lightdm.conf.d/60-mint-defaults.conf"
- failed=1
- fi
-
- if [[ "$failed" -ne 0 ]]; then
- echo
- echo "${RED}${BOLD}DO NOT REBOOT YET.${NC}"
- echo "${RED}${BOLD}Post-conversion validation FAILED.${NC}"
- echo "Report: ${report}"
- echo "Log: ${LOG_FILE}"
- echo
- echo "Suggested next steps:"
- echo " 1) Open the report and fix the failing items."
- echo " 2) If you need to revert APT sources immediately:"
- echo " ${ON_ERROR_BACKUP_HINT}"
- echo
- exit 12
- fi
-
- ok "Post-conversion validation passed."
}
-# -------------------------
-# Plan mode (temp APT env)
-# -------------------------
-apt_simulate_with_temp_sources() {
+###############################################################################
+# Disclaimer gate (convert only)
+###############################################################################
+convert_disclaimer_gate() {
+ [[ "$SUBCMD" == "convert" ]] || return 0
+
+ # Force explicit flag + interactive confirmation (even with --yes).
+ [[ "$RISK_ACK_FLAG" == "yes" ]] || die "convert requires --i-accept-the-risk"
+
+ echo
+ echo "${c_red}${c_bold}======================================================================${c_reset}"
+ echo "${c_red}${c_bold} UNSUPPORTED MIGRATION METHOD — PROBABLY A REALLY DUMB IDEA${c_reset}"
+ echo "${c_red}${c_bold}======================================================================${c_reset}"
+ echo "${c_red}${c_bold}This script attempts to graft Linux Mint repositories/packages onto Ubuntu.${c_reset}"
+ echo "${c_red}${c_bold}It is NOT supported by Linux Mint, Canonical, your IT department, or your employer.${c_reset}"
+ echo "${c_red}${c_bold}It can break APT, boot/login, device management, VPN/EDR, and leave the system unrecoverable.${c_reset}"
+ echo "${c_red}${c_bold}${c_reset}"
+ echo "${c_red}${c_bold}Recommended approach: do a CLEAN Linux Mint install and restore your data/apps from backup.${c_reset}"
+ echo "${c_red}${c_bold}If this is a corporate-managed device: STOP and get approval first.${c_reset}"
+ echo "${c_red}${c_bold}======================================================================${c_reset}"
+ echo
+
+ echo "To continue anyway, type exactly:"
+ echo " I UNDERSTAND THIS IS UNSUPPORTED"
+ echo "Anything else will abort."
+ echo
+
+ local answer=""
+ read -r -p "> " answer
+ [[ "$answer" == "I UNDERSTAND THIS IS UNSUPPORTED" ]] || die "Disclaimer not acknowledged. Aborting."
+ ok "Disclaimer acknowledged."
+}
+
+###############################################################################
+# Plan mode (safe simulation using TEMP sources + temp keyring)
+###############################################################################
+plan_mode() {
+ require_root
+ detect_os
+ check_apt_locks
+
+ if [[ "$AUTO_FIX" == "yes" ]]; then
+ apt_fix_basic
+ fi
+
+ info "Plan mode: simulating install with temporary sources/keyring (no APT sources are modified)."
+ ensure_deps_for_keyring
+
local tmp
tmp="$(mktemp -d)"
- trap 'rm -rf "$tmp"' RETURN
+ chmod 700 "$tmp"
+ local tmp_key="${tmp}/linuxmint-repo.gpg"
- mkdir -p "$tmp/etc"
- cp -a /etc/apt "$tmp/etc/"
-
- mkdir -p "$tmp/usr/share/keyrings"
- local temp_keyring="$tmp/usr/share/keyrings/linuxmint-repo.gpg"
- info "Plan mode: creating temporary Mint keyring at $temp_keyring"
- mint_repo_key_write_to "$temp_keyring" "no"
+ # Create a temp keyring (never touches system keyring file)
+ info "Preparing temporary Mint keyring for plan mode: ${tmp_key}"
+ OVERWRITE_KEYRING="yes"
+ RECREATE_KEYRING="yes"
+ mint_repo_key_write_to "$tmp_key"
detect_ubuntu_mirrors
-
- cat > "$tmp/etc/apt/sources.list" < "$tmp_sources" < "$tmp/etc/apt/preferences.d/50-linuxmint-conversion.pref" <<'EOF'
+ mkdir -p "${tmp}/lists" "${tmp}/cache" "${tmp}/prefs.d"
+ cp -a /etc/apt/preferences.d/*.pref "${tmp}/prefs.d/" 2>/dev/null || true
+
+ # Add our pinning into temp prefs
+ cat > "${tmp}/prefs.d/50-linuxmint-conversion.pref" <<'EOF'
Package: *
-Pin: release o=LinuxMint
+Pin: origin "packages.linuxmint.com"
Pin-Priority: 100
-Package: mint* mintsources* mintupdate* mintsystem* mintstick* mintmenu* mintlocale* mintdrivers* mintreport* mintwelcome*
-Pin: release o=LinuxMint
-Pin-Priority: 700
-
-Package: mint* mintsources* mintupdate* mintsystem* mintstick* mintmenu* mintlocale* mintdrivers* mintreport* mintwelcome*
+Package: mint* mintsources* mintupdate* mintsystem* mintstick* mintmenu* mintlocale* mintdrivers* mintreport* mintwelcome* mintinstall*
Pin: origin "packages.linuxmint.com"
Pin-Priority: 700
-
-Package: cinnamon* nemo* muffin* cjs* slick-greeter* lightdm* pix* xviewer* mint-themes* mint-y-icons* mint-x-icons*
-Pin: release o=LinuxMint
-Pin-Priority: 900
-
-Package: cinnamon* nemo* muffin* cjs* slick-greeter* lightdm* pix* xviewer* mint-themes* mint-y-icons* mint-x-icons*
-Pin: origin "packages.linuxmint.com"
-Pin-Priority: 900
-
-Package: python3-xapp* python-xapp* libxapp* gir1.2-xapp* xapps* xapps-common xapp-symbolic-icons xapp-status-icon
-Pin: release o=LinuxMint
-Pin-Priority: 1001
-
-Package: python3-xapp* python-xapp* libxapp* gir1.2-xapp* xapps* xapps-common xapp-symbolic-icons xapp-status-icon
-Pin: origin "packages.linuxmint.com"
-Pin-Priority: 1001
-
-Package: libnemo-extension1* nemo-data*
-Pin: release o=LinuxMint
-Pin-Priority: 1001
-
-Package: libnemo-extension1* nemo-data*
-Pin: origin "packages.linuxmint.com"
-Pin-Priority: 1001
-
-Package: libcinnamon-control-center1* libcinnamon-menu-3-0* libcinnamon-desktop4* cinnamon-desktop-data* cinnamon-control-center-data* cinnamon-l10n*
-Pin: release o=LinuxMint
-Pin-Priority: 1001
-
-Package: libcinnamon-control-center1* libcinnamon-menu-3-0* libcinnamon-desktop4* cinnamon-desktop-data* cinnamon-control-center-data* cinnamon-l10n*
+EOF
+ cat > "${tmp}/prefs.d/51-linuxmint-desktop-stack.pref" <<'EOF'
+Package: cinnamon* nemo* muffin* cjs* libcjs* libmuffin* libnemo-extension* nemo-* xapp* xapps-* slick-greeter* lightdm* mint-themes* mint-y-icons* mint-x-icons* pix* xviewer* cinnamon-settings-daemon* cinnamon-control-center* cinnamon-session* cinnamon-desktop-data* cinnamon-l10n* libcinnamon* gir1.2-cmenu-3.0 libcinnamon-menu-3-0*
Pin: origin "packages.linuxmint.com"
Pin-Priority: 1001
EOF
- mkdir -p "$tmp/var/lib/apt/lists/partial" "$tmp/var/cache/apt/archives/partial"
+ local pkgs
+ mapfile -t pkgs < <(required_packages_for_convert)
- local recommends="--no-install-recommends"
- [[ "$WITH_RECOMMENDS" == "yes" ]] && recommends=""
+ info "Running plan simulation..."
+ export DEBIAN_FRONTEND=noninteractive
- local -a 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)
+ # We must point apt at temp source list + temp lists/cache, but use real dpkg status.
+ apt-get \
+ -o "Dir::Etc::sourcelist=${tmp_sources}" \
+ -o "Dir::Etc::sourceparts=-" \
+ -o "Dir::Etc::PreferencesParts=${tmp}/prefs.d" \
+ -o "Dir::State::Lists=${tmp}/lists" \
+ -o "Dir::Cache=${tmp}/cache" \
+ -o "Dir::State::status=/var/lib/dpkg/status" \
+ -y update
- info "Plan mode: apt update (temporary dirs)..."
- DEBIAN_FRONTEND=noninteractive apt-get \
- -o Dir::Etc="$tmp/etc/apt" \
- -o Dir::Etc::sourceparts="-" \
- -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
+ # Simulation install
+ apt-get \
+ -o "Dir::Etc::sourcelist=${tmp_sources}" \
+ -o "Dir::Etc::sourceparts=-" \
+ -o "Dir::Etc::PreferencesParts=${tmp}/prefs.d" \
+ -o "Dir::State::Lists=${tmp}/lists" \
+ -o "Dir::Cache=${tmp}/cache" \
+ -o "Dir::State::status=/var/lib/dpkg/status" \
+ -s $(apt_get_opts_common) install "${pkgs[@]}" \
+ | tee "${LOG_DIR}/plan-$(date +%Y%m%d-%H%M%S).txt" >/dev/null
- local plan_log="$LOG_DIR/plan-$(date +%Y%m%d-%H%M%S).txt"
- info "Plan mode: simulated install: ${pkgs[*]}"
- if ! run_apt_tee "$plan_log" \
- -o Dir::Etc="$tmp/etc/apt" \
- -o Dir::Etc::sourceparts="-" \
- -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[@]}"; then
- tail -n 160 "$plan_log" >&2 || true
- die "Plan simulation failed. See: $plan_log"
- fi
-
- ok "Plan completed. Review: $plan_log"
+ ok "Plan mode complete. Temp dir preserved for review: ${tmp}"
}
-# -------------------------
-# Convert mode
-# -------------------------
-convert_apply() {
- [[ "$ACCEPT_RISK" == "yes" ]] || die "You must pass --i-accept-the-risk to run convert."
+###############################################################################
+# Post-conversion validation (writes report file reliably)
+###############################################################################
+post_convert_validation() {
+ local report="$1"
+ : > "$report" || die "Unable to write report file: $report"
- require_unsupported_disclaimer
- preflight_common "yes"
-
- if [[ "$ASSUME_YES" != "yes" ]]; then
+ local fail="no"
+ {
+ echo "Post-convert validation report: $(date -Is)"
+ echo "Edition: ${EDITION}"
+ echo "Ubuntu base: ${UBUNTU_BASE} | Mint target: ${TARGET_MINT}"
echo
- warn "This can break a corporate-managed machine. Ensure you have approval + a rollback plan."
- 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."
+ echo "Keyring: ${KEYRING_OUT}"
+ if [[ -r "$KEYRING_OUT" ]] && gpg --batch --quiet --show-keys "$KEYRING_OUT" >/dev/null 2>&1; then
+ echo " OK: keyring readable"
+ else
+ echo " FAIL: keyring missing or unreadable"
+ fail="yes"
+ fi
+
+ echo
+ echo "Sources: ${SOURCES_OUT}"
+ if [[ -r "$SOURCES_OUT" ]] && grep -q "packages.linuxmint.com" "$SOURCES_OUT"; then
+ echo " OK: sources file present"
+ else
+ echo " FAIL: sources file missing or does not reference Mint"
+ fail="yes"
+ fi
+
+ echo
+ echo "APT health:"
+ if apt-get -s check >/dev/null 2>&1; then
+ echo " OK: apt-get check"
+ else
+ echo " WARN: apt-get check reports issues"
+ fail="yes"
+ fi
+
+ echo
+ echo "Display manager:"
+ if systemctl is-enabled lightdm >/dev/null 2>&1; then
+ echo " OK: lightdm enabled"
+ else
+ echo " WARN: lightdm not enabled"
+ fail="yes"
+ fi
+
+ echo
+ echo "Sessions present:"
+ ls -1 /usr/share/xsessions 2>/dev/null | egrep -i 'cinnamon|mate|xfce|ubuntu|gnome' || true
+
+ echo
+ echo "Cinnamon helper sanity (if installed):"
+ if [[ -x /usr/bin/csd-power ]]; then
+ if /usr/bin/csd-power --help >/dev/null 2>&1; then
+ echo " OK: csd-power runnable"
+ else
+ echo " FAIL: csd-power not runnable (may cause login loop)"
+ echo " Hint: ldd /usr/bin/csd-power ; journalctl -b _UID=\$UID"
+ fail="yes"
+ fi
+ else
+ echo " NOTE: csd-power not found"
+ fi
+
+ echo
+ echo "LightDM config:"
+ grep -R --line-number -E 'user-session=|greeter-session=' /etc/lightdm 2>/dev/null || true
+ } >> "$report"
+
+ [[ "$fail" == "no" ]]
+}
+
+###############################################################################
+# Convert mode (main)
+###############################################################################
+convert_mode() {
+ require_root
+ detect_os
+ check_apt_locks
+ convert_disclaimer_gate
+
+ setup_logging
+
+ if [[ "$AUTO_FIX" == "yes" ]]; then
+ apt_fix_basic
fi
- local backup_dir
- backup_dir="$(backup_system_state)"
+ # Backup first (single, stable)
+ BACKUP_DIR="$(backup_system_state)"
+ ON_ERROR_BACKUP_HINT="sudo bash ${SCRIPT_NAME} rollback ${BACKUP_DIR}"
+
timeshift_snapshot_best_effort
- local held
- held="$(apt-mark showhold || true)"
- if [[ -n "$held" ]]; then
- warn "Held packages detected:"
- echo "$held" | sed 's/^/ HOLD: /'
- if [[ "$ALLOW_UNHOLD" == "yes" ]]; then
- info "Saving holds list to ${backup_dir}/held-packages.txt and temporarily unholding..."
- echo "$held" > "${backup_dir}/held-packages.txt"
- # shellcheck disable=SC2086
- apt-mark unhold $held
- else
- die "Held packages will block dependency resolution. Resolve holds or re-run with --allow-unhold."
- fi
- fi
-
- DEBIAN_FRONTEND=noninteractive apt-get -y install debconf-utils || true
- if have_cmd debconf-set-selections; then
- echo "lightdm shared/default-x-display-manager select lightdm" | debconf-set-selections || true
- fi
-
- 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"
+ disable_thirdparty_sources_system "$BACKUP_DIR"
else
- warn "--keep-ppas set. Proceeding with third-party sources enabled may increase conflict risk."
+ warn "--keep-ppas enabled; leaving third-party sources intact (riskier)."
fi
- mint_repo_key_install
+ # Keyring + sources + pinning
+ mint_repo_key_install_system
write_mint_sources_system
write_mint_pinning_system
+ remove_nosnap_pref_if_needed
- info "APT update..."
- DEBIAN_FRONTEND=noninteractive apt-get -o Acquire::Retries=3 update
-
- if ! apt-cache show mint-meta-core >/dev/null 2>&1; then
- die "Mint repo not usable: apt cannot see 'mint-meta-core'. Check mirror/key/network."
- fi
-
- local recommends="--no-install-recommends"
- [[ "$WITH_RECOMMENDS" == "yes" ]] && recommends=""
-
- local -a pkgs=()
- case "$EDITION" in
- cinnamon) pkgs+=(mint-meta-cinnamon) ;;
- mate) pkgs+=(mint-meta-mate) ;;
- xfce) pkgs+=(mint-meta-xfce) ;;
- esac
- pkgs+=(mint-meta-core mintsystem mintupdate mintsources mint-meta-codecs)
-
- info "Simulation (safety check) of install: ${pkgs[*]}"
- local sim_out="${backup_dir}/apt-sim-install.txt"
- if ! run_apt_tee "$sim_out" -s install $recommends "${pkgs[@]}"; then
- tail -n 200 "$sim_out" >&2 || true
- die "APT simulation failed. See: $sim_out"
- fi
- parse_and_guard_apt_actions "$sim_out"
-
- apply_known_diversions
-
- info "Installing Mint packages..."
export DEBIAN_FRONTEND=noninteractive
- export NEEDRESTART_MODE=a
+ info "APT update..."
+ apt-get -y update
- if ! apt-get -y install \
- -o Dpkg::Options::=--force-overwrite \
- -o Dpkg::Options::=--force-confdef \
- -o Dpkg::Options::=--force-confold \
- $recommends \
- "${pkgs[@]}"; then
- warn "Initial install hit errors; attempting fix-broken with overwrite smoothing..."
- apt_fix_broken_overwrite
- apt-get -y install \
- -o Dpkg::Options::=--force-overwrite \
- -o Dpkg::Options::=--force-confdef \
- -o Dpkg::Options::=--force-confold \
- $recommends \
- "${pkgs[@]}"
+ # Purge known conflicting Ubuntu flavor bits (especially ubuntucinnamon-environment)
+ purge_conflicting_ubuntu_flavor_packages
+
+ # Determine packages and run simulation first
+ local pkgs
+ mapfile -t pkgs < <(required_packages_for_convert)
+ apt_simulate_install "${pkgs[@]}"
+
+ # Install
+ apt_install_mint_stack "${pkgs[@]}"
+
+ # Re-run apt fix
+ apt-get -y -f install || true
+ apt-get -y --fix-broken install || true
+
+ # Stage DM + session defaults
+ set_display_manager_and_session_defaults
+
+ # Best-effort fix for the csd-power/csd-color symbol issue + Xsession "has_option" breakage
+ fix_cinnamon_symbol_rr_best_effort
+
+ # Write post-convert report into backup dir
+ local report="${BACKUP_DIR}/post-convert-validation.txt"
+ info "Running post-conversion validation (report: ${report})"
+ if post_convert_validation "$report"; then
+ ok "Post-conversion validation PASSED."
+ echo
+ ok "Conversion complete."
+ info "Reboot recommended."
+ info "If you need rollback: sudo bash ${SCRIPT_NAME} rollback ${BACKUP_DIR}"
+ else
+ echo
+ echo "${c_red}${c_bold}DO NOT REBOOT YET.${c_reset}"
+ echo "${c_red}${c_bold}Post-conversion validation FAILED.${c_reset}"
+ echo "Report: ${report}"
+ echo "Log: ${LOG_FILE}"
+ echo
+ echo "Suggested next steps:"
+ echo " 1) Open the report and fix the failing items."
+ echo " 2) If you need to revert APT sources immediately:"
+ echo " sudo bash ${SCRIPT_NAME} rollback ${BACKUP_DIR}"
+ exit 2
fi
-
- 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"
- DEBIAN_FRONTEND=noninteractive apt-get -o Acquire::Retries=3 update
- fi
- fi
-
- set_mint_defaults_display_manager_and_session
-
- if [[ -f "${backup_dir}/held-packages.txt" ]]; then
- info "Re-applying package holds from ${backup_dir}/held-packages.txt"
- # shellcheck disable=SC2046
- apt-mark hold $(cat "${backup_dir}/held-packages.txt") || true
- fi
-
- post_convert_validate_or_die "$backup_dir"
-
- ok "Conversion completed successfully."
- info "Backup dir: ${backup_dir}"
- info "Log file: ${LOG_FILE}"
- echo
- echo "Next steps:"
- echo " 1) Reboot (required for display manager/session defaults to fully apply)."
- echo " 2) Validate corp software end-to-end: VPN, EDR, SSO, printers, etc."
-}
-
-# -------------------------
-# Rollback mode: restore /etc/apt from backup (does NOT remove installed packages)
-# -------------------------
-rollback_apply() {
- need_root
- local backup_dir="$1"
- [[ -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"
-
- if [[ "$backup_dir" != /root/ubuntu-to-mint-backup-* ]]; then
- warn "Backup dir does not match expected pattern /root/ubuntu-to-mint-backup-*. Proceeding anyway."
- fi
-
- info "Restoring /etc/apt from backup: ${backup_dir}/etc/apt"
- if [[ -d /etc/apt ]]; then
- mv /etc/apt "/etc/apt.pre-rollback.$(date +%Y%m%d-%H%M%S)" || true
- fi
- cp -a "${backup_dir}/etc/apt" /etc/apt
-
- 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 system state."
}
+###############################################################################
+# Main
+###############################################################################
main() {
- case "$MODE" in
+ case "$SUBCMD" in
doctor)
- preflight_common "$AUTO_FIX"
- ok "Doctor completed."
+ require_root
+ setup_logging
+ parse_args "$@"
+ detect_os
+ check_apt_locks
+ [[ "$AUTO_FIX" == "yes" ]] && apt_fix_basic
+ doctor_report
;;
plan)
- preflight_common "$AUTO_FIX"
- apt_simulate_with_temp_sources
+ setup_logging
+ parse_args "$@"
+ plan_mode
;;
convert)
- convert_apply
+ parse_args "$@"
+ convert_mode
;;
rollback)
- rollback_apply "$ROLLBACK_DIR"
+ require_root
+ setup_logging
+ [[ $# -ge 1 ]] || die "rollback requires a backup directory path."
+ rollback_from_backup "$1"
+ ;;
+ ""|help|-h|--help)
+ usage
;;
*)
usage
- exit 1
+ die "Unknown subcommand: ${SUBCMD}"
;;
esac
}