From abd5d74cb1ab6b44983710a03b0e0fddae9f6026 Mon Sep 17 00:00:00 2001
From: "LINUXexpert.org" <51408202+LINUXexpert-org@users.noreply.github.com>
Date: Fri, 23 Jan 2026 11:11:32 -0700
Subject: [PATCH] Number of enhancements
---
ubuntu-to-mint-convert-v3.sh | 1213 ++++++++++++++++++++++++++--------
1 file changed, 942 insertions(+), 271 deletions(-)
diff --git a/ubuntu-to-mint-convert-v3.sh b/ubuntu-to-mint-convert-v3.sh
index 968946e..5cff9d9 100644
--- a/ubuntu-to-mint-convert-v3.sh
+++ b/ubuntu-to-mint-convert-v3.sh
@@ -2,106 +2,163 @@
# 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
+#
+# 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
+#
+# 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.
+# ----------------------------------------------------------------------
+# 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 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")
+# 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 : 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)
+# 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.2"
-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
+export LC_ALL=C
+
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
- if [[ -n "${ON_ERROR_BACKUP_HINT}" ]]; then
- echo "Rollback hint: ${ON_ERROR_BACKUP_HINT}" >&2
- fi
+ [[ -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
- 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 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:
- --edition cinnamon|mate|xfce
+ --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
- --keep-ppas
- --preserve-snap
- --with-recommends
- --yes
+ --mint-mirror URL (default: https://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 ---
+# -------------------------
+# Defaults / arg parsing
+# -------------------------
MODE="${1:-}"
+[[ -n "$MODE" ]] || { usage; exit 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"
TARGET_MINT=""
-MINT_MIRROR="http://packages.linuxmint.com"
+MINT_MIRROR="https://packages.linuxmint.com"
KEEP_PPAS="no"
+ALLOW_UNHOLD="no"
PRESERVE_SNAP="yes"
WITH_RECOMMENDS="no"
ASSUME_YES="no"
-
-# --- Parse args (simple) ---
-shift || true
ACCEPT_RISK="no"
+AUTO_FIX="no"
+KEYRING_MODE="auto" # auto|overwrite|recreate
+PREFER_WAYLAND="no"
+
while [[ $# -gt 0 ]]; do
case "$1" in
--i-accept-the-risk) ACCEPT_RISK="yes"; shift;;
@@ -109,8 +166,14 @@ while [[ $# -gt 0 ]]; do
--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)";;
@@ -124,6 +187,10 @@ validate_choice() {
esac
}
+validate_mirror() {
+ [[ "$MINT_MIRROR" =~ ^https?:// ]] || die "--mint-mirror must start with http:// or https://"
+}
+
read_os_release() {
[[ -r /etc/os-release ]] || die "/etc/os-release missing"
# shellcheck disable=SC1091
@@ -134,29 +201,33 @@ read_os_release() {
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
+}
+
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
+ 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"
}
set_targets_from_ubuntu() {
UBUNTU_CODENAME="$(detect_ubuntu_codename)"
+ local -a ALLOWED_TARGETS=()
case "$UBUNTU_CODENAME" in
noble)
UBUNTU_BASE="noble"
- DEFAULT_MINT="zena" # Mint 22.3 Zena (Ubuntu Noble base)
+ DEFAULT_MINT="zena"
ALLOWED_TARGETS=(zena zara xia wilma)
;;
jammy)
UBUNTU_BASE="jammy"
- DEFAULT_MINT="virginia" # Mint 21.3 on Ubuntu Jammy base
+ DEFAULT_MINT="virginia"
ALLOWED_TARGETS=(virginia victoria vera vanessa)
;;
*)
@@ -164,18 +235,12 @@ set_targets_from_ubuntu() {
;;
esac
- if [[ -z "$TARGET_MINT" ]]; then
- TARGET_MINT="$DEFAULT_MINT"
- fi
+ [[ -n "$TARGET_MINT" ]] || TARGET_MINT="$DEFAULT_MINT"
local ok_target="no"
for t in "${ALLOWED_TARGETS[@]}"; do
- if [[ "$TARGET_MINT" == "$t" ]]; then
- ok_target="yes"
- break
- fi
+ [[ "$TARGET_MINT" == "$t" ]] && ok_target="yes" && break
done
-
if [[ "$ok_target" != "yes" ]]; then
local allowed_str
printf -v allowed_str '%s ' "${ALLOWED_TARGETS[@]}"
@@ -184,75 +249,11 @@ set_targets_from_ubuntu() {
fi
}
-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}"
-
- if have_cmd systemd-detect-virt; then
- if systemd-detect-virt --container >/dev/null 2>&1; then
- die "Detected container environment. Aborting."
- fi
- fi
-
- local arch
- arch="$(dpkg --print-architecture)"
- [[ "$arch" == "amd64" ]] || warn "Architecture is '$arch'. Proceeding may fail."
-
- 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
-
- 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 checks (soft warnings)
- 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)."
-
- 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
-
- 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
-
- if have_cmd snap; then
- info "Snap detected. Installed snaps:"
- snap list || true
- else
- info "snap not installed."
- fi
-
- ok "Preflight completed."
-}
-
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 [[ -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
@@ -267,92 +268,309 @@ detect_ubuntu_mirrors() {
m="$(echo "$sources_txt" | grep -Eo 'https?://[^ ]+/ubuntu' | head -n1 || true)"
[[ -n "$m" ]] && UBUNTU_ARCHIVE_MIRROR="$m"
- 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}"
}
-# -----------------------------
-# KEYRING HANDLING (UPDATED)
-# -----------------------------
+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
+ if [[ -e "$l" ]] && fuser "$l" >/dev/null 2>&1; then
+ die "APT/DPKG lock is held ($l). Close updaters and try again."
+ fi
+ done
+
+ 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
+ else
+ have_cmd curl || warn "curl not found (recommended)."
+ have_cmd gpg || warn "gpg not found (recommended)."
+ 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)
+# -------------------------
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
- DEBIAN_FRONTEND=noninteractive apt-get update -y
- DEBIAN_FRONTEND=noninteractive apt-get install -y gnupg dirmngr ca-certificates curl
+ local out_dir
+ out_dir="$(dirname "$out_keyring")"
+ mkdir -p "$out_dir"
- mkdir -p "$(dirname "$out_keyring")"
+ # Atomicity: temp file in SAME directory as out_keyring
+ local tmp_out
+ tmp_out="$(mktemp -p "$out_dir" ".linuxmint-repo.gpg.tmp.XXXXXX")"
+ chmod 600 "$tmp_out"
local gnupghome
gnupghome="$(mktemp -d)"
chmod 700 "$gnupghome"
- # 1) Try hkps (443)
- if gpg --homedir "$gnupghome" --batch --keyserver hkps://keyserver.ubuntu.com --recv-keys "$keyid" >/dev/null 2>&1; then
- :
- # 2) Try hkp over port 80 (often allowed when hkps is blocked)
- elif gpg --homedir "$gnupghome" --batch --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys "$keyid" >/dev/null 2>&1; then
- :
+ 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"
else
- # 3) Fallback: fetch armored key over HTTPS/HTTP and dearmor
- info "Keyserver blocked; fetching key over HTTPS from Ubuntu keyserver..."
+ got="no"
+ fi
+
+ if [[ "$got" == "yes" ]]; then
+ if ! gpg --homedir "$gnupghome" --batch --export "$keyid" | gpg --batch --dearmor -o "$tmp_out"; then
+ rm -rf "$gnupghome"
+ rm -f "$tmp_out"
+ die "Failed to export+dearmor the Mint repo key from keyserver results."
+ fi
+ else
+ info "Keyserver blocked; fetching key over HTTPS from Ubuntu keyserver (exact match)..."
local armored="$gnupghome/linuxmint-repo.asc"
- if ! curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x${keyid}" -o "$armored"; then
- curl -fsSL "http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x${keyid}" -o "$armored" \
+ if ! curl -fsSL --connect-timeout 10 --max-time 30 \
+ "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x${keyid}&exact=on" -o "$armored"; then
+ curl -fsSL --connect-timeout 10 --max-time 30 \
+ "http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x${keyid}&exact=on" -o "$armored" \
|| die "Unable to fetch Mint repo key via keyserver or HTTPS fallback."
fi
- # Sanity: ensure it's a PGP public key block (avoid proxy HTML portals)
grep -q "BEGIN PGP PUBLIC KEY BLOCK" "$armored" \
|| die "Downloaded key is not a PGP public key block (proxy portal/HTML?)"
- # Validate: the expected keyid appears in the key block (pub OR sub)
local found="no"
while IFS= read -r kid; do
- if [[ "${kid^^}" == "${keyid^^}" ]]; then
- found="yes"
- break
- fi
+ [[ "${kid^^}" == "${keyid^^}" ]] && found="yes" && break
done < <(gpg --batch --with-colons --show-keys "$armored" | awk -F: '$1=="pub"||$1=="sub"{print $5}')
[[ "$found" == "yes" ]] || die "Fetched key does not contain expected keyid ${keyid^^}"
- # Optional guard: check UID contains expected label
- if ! gpg --batch --with-colons --show-keys "$armored" | awk -F: '$1=="uid"{print $10}' | grep -qi "Linux Mint Repository Signing Key"; then
- warn "Keyid matched but UID did not match expected Mint repo UID; review the downloaded key if concerned."
- fi
-
- gpg --batch --dearmor -o "$out_keyring" "$armored"
- chmod 644 "$out_keyring"
- rm -rf "$gnupghome"
- return 0
+ gpg --batch --dearmor -o "$tmp_out" "$armored"
fi
- # If we got here, gpg received the key into temp keyring; export+dearmor
- gpg --homedir "$gnupghome" --batch --export "$keyid" | gpg --batch --dearmor -o "$out_keyring"
+ chmod 644 "$tmp_out"
+ mv -f "$tmp_out" "$out_keyring"
chmod 644 "$out_keyring"
rm -rf "$gnupghome"
+
+ # Post-write fingerprint logging + optional hard check
+ 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
}
mint_repo_key_install() {
local keyring="/usr/share/keyrings/linuxmint-repo.gpg"
+ local keyid="A6616109451BBBF2"
+
+ have_regular_file_or_die "$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
+ fi
+
info "Installing Linux Mint repo signing key into ${keyring}"
- mint_repo_key_write_to "$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})."
+ fi
+
ok "Key installed."
}
+# -------------------------
+# 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
+ fi
+}
+
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 ${list}"
cat > "$list" < "$pref" <<'EOF'
-# Default: do NOT prefer Mint for everything.
Package: *
Pin: release o=LinuxMint
Pin-Priority: 100
-# Prefer Mint for Mint tooling
Package: mint* mintsources* mintupdate* mintsystem* mintstick* mintmenu* mintlocale* mintdrivers* mintreport* mintwelcome*
Pin: release o=LinuxMint
Pin-Priority: 700
-# Prefer Mint for Cinnamon/Nemo stack (apps)
+Package: mint* mintsources* mintupdate* mintsystem* mintstick* mintmenu* mintlocale* mintdrivers* mintreport* mintwelcome*
+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
-# Prefer Mint for XApp stack (critical deps for mintreport + nemo/cinnamon)
+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
-# Prefer Mint for Cinnamon/Nemo libraries that must match exact versions
+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*
+Pin: origin "packages.linuxmint.com"
+Pin-Priority: 1001
EOF
ok "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"
@@ -427,21 +662,34 @@ disable_thirdparty_sources_system() {
for f in /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; do
[[ "$(basename "$f")" == "official-package-repositories.list" ]] && continue
-
- # Preserve known security/VPN vendor repos
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
+ 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"
@@ -451,16 +699,16 @@ backup_system_state() {
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
+
+ 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
- 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"
}
@@ -468,97 +716,45 @@ backup_system_state() {
timeshift_snapshot_best_effort() {
if have_cmd timeshift; then
info "Timeshift detected. Attempting pre-change snapshot (best-effort)..."
- timeshift --create --comments "pre ubuntu->mint option-b $(date -Is)" --tags D || warn "Timeshift snapshot failed (may not be configured)."
+ 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
}
-# ---------------------------------------
-# PLAN MODE (UPDATED - SELF-SUFFICIENT)
-# ---------------------------------------
-apt_simulate_with_temp_sources() {
- # Plan mode: simulate using a temporary APT environment.
- # This does NOT modify system APT sources.
- # It keeps system trust/key config intact and only overrides the sources list + state/cache dirs.
+# -------------------------
+# 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
- local tmp
- tmp="$(mktemp -d)"
- mkdir -p \
- "$tmp/var/lib/apt/lists/partial" \
- "$tmp/var/cache/apt/archives/partial" \
- "$tmp/usr/share/keyrings"
+ DEBIAN_FRONTEND=noninteractive apt-get "$@" 2>&1 | tee "$outfile"
+ local rc="${PIPESTATUS[0]}"
- 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"
-
- detect_ubuntu_mirrors
-
- local sources="$tmp/sources.list"
- cat > "$sources" </dev/null
-
- rm -rf "$tmp"
- ok "Plan completed. Review: $plan_log"
+ 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
+ local removed removed_count
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}"
- 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 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
- if [[ "$p" =~ $critical_re ]]; then
- bad+="$p"$'\n'
- fi
+ [[ "$p" =~ $critical_re ]] && bad+="$p"$'\n'
done <<< "$removed"
if [[ -n "$bad" ]]; then
@@ -566,20 +762,435 @@ parse_and_guard_apt_actions() {
die "Refusing to proceed: simulation removes critical packages above."
fi
- if [[ "$removed_count" -gt 20 ]]; then
+ 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 user asked for Wayland, try to find a LightDM-compatible session FIRST.
+ # Note: Most Wayland sessions ship under /usr/share/wayland-sessions and are
+ # typically intended for GDM. Since this script standardizes on LightDM, we only
+ # select Wayland if it appears as an Xsession.
+ 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
+
+ # Default / safer: X11 session candidates
+ local -a candidates=()
+ case "$desired" in
+ cinnamon) candidates=(cinnamon cinnamon2d) ;;
+ mate) candidates=(mate) ;;
+ xfce) candidates=(xfce xfce4) ;;
+ *) candidates=("$desired") ;;
+ 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")"
+
+ if [[ "$PREFER_WAYLAND" == "yes" && "$session" != *wayland* ]]; then
+ warn "--prefer-wayland was set, but no LightDM-compatible Wayland session was found; using X11 session '${session}'."
+ fi
+
+ info "Staging defaults (apply fully after reboot): DM=${dm_pkg}, greeter=${greeter}, session=${session}"
+
+ DEBIAN_FRONTEND=noninteractive apt-get -y install "${dm_pkg}" "${greeter}"
+
+ 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"
+
+ mkdir -p /etc/X11
+ echo "${dm_path}" > /etc/X11/default-display-manager
+
+ 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
+ done
+ echo "lightdm shared/default-x-display-manager select lightdm" | debconf-set-selections || true
+ fi
+
+ mkdir -p /etc/lightdm/lightdm.conf.d
+ cat > /etc/lightdm/lightdm.conf.d/60-mint-defaults.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
+
+ ok "Defaults staged: lightdm + ${session}"
+}
+
+# -------------------------
+# 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; }
+
+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
+}
+
+post_convert_validate_or_die() {
+ local backup_dir="$1"
+ local report="${backup_dir}/post-convert-validation.txt"
+ : > "$report"
+
+ v() { echo "$*" | tee -a "$report" >/dev/null; }
+
+ local failed=0
+ local session
+ session="$(find_session_name_for_edition "$EDITION" "$PREFER_WAYLAND")"
+
+ 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"
+ else
+ v "FAIL: missing session desktop file: /usr/share/xsessions/${session}.desktop"
+ failed=1
+ fi
+
+ if [[ "$PREFER_WAYLAND" != "yes" && "$session" == *wayland* ]]; then
+ v "WARN: selected session appears to be Wayland despite X11 preference: ${session}"
+ 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
+ else
+ v "FAIL: missing /etc/X11/default-display-manager"
+ failed=1
+ 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() {
+ local tmp
+ tmp="$(mktemp -d)"
+ trap 'rm -rf "$tmp"' RETURN
+
+ 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"
+
+ detect_ubuntu_mirrors
+
+ cat > "$tmp/etc/apt/sources.list" < "$tmp/etc/apt/preferences.d/50-linuxmint-conversion.pref" <<'EOF'
+Package: *
+Pin: release o=LinuxMint
+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*
+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*
+Pin: origin "packages.linuxmint.com"
+Pin-Priority: 1001
+EOF
+
+ mkdir -p "$tmp/var/lib/apt/lists/partial" "$tmp/var/cache/apt/archives/partial"
+
+ 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 mint-meta-codecs mintsystem mintupdate mintsources)
+
+ 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
+
+ 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"
+}
+
+# -------------------------
+# Convert mode
+# -------------------------
convert_apply() {
[[ "$ACCEPT_RISK" == "yes" ]] || die "You must pass --i-accept-the-risk to run convert."
- preflight_common
+
+ # Disclaimer is required only for convert (per request)
+ require_unsupported_disclaimer
+
+ preflight_common "yes"
if [[ "$ASSUME_YES" != "yes" ]]; then
echo
- warn "This can break a corporate-managed machine. Ensure you have approval + a rollback plan (snapshot/backup)."
+ 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."
@@ -589,6 +1200,27 @@ convert_apply() {
backup_dir="$(backup_system_state)"
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
+
+ # Preseed display manager selection early (reduces DM prompt/flip-flops)
+ 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
@@ -605,10 +1237,14 @@ convert_apply() {
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 pkgs=()
+ local -a pkgs=()
case "$EDITION" in
cinnamon) pkgs+=(mint-meta-cinnamon) ;;
mate) pkgs+=(mint-meta-mate) ;;
@@ -618,46 +1254,80 @@ convert_apply() {
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
+ 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
- apt-get -y install \
- -o Dpkg::Options::=--force-confdef \
- -o Dpkg::Options::=--force-confold \
- $recommends \
- "${pkgs[@]}"
+
+ 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[@]}"
+ 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"
- apt-get -o Acquire::Retries=3 update
+ DEBIAN_FRONTEND=noninteractive apt-get -o Acquire::Retries=3 update
fi
fi
- ok "Conversion complete (Option B)."
+ 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."
- echo " 2) At the login screen, choose the '${EDITION^}' session."
- echo " 3) Validate corp software: VPN, EDR/agent, SSO, MDM posture, printers, smartcard, etc."
+ 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:-}"
+ 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"
- rm -rf /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
@@ -675,17 +1345,18 @@ rollback_apply() {
main() {
case "$MODE" in
doctor)
- preflight_common
+ preflight_common "$AUTO_FIX"
+ ok "Doctor completed."
;;
plan)
- preflight_common
+ preflight_common "$AUTO_FIX"
apt_simulate_with_temp_sources
;;
convert)
convert_apply
;;
rollback)
- rollback_apply "${2:-}"
+ rollback_apply "$ROLLBACK_DIR"
;;
*)
usage