Files
ubuntu2mint/ubuntu-to-mint-convert-v3.sh
T
LINUXexpert.org 5d1a7512f2 Enhance keyring handling for Mint repo signing key
Updated keyring handling to include fallback methods for fetching the Linux Mint repo signing key. Added support for both hkps and hkp keyservers, as well as HTTPS fallback.
2026-01-22 16:16:47 -07:00

655 lines
22 KiB
Bash

#!/usr/bin/env bash
# ubuntu-to-mint-convert-v3.sh
#
# Copyright (C) 2026 LINUXexpert.org
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program. If not, see <https://www.gnu.org/licenses/>.
#
# Keep Ubuntu as the base OS and add Linux Mint repos + Mint desktop/tooling.
# This is NOT an officially supported migration path. Use at your own risk.
#
# Supported paths (current supported Mint bases):
# - Ubuntu 24.04 (noble) -> Linux Mint 22.x (default: 22.3 "zena")
# - Ubuntu 22.04 (jammy) -> Linux Mint 21.x (default: 21.3 "virginia")
#
# Modes:
# doctor : run preflight checks only
# plan [opts] : dry-run apt simulation using temporary apt dirs (no system changes)
# convert --i-accept-the-risk [opts] : apply changes, install Mint packages
# rollback <backup_dir> : restore /etc/apt + sources from a backup created by this script
#
# Common options:
# --edition cinnamon|mate|xfce (default: cinnamon)
# --target zena|zara|xia|wilma|virginia|victoria|vera|vanessa (default depends on Ubuntu base)
# --mint-mirror URL (default: http://packages.linuxmint.com)
# --keep-ppas (default: PPAs/3rd-party sources are disabled during convert)
# --preserve-snap (default: yes; removes Mint's "nosnap" pin if it appears)
# --with-recommends (default: no; safer for corporate systems)
# --yes (skip interactive confirmation)
#
set -Eeuo pipefail
IFS=$'\n\t'
SCRIPT_VERSION="3.0"
LOG_DIR="/var/log/ubuntu-to-mint"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/ubuntu-to-mint-$(date +%Y%m%d-%H%M%S).log"
exec > >(tee -a "$LOG_FILE") 2>&1
RED=$'\033[31m'; YEL=$'\033[33m'; GRN=$'\033[32m'; BLU=$'\033[34m'; NC=$'\033[0m'
die() { echo "${RED}ERROR:${NC} $*" >&2; exit 1; }
warn(){ echo "${YEL}WARN:${NC} $*" >&2; }
info(){ echo "${BLU}INFO:${NC} $*"; }
ok() { echo "${GRN}OK:${NC} $*"; }
ON_ERROR_BACKUP_HINT=""
on_err() {
local line="${1:-?}" code="${2:-?}"
echo "${RED}FAILED${NC} at line ${line} (exit ${code})." >&2
echo "Command: ${BASH_COMMAND}" >&2
if [[ -n "${ON_ERROR_BACKUP_HINT}" ]]; then
echo "Rollback hint: ${ON_ERROR_BACKUP_HINT}" >&2
fi
echo "Log: ${LOG_FILE}" >&2
exit "$code"
}
trap 'on_err "$LINENO" "$?"' ERR
need_root() { [[ ${EUID:-$(id -u)} -eq 0 ]] || die "Run as root (use sudo)."; }
have_cmd() { command -v "$1" >/dev/null 2>&1; }
usage() {
cat <<'EOF'
Usage:
sudo bash ubuntu-to-mint-convert-v3.sh doctor
sudo bash ubuntu-to-mint-convert-v3.sh plan [--edition cinnamon|mate|xfce] [--target <mint_codename>] [--mint-mirror <url>] [--with-recommends]
sudo bash ubuntu-to-mint-convert-v3.sh convert --i-accept-the-risk [options...]
sudo bash ubuntu-to-mint-convert-v3.sh rollback /root/ubuntu-to-mint-backup-YYYYMMDD-HHMMSS
Options:
--edition cinnamon|mate|xfce
--target zena|zara|xia|wilma|virginia|victoria|vera|vanessa
--mint-mirror URL
--keep-ppas
--preserve-snap
--with-recommends
--yes
EOF
}
# --- Defaults ---
MODE="${1:-}"
EDITION="cinnamon"
TARGET_MINT=""
MINT_MIRROR="http://packages.linuxmint.com"
KEEP_PPAS="no"
PRESERVE_SNAP="yes"
WITH_RECOMMENDS="no"
ASSUME_YES="no"
# --- Parse args (simple) ---
shift || true
ACCEPT_RISK="no"
while [[ $# -gt 0 ]]; do
case "$1" in
--i-accept-the-risk) ACCEPT_RISK="yes"; shift;;
--edition) EDITION="${2:-}"; shift 2;;
--target) TARGET_MINT="${2:-}"; shift 2;;
--mint-mirror) MINT_MIRROR="${2:-}"; shift 2;;
--keep-ppas) KEEP_PPAS="yes"; shift;;
--preserve-snap) PRESERVE_SNAP="yes"; shift;;
--with-recommends) WITH_RECOMMENDS="yes"; shift;;
--yes) ASSUME_YES="yes"; shift;;
-h|--help) usage; exit 0;;
*) die "Unknown arg: $1 (use --help)";;
esac
done
validate_choice() {
case "$1" in
cinnamon|mate|xfce) ;;
*) die "Invalid --edition '$1' (use cinnamon|mate|xfce)";;
esac
}
read_os_release() {
[[ -r /etc/os-release ]] || die "/etc/os-release missing"
# shellcheck disable=SC1091
. /etc/os-release
OS_ID="${ID:-}"
OS_NAME="${NAME:-}"
OS_VERSION_CODENAME="${VERSION_CODENAME:-}"
OS_VERSION_ID="${VERSION_ID:-}"
}
detect_ubuntu_codename() {
local c=""
if have_cmd lsb_release; then
c="$(lsb_release -cs 2>/dev/null || true)"
fi
if [[ -z "$c" ]]; then
c="${OS_VERSION_CODENAME:-}"
fi
[[ -n "$c" ]] || die "Could not determine Ubuntu codename."
echo "$c"
}
set_targets_from_ubuntu() {
UBUNTU_CODENAME="$(detect_ubuntu_codename)"
case "$UBUNTU_CODENAME" in
noble)
UBUNTU_BASE="noble"
DEFAULT_MINT="zena" # Mint 22.3 Zena (Ubuntu Noble base)
ALLOWED_TARGETS=(zena zara xia wilma)
;;
jammy)
UBUNTU_BASE="jammy"
DEFAULT_MINT="virginia" # Mint 21.3 on Ubuntu Jammy base
ALLOWED_TARGETS=(virginia victoria vera vanessa)
;;
*)
die "Unsupported Ubuntu codename '$UBUNTU_CODENAME'. Supports Ubuntu noble (24.04) or jammy (22.04) only."
;;
esac
if [[ -z "$TARGET_MINT" ]]; then
TARGET_MINT="$DEFAULT_MINT"
fi
local ok_target="no"
for t in "${ALLOWED_TARGETS[@]}"; do
if [[ "$TARGET_MINT" == "$t" ]]; then
ok_target="yes"
break
fi
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
}
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 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"
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)
# -----------------------------
mint_repo_key_write_to() {
local out_keyring="$1"
local keyid="A6616109451BBBF2"
[[ -n "$out_keyring" ]] || die "mint_repo_key_write_to requires an output path"
DEBIAN_FRONTEND=noninteractive apt-get update -y
DEBIAN_FRONTEND=noninteractive apt-get install -y gnupg dirmngr ca-certificates curl
mkdir -p "$(dirname "$out_keyring")"
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
:
else
# 3) Fallback: fetch armored key over HTTPS/HTTP and dearmor
info "Keyserver blocked; fetching key over HTTPS from Ubuntu keyserver..."
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" \
|| die "Unable to fetch Mint repo key via keyserver or HTTPS fallback."
fi
# Basic safety check: ensure the fetched key contains the expected key id (last 16 hex of fingerprint)
local fpr_last16
fpr_last16="$(gpg --batch --with-colons --show-keys "$armored" | awk -F: '$1=="fpr"{print $10}' | tail -n1 | tail -c 17 | tr -d '\n' | tr '[:lower:]' '[:upper:]')"
[[ "$fpr_last16" == "${keyid^^}" ]] || die "Fetched key fingerprint suffix mismatch (expected ${keyid^^}, got ${fpr_last16:-<none>})."
gpg --batch --dearmor -o "$out_keyring" "$armored"
chmod 644 "$out_keyring"
rm -rf "$gnupghome"
return 0
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 "$out_keyring"
rm -rf "$gnupghome"
}
mint_repo_key_install() {
local keyring="/usr/share/keyrings/linuxmint-repo.gpg"
info "Installing Linux Mint repo signing key into ${keyring}"
mint_repo_key_write_to "$keyring"
ok "Key installed."
}
write_mint_sources_system() {
local list="/etc/apt/sources.list.d/official-package-repositories.list"
local keyring="/usr/share/keyrings/linuxmint-repo.gpg"
detect_ubuntu_mirrors
info "Writing Mint+Ubuntu sources to ${list}"
cat > "$list" <<EOF
# Do not edit this file manually.
# Generated by ubuntu-to-mint-convert-v3.sh on $(date -Is)
#
# Linux Mint repository:
deb [signed-by=${keyring}] ${MINT_MIRROR%/} ${TARGET_MINT} main upstream import backport
# Ubuntu base repositories:
deb ${UBUNTU_ARCHIVE_MIRROR%/} ${UBUNTU_BASE} main restricted universe multiverse
deb ${UBUNTU_ARCHIVE_MIRROR%/} ${UBUNTU_BASE}-updates main restricted universe multiverse
deb ${UBUNTU_ARCHIVE_MIRROR%/} ${UBUNTU_BASE}-backports main restricted universe multiverse
deb ${UBUNTU_SECURITY_MIRROR%/} ${UBUNTU_BASE}-security main restricted universe multiverse
EOF
if [[ -f /etc/apt/sources.list ]]; then
sed -i 's/^[[:space:]]*deb /# deb /' /etc/apt/sources.list || true
fi
ok "Sources written."
}
write_mint_pinning_system() {
local pref="/etc/apt/preferences.d/50-linuxmint-conversion.pref"
info "Writing conservative APT pinning to ${pref}"
cat > "$pref" <<'EOF'
Package: *
Pin: origin "packages.linuxmint.com"
Pin-Priority: 100
Package: mint* mintsources* mintupdate* mintsystem* mintstick* mintmenu* mintlocale* mintdrivers* mintreport* mintwelcome*
Pin: origin "packages.linuxmint.com"
Pin-Priority: 700
Package: cinnamon* nemo* muffin* cjs* xapp* slick-greeter* lightdm* pix* xviewer* mint-themes* mint-y-icons* mint-x-icons*
Pin: origin "packages.linuxmint.com"
Pin-Priority: 700
EOF
ok "Pinning written."
}
disable_thirdparty_sources_system() {
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")" == "official-package-repositories.list" ]] && continue
mv -v "$f" "${disabled_dir}/" || true
done
shopt -u nullglob
ok "Third-party sources disabled (restorable via rollback)."
}
backup_system_state() {
local backup_dir="/root/ubuntu-to-mint-backup-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$backup_dir"
ON_ERROR_BACKUP_HINT="sudo bash $0 rollback ${backup_dir}"
info "Creating backup at: ${backup_dir}"
mkdir -p "${backup_dir}/etc"
cp -a /etc/apt "${backup_dir}/etc/" || true
cp -a /etc/os-release /etc/lsb-release 2>/dev/null "${backup_dir}/etc/" || true
cp -a /etc/fstab /etc/hostname /etc/hosts 2>/dev/null "${backup_dir}/etc/" || true
dpkg-query -W -f='${Package}\t${Version}\n' > "${backup_dir}/dpkg-packages.tsv" || true
apt-mark showmanual > "${backup_dir}/apt-manual.txt" || true
apt-mark showhold > "${backup_dir}/apt-holds.txt" || true
systemctl list-unit-files --state=enabled > "${backup_dir}/enabled-services.txt" || true
if have_cmd snap; then snap list > "${backup_dir}/snap-list.txt" || true; fi
if have_cmd flatpak; then flatpak list > "${backup_dir}/flatpak-list.txt" || true; fi
ok "Backup complete."
echo "$backup_dir"
}
timeshift_snapshot_best_effort() {
if have_cmd timeshift; then
info "Timeshift detected. Attempting pre-change snapshot (best-effort)..."
timeshift --create --comments "pre ubuntu->mint option-b $(date -Is)" --tags D || warn "Timeshift snapshot failed (may not be configured)."
else
warn "Timeshift not installed. Strongly recommended to snapshot/backup before converting."
fi
}
# ---------------------------------------
# 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.
local tmp
tmp="$(mktemp -d)"
mkdir -p \
"$tmp/var/lib/apt/lists/partial" \
"$tmp/var/cache/apt/archives/partial" \
"$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"
detect_ubuntu_mirrors
local sources="$tmp/sources.list"
cat > "$sources" <<EOF
deb [signed-by=${temp_keyring}] ${MINT_MIRROR%/} ${TARGET_MINT} main upstream import backport
deb ${UBUNTU_ARCHIVE_MIRROR%/} ${UBUNTU_BASE} main restricted universe multiverse
deb ${UBUNTU_ARCHIVE_MIRROR%/} ${UBUNTU_BASE}-updates main restricted universe multiverse
deb ${UBUNTU_ARCHIVE_MIRROR%/} ${UBUNTU_BASE}-backports main restricted universe multiverse
deb ${UBUNTU_SECURITY_MIRROR%/} ${UBUNTU_BASE}-security main restricted universe multiverse
EOF
local recommends="--no-install-recommends"
[[ "$WITH_RECOMMENDS" == "yes" ]] && recommends=""
local pkgs=()
case "$EDITION" in
cinnamon) pkgs+=(mint-meta-cinnamon) ;;
mate) pkgs+=(mint-meta-mate) ;;
xfce) pkgs+=(mint-meta-xfce) ;;
esac
pkgs+=(mint-meta-core mint-meta-codecs mintsystem mintupdate mintsources)
info "Plan mode: apt update (temporary state/cache + temporary sources list)..."
apt-get \
-o Dir::Etc::sourcelist="$sources" \
-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
info "Plan mode: simulated install: ${pkgs[*]}"
local plan_log="$LOG_DIR/plan-$(date +%Y%m%d-%H%M%S).txt"
apt-get \
-o Dir::Etc::sourcelist="$sources" \
-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[@]}" | tee "$plan_log" >/dev/null
rm -rf "$tmp"
ok "Plan completed. Review: $plan_log"
}
parse_and_guard_apt_actions() {
local sim_output_file="$1"
[[ -r "$sim_output_file" ]] || die "Missing simulation output: $sim_output_file"
local removed
removed="$(grep -E '^Remv ' "$sim_output_file" | awk '{print $2}' || true)"
local removed_count
removed_count="$(echo "$removed" | grep -c . || true)"
info "APT simulation: packages marked for removal: ${removed_count}"
local critical_re='^(sudo|openssh-server|ssh|network-manager|systemd|systemd-sysv|dbus|polkit|linux-image|linux-generic|linux-modules|grub|grub2|initramfs-tools|libc6|libstdc\+\+6|snapd)$'
local bad=""
while IFS= read -r p; do
[[ -z "$p" ]] && continue
if [[ "$p" =~ $critical_re ]]; then
bad+="$p"$'\n'
fi
done <<< "$removed"
if [[ -n "$bad" ]]; then
echo "$bad" | sed 's/^/ - /'
die "Refusing to proceed: simulation removes critical packages above."
fi
if [[ "$removed_count" -gt 20 ]]; then
die "Refusing to proceed: too many removals (${removed_count}). Inspect conflicts."
fi
ok "Guard rails passed."
}
convert_apply() {
[[ "$ACCEPT_RISK" == "yes" ]] || die "You must pass --i-accept-the-risk to run convert."
preflight_common
if [[ "$ASSUME_YES" != "yes" ]]; then
echo
warn "This can break a corporate-managed machine. Ensure you have approval + a rollback plan (snapshot/backup)."
echo "Target: Ubuntu ${UBUNTU_BASE} -> Mint ${TARGET_MINT} (${EDITION})"
read -r -p "Type 'I UNDERSTAND' to continue: " ans
[[ "$ans" == "I UNDERSTAND" ]] || die "Aborted by user."
fi
local backup_dir
backup_dir="$(backup_system_state)"
timeshift_snapshot_best_effort
systemctl stop apt-daily.service apt-daily-upgrade.service 2>/dev/null || true
systemctl kill --kill-who=all apt-daily.service apt-daily-upgrade.service 2>/dev/null || true
if [[ "$KEEP_PPAS" != "yes" ]]; then
disable_thirdparty_sources_system "$backup_dir"
else
warn "--keep-ppas set. Proceeding with third-party sources enabled may increase conflict risk."
fi
mint_repo_key_install
write_mint_sources_system
write_mint_pinning_system
info "APT update..."
DEBIAN_FRONTEND=noninteractive apt-get -o Acquire::Retries=3 update
local recommends="--no-install-recommends"
[[ "$WITH_RECOMMENDS" == "yes" ]] && recommends=""
local pkgs=()
case "$EDITION" in
cinnamon) pkgs+=(mint-meta-cinnamon) ;;
mate) pkgs+=(mint-meta-mate) ;;
xfce) pkgs+=(mint-meta-xfce) ;;
esac
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"
DEBIAN_FRONTEND=noninteractive apt-get -s install $recommends "${pkgs[@]}" | tee "$sim_out" >/dev/null
parse_and_guard_apt_actions "$sim_out"
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 [[ "$PRESERVE_SNAP" == "yes" ]]; then
local nosnap="/etc/apt/preferences.d/nosnap.pref"
if [[ -f "$nosnap" ]]; then
warn "Mint 'nosnap' preference detected at ${nosnap}. Removing to preserve snap functionality."
rm -f "$nosnap"
apt-get -o Acquire::Retries=3 update
fi
fi
ok "Conversion complete (Option B)."
info "Backup dir: ${backup_dir}"
info "Log file: ${LOG_FILE}"
echo
echo "Next steps:"
echo " 1) Reboot."
echo " 2) At the login screen, choose the '${EDITION^}' session."
echo " 3) Validate corp software: VPN, EDR/agent, SSO, MDM posture, printers, smartcard, etc."
}
rollback_apply() {
need_root
local backup_dir="${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"
info "Restoring /etc/apt from backup: ${backup_dir}/etc/apt"
rm -rf /etc/apt
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() {
case "$MODE" in
doctor)
preflight_common
;;
plan)
preflight_common
apt_simulate_with_temp_sources
;;
convert)
convert_apply
;;
rollback)
rollback_apply "${2:-}"
;;
*)
usage
exit 1
;;
esac
}
main "$@"