Updated script version to 4.8 and added LightDM configuration for boot. Cleaned up help messages and ensured proper handling of display managers.
902 lines
28 KiB
Bash
902 lines
28 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/>.
|
|
|
|
set -euo pipefail
|
|
|
|
# =========================
|
|
# Version
|
|
# =========================
|
|
SCRIPT_VERSION="4.8"
|
|
|
|
# =========================
|
|
# Globals / Defaults
|
|
# =========================
|
|
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
|
|
|
|
# CLI defaults
|
|
CMD=""
|
|
EDITION="cinnamon"
|
|
TARGET_MINT=""
|
|
MINT_MIRROR="http://packages.linuxmint.com"
|
|
KEEP_PPAS="no"
|
|
PRESERVE_SNAP="yes"
|
|
WITH_RECOMMENDS="no"
|
|
ASSUME_YES="no"
|
|
I_ACCEPT_RISK="no"
|
|
ROLLBACK_DIR=""
|
|
|
|
# OS detection
|
|
OS_ID=""
|
|
OS_VERSION_ID=""
|
|
OS_CODENAME=""
|
|
UBUNTU_BASE=""
|
|
DEFAULT_MINT=""
|
|
ALLOWED_TARGETS=()
|
|
|
|
# Key handling
|
|
MINT_KEYID="A6616109451BBBF2"
|
|
SYSTEM_KEYRING="/usr/share/keyrings/linuxmint-repo.gpg"
|
|
|
|
# Error handling
|
|
ON_ERROR_BACKUP_HINT=""
|
|
|
|
# =========================
|
|
# Pretty output
|
|
# =========================
|
|
is_tty() { [[ -t 1 ]]; }
|
|
|
|
if is_tty && command -v tput >/dev/null 2>&1; then
|
|
RED="$(tput setaf 1)"
|
|
GREEN="$(tput setaf 2)"
|
|
YELLOW="$(tput setaf 3)"
|
|
BLUE="$(tput setaf 4)"
|
|
BOLD="$(tput bold)"
|
|
RESET="$(tput sgr0)"
|
|
else
|
|
RED=""; GREEN=""; YELLOW=""; BLUE=""; BOLD=""; RESET=""
|
|
fi
|
|
|
|
info() { echo "${BLUE}INFO:${RESET} $*"; }
|
|
ok() { echo "${GREEN}OK:${RESET} $*"; }
|
|
warn() { echo "${YELLOW}WARN:${RESET} $*"; }
|
|
die() { echo "${RED}ERROR:${RESET} $*"; [[ -n "${ON_ERROR_BACKUP_HINT:-}" ]] && echo "${YELLOW}${ON_ERROR_BACKUP_HINT}${RESET}"; exit 1; }
|
|
|
|
have_cmd() { command -v "$1" >/dev/null 2>&1; }
|
|
|
|
need_root() {
|
|
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
|
|
die "This must be run as root. Use: sudo bash $0 ..."
|
|
fi
|
|
}
|
|
|
|
# =========================
|
|
# Usage
|
|
# =========================
|
|
usage() {
|
|
cat <<EOF
|
|
ubuntu-to-mint-convert-v3.sh (v${SCRIPT_VERSION})
|
|
|
|
Usage (command-first):
|
|
sudo bash $0 doctor [options]
|
|
sudo bash $0 plan [options]
|
|
sudo bash $0 convert --i-accept-the-risk [options]
|
|
sudo bash $0 rollback /root/ubuntu-to-mint-backup-YYYYMMDD-HHMMSS
|
|
|
|
Usage (options-first ALSO supported in v${SCRIPT_VERSION}):
|
|
sudo bash $0 [options] doctor
|
|
sudo bash $0 [options] plan
|
|
sudo bash $0 [options] convert --i-accept-the-risk
|
|
sudo bash $0 rollback /path/to/backup
|
|
|
|
Options:
|
|
--edition cinnamon|mate|xfce (default: cinnamon)
|
|
--target <mint_codename> Override Mint codename for your Ubuntu base
|
|
--mint-mirror <url> (default: http://packages.linuxmint.com)
|
|
--keep-ppas Do not disable third-party sources (not recommended)
|
|
--preserve-snap Keep snapd (default: enabled)
|
|
--with-recommends Allow recommended packages (default: off)
|
|
--yes Non-interactive / auto-confirm
|
|
--i-accept-the-risk Required for convert
|
|
EOF
|
|
}
|
|
|
|
# =========================
|
|
# APT / dpkg sanity
|
|
# =========================
|
|
ensure_no_apt_locks() {
|
|
local locks=(/var/lib/dpkg/lock-frontend /var/lib/dpkg/lock /var/lib/apt/lists/lock /var/cache/apt/archives/lock)
|
|
for l in "${locks[@]}"; do
|
|
if fuser "$l" >/dev/null 2>&1; then
|
|
die "APT/dpkg lock is held (lock file: $l). Close package managers and try again."
|
|
fi
|
|
done
|
|
}
|
|
|
|
apt_fix_broken() {
|
|
info "Attempting basic dpkg/apt remediation (best-effort)..."
|
|
DEBIAN_FRONTEND=noninteractive dpkg --configure -a || true
|
|
DEBIAN_FRONTEND=noninteractive apt-get -y -f install || true
|
|
}
|
|
|
|
apt_opts_common() {
|
|
local -a opts
|
|
opts=(-y -o Dpkg::Use-Pty=0 -o Acquire::Retries=3)
|
|
if [[ "$WITH_RECOMMENDS" != "yes" ]]; then
|
|
opts+=(--no-install-recommends)
|
|
fi
|
|
echo "${opts[@]}"
|
|
}
|
|
|
|
# =========================
|
|
# OS Detection
|
|
# =========================
|
|
detect_os() {
|
|
[[ -r /etc/os-release ]] || die "Cannot read /etc/os-release"
|
|
# shellcheck disable=SC1091
|
|
. /etc/os-release
|
|
|
|
OS_ID="${ID:-}"
|
|
OS_VERSION_ID="${VERSION_ID:-}"
|
|
OS_CODENAME="${VERSION_CODENAME:-${UBUNTU_CODENAME:-}}"
|
|
|
|
info "Script v${SCRIPT_VERSION}"
|
|
info "Log: ${LOG_FILE}"
|
|
info "Detected OS: ${NAME:-unknown} (ID=${OS_ID}, VERSION_ID=${OS_VERSION_ID}, CODENAME=${OS_CODENAME})"
|
|
|
|
[[ "$OS_ID" == "ubuntu" ]] || die "Unsupported OS ID '${OS_ID}'. This script supports Ubuntu bases only."
|
|
|
|
case "$OS_CODENAME" in
|
|
noble)
|
|
UBUNTU_BASE="noble"
|
|
DEFAULT_MINT="zena"
|
|
ALLOWED_TARGETS=(zena zara xia wilma)
|
|
;;
|
|
jammy)
|
|
UBUNTU_BASE="jammy"
|
|
DEFAULT_MINT="virginia"
|
|
ALLOWED_TARGETS=(virginia victoria vera vanessa)
|
|
;;
|
|
*)
|
|
die "Unsupported Ubuntu codename '${OS_CODENAME}'. Supported: noble (24.04), jammy (22.04)."
|
|
;;
|
|
esac
|
|
|
|
if [[ -z "$TARGET_MINT" ]]; then
|
|
TARGET_MINT="$DEFAULT_MINT"
|
|
fi
|
|
|
|
TARGET_MINT="${TARGET_MINT,,}"
|
|
EDITION="${EDITION,,}"
|
|
|
|
case "$EDITION" in
|
|
cinnamon|mate|xfce) : ;;
|
|
*) die "Unsupported --edition '${EDITION}'. Allowed: cinnamon|mate|xfce" ;;
|
|
esac
|
|
|
|
local found="no"
|
|
for t in "${ALLOWED_TARGETS[@]}"; do
|
|
if [[ "$t" == "$TARGET_MINT" ]]; then found="yes"; break; fi
|
|
done
|
|
[[ "$found" == "yes" ]] || die "--target '${TARGET_MINT}' not allowed for Ubuntu '${OS_CODENAME}'. Allowed: ${ALLOWED_TARGETS[*]}"
|
|
|
|
info "Ubuntu base: ${UBUNTU_BASE} | Target Mint codename: ${TARGET_MINT} | Edition: ${EDITION}"
|
|
}
|
|
|
|
# =========================
|
|
# Backup / Restore
|
|
# =========================
|
|
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="Rollback suggestion: 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
|
|
cp -a /etc/lightdm 2>/dev/null "${backup_dir}/etc/" || true
|
|
cp -a /etc/X11/default-display-manager 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"
|
|
}
|
|
|
|
rollback() {
|
|
need_root
|
|
local dir="${1:-}"
|
|
[[ -n "$dir" ]] || die "rollback requires a backup directory argument"
|
|
[[ -d "$dir" ]] || die "backup directory not found: $dir"
|
|
[[ -d "$dir/etc/apt" ]] || die "backup does not contain etc/apt: $dir/etc/apt"
|
|
|
|
info "Restoring /etc/apt from: $dir/etc/apt"
|
|
rm -rf /etc/apt
|
|
cp -a "$dir/etc/apt" /etc/apt
|
|
|
|
if [[ -d "$dir/disabled-sources" ]]; then
|
|
info "Restoring disabled third-party sources from backup..."
|
|
mkdir -p /etc/apt/sources.list.d
|
|
cp -a "$dir/disabled-sources/"* /etc/apt/sources.list.d/ 2>/dev/null || true
|
|
fi
|
|
|
|
ok "Rollback APT config restored."
|
|
info "Now run:"
|
|
echo " sudo apt-get update"
|
|
echo " sudo apt-get -f install"
|
|
}
|
|
|
|
# =========================
|
|
# Timeshift snapshot
|
|
# =========================
|
|
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
|
|
}
|
|
|
|
# =========================
|
|
# Mint repo key bootstrap (no keyservers)
|
|
# =========================
|
|
gpg_key_file_has_keyid() {
|
|
local f="$1"
|
|
local keyid="$2"
|
|
gpg --batch --with-colons --show-keys "$f" 2>/dev/null | awk -F: '$1=="pub"||$1=="sub"{print toupper($5)}' | grep -qx "$(echo "$keyid" | tr '[:lower:]' '[:upper:]')"
|
|
}
|
|
|
|
mint_keyring_build_from_linuxmint_keyring_deb() {
|
|
local out_keyring="$1"
|
|
[[ -n "$out_keyring" ]] || die "mint_keyring_build_from_linuxmint_keyring_deb: missing output path"
|
|
|
|
DEBIAN_FRONTEND=noninteractive apt-get update -y
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates curl gnupg dpkg-dev >/dev/null
|
|
|
|
local tmpdir
|
|
tmpdir="$(mktemp -d)"
|
|
chmod 700 "$tmpdir"
|
|
|
|
local pool_https="https://packages.linuxmint.com/pool/main/l/linuxmint-keyring/"
|
|
local pool_http="http://packages.linuxmint.com/pool/main/l/linuxmint-keyring/"
|
|
local index=""
|
|
|
|
info "Bootstrapping Mint keyring from linuxmint-keyring (.deb) pool..."
|
|
if index="$(curl -fsSL "$pool_https" 2>/dev/null)"; then
|
|
:
|
|
elif index="$(curl -fsSL "$pool_http" 2>/dev/null)"; then
|
|
:
|
|
else
|
|
rm -rf "$tmpdir"
|
|
die "Unable to fetch linuxmint-keyring pool index (blocked network/proxy?)"
|
|
fi
|
|
|
|
local debs
|
|
debs="$(printf '%s' "$index" | grep -oE 'linuxmint-keyring_[0-9][^"]*_all\.deb' | sort -Vu | uniq || true)"
|
|
[[ -n "$debs" ]] || { rm -rf "$tmpdir"; die "Could not find linuxmint-keyring_*.deb in pool index." ; }
|
|
|
|
local deb
|
|
deb="$(printf '%s\n' "$debs" | tail -n 1)"
|
|
info "Selected linuxmint-keyring package: $deb"
|
|
|
|
local deb_url=""
|
|
if curl -fsI "${pool_https}${deb}" >/dev/null 2>&1; then
|
|
deb_url="${pool_https}${deb}"
|
|
else
|
|
deb_url="${pool_http}${deb}"
|
|
fi
|
|
|
|
curl -fSL "$deb_url" -o "${tmpdir}/${deb}" || { rm -rf "$tmpdir"; die "Failed to download ${deb_url}"; }
|
|
|
|
mkdir -p "${tmpdir}/extract"
|
|
dpkg-deb -x "${tmpdir}/${deb}" "${tmpdir}/extract"
|
|
|
|
local -a candidates=()
|
|
while IFS= read -r f; do candidates+=("$f"); done < <(find "${tmpdir}/extract" -type f \( -name '*.gpg' -o -name '*.asc' -o -name '*.key' \) 2>/dev/null | sort)
|
|
|
|
[[ ${#candidates[@]} -gt 0 ]] || { rm -rf "$tmpdir"; die "No key candidates found inside linuxmint-keyring deb." ; }
|
|
|
|
local chosen=""
|
|
for f in "${candidates[@]}"; do
|
|
if gpg_key_file_has_keyid "$f" "$MINT_KEYID"; then
|
|
chosen="$f"
|
|
break
|
|
fi
|
|
done
|
|
|
|
[[ -n "$chosen" ]] || {
|
|
warn "Candidates found:"
|
|
printf ' - %s\n' "${candidates[@]}" || true
|
|
rm -rf "$tmpdir"
|
|
die "None of the candidate key files contained keyid ${MINT_KEYID}."
|
|
}
|
|
|
|
mkdir -p "$(dirname "$out_keyring")"
|
|
|
|
local tmp_out
|
|
tmp_out="$(mktemp "${tmpdir}/keyring.XXXXXX")"
|
|
rm -f "$tmp_out"
|
|
|
|
info "Using key candidate: $chosen"
|
|
if [[ "$chosen" == *.asc || "$chosen" == *.key ]]; then
|
|
grep -q "BEGIN PGP PUBLIC KEY BLOCK" "$chosen" 2>/dev/null || { rm -rf "$tmpdir"; die "Selected key candidate is not armored PGP: $chosen"; }
|
|
gpg --batch --dearmor -o "$tmp_out" "$chosen" || { rm -rf "$tmpdir"; die "gpg dearmor failed for key candidate"; }
|
|
else
|
|
cp -a "$chosen" "$tmp_out"
|
|
fi
|
|
|
|
gpg_key_file_has_keyid "$tmp_out" "$MINT_KEYID" || { rm -rf "$tmpdir"; die "Built keyring does not contain ${MINT_KEYID}"; }
|
|
|
|
rm -f "$out_keyring"
|
|
install -m 0644 "$tmp_out" "$out_keyring"
|
|
|
|
ok "Mint repo keyring written: $out_keyring (contains ${MINT_KEYID})"
|
|
rm -rf "$tmpdir"
|
|
}
|
|
|
|
mint_repo_key_install_system() {
|
|
local keyring="$SYSTEM_KEYRING"
|
|
info "Installing Linux Mint repo signing key into ${keyring}"
|
|
|
|
if [[ -f "$keyring" ]]; then
|
|
if gpg_key_file_has_keyid "$keyring" "$MINT_KEYID"; then
|
|
ok "Keyring already exists and contains ${MINT_KEYID}."
|
|
return 0
|
|
fi
|
|
|
|
if [[ "$ASSUME_YES" == "yes" ]]; then
|
|
warn "Existing keyring does not contain expected key; overwriting due to --yes."
|
|
rm -f "$keyring"
|
|
else
|
|
warn "Existing keyring at ${keyring} does not contain expected key ${MINT_KEYID}."
|
|
warn "Re-run with --yes to overwrite automatically, or delete it manually and re-run."
|
|
die "Keyring mismatch; refusing to proceed without explicit overwrite."
|
|
fi
|
|
fi
|
|
|
|
mint_keyring_build_from_linuxmint_keyring_deb "$keyring"
|
|
}
|
|
|
|
# =========================
|
|
# APT sources + pinning
|
|
# =========================
|
|
detect_ubuntu_mirrors() {
|
|
UBUNTU_ARCHIVE_MIRROR="http://archive.ubuntu.com/ubuntu"
|
|
UBUNTU_SECURITY_MIRROR="http://security.ubuntu.com/ubuntu"
|
|
|
|
local first_archive=""
|
|
first_archive="$(grep -RhoE 'deb (http|https)://[^ ]+/ubuntu' /etc/apt/sources.list /etc/apt/sources.list.d/*.list 2>/dev/null | head -n1 | awk '{print $2}' || true)"
|
|
if [[ -n "$first_archive" ]]; then
|
|
UBUNTU_ARCHIVE_MIRROR="$first_archive"
|
|
fi
|
|
|
|
local first_security=""
|
|
first_security="$(grep -RhoE 'deb (http|https)://security\.ubuntu\.com/ubuntu' /etc/apt/sources.list /etc/apt/sources.list.d/*.list 2>/dev/null | head -n1 | awk '{print $2}' || true)"
|
|
if [[ -n "$first_security" ]]; then
|
|
UBUNTU_SECURITY_MIRROR="$first_security"
|
|
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="$SYSTEM_KEYRING"
|
|
|
|
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
|
|
sed -i 's/^[[:space:]]*deb-src /# deb-src /' /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 APT pinning to ${pref}"
|
|
|
|
cat > "$pref" <<'EOF'
|
|
Package: *
|
|
Pin: origin "packages.linuxmint.com"
|
|
Pin-Priority: 500
|
|
|
|
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* xapps* 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"
|
|
|
|
if [[ "$KEEP_PPAS" == "yes" ]]; then
|
|
warn "--keep-ppas set; leaving third-party sources enabled."
|
|
return 0
|
|
fi
|
|
|
|
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)."
|
|
}
|
|
|
|
# =========================
|
|
# LightDM / Session defaults
|
|
# =========================
|
|
session_name_for_edition() {
|
|
case "$EDITION" in
|
|
cinnamon) echo "cinnamon";;
|
|
mate) echo "mate";;
|
|
xfce) echo "xfce";;
|
|
*) echo "cinnamon";;
|
|
esac
|
|
}
|
|
|
|
ensure_lightdm_on_boot() {
|
|
# Make LightDM the default display manager and ensure graphical boot.
|
|
if [[ ! -d /run/systemd/system ]]; then
|
|
warn "systemd not detected; cannot enable LightDM on boot in this environment."
|
|
return 0
|
|
fi
|
|
|
|
info "Ensuring LightDM starts on boot (graphical.target + default display manager)..."
|
|
|
|
# Unmask LightDM in case something previously masked it
|
|
systemctl unmask lightdm >/dev/null 2>&1 || true
|
|
|
|
# Ensure the default DM marker is LightDM
|
|
echo "/usr/sbin/lightdm" > /etc/X11/default-display-manager || true
|
|
|
|
# Debian/Ubuntu DM selection often uses update-alternatives
|
|
if have_cmd update-alternatives; then
|
|
update-alternatives --set x-display-manager /usr/sbin/lightdm >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
# Ensure graphical boot target
|
|
systemctl set-default graphical.target >/dev/null 2>&1 || true
|
|
systemctl enable graphical.target >/dev/null 2>&1 || true
|
|
|
|
# Enable LightDM for boot
|
|
systemctl enable lightdm >/dev/null 2>&1 || true
|
|
|
|
# Prevent GDM from stealing the greeter
|
|
if systemctl list-unit-files | awk '{print $1}' | grep -qx 'gdm3.service'; then
|
|
systemctl disable --now gdm3 >/dev/null 2>&1 || true
|
|
systemctl mask gdm3 >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
# Restart the active DM if we can
|
|
systemctl restart lightdm >/dev/null 2>&1 || true
|
|
|
|
ok "LightDM configured to start on boot."
|
|
}
|
|
|
|
ensure_lightdm_defaults() {
|
|
local sess
|
|
sess="$(session_name_for_edition)"
|
|
|
|
info "Configuring LightDM defaults for edition=${EDITION} (session=${sess})"
|
|
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y lightdm slick-greeter
|
|
|
|
# Keep Ubuntu's gdm3 installed (if present) but ensure it can't win.
|
|
if systemctl is-enabled gdm3 >/dev/null 2>&1; then
|
|
warn "Disabling gdm3 (keeping installed)."
|
|
systemctl disable --now gdm3 || true
|
|
systemctl mask gdm3 || true
|
|
if [[ -f /etc/gdm3/custom.conf ]]; then
|
|
sed -i 's/^[#[:space:]]*WaylandEnable=.*/WaylandEnable=false/' /etc/gdm3/custom.conf || true
|
|
grep -q '^WaylandEnable=' /etc/gdm3/custom.conf || echo 'WaylandEnable=false' >> /etc/gdm3/custom.conf
|
|
fi
|
|
fi
|
|
|
|
mkdir -p /etc/lightdm/lightdm.conf.d
|
|
shopt -s nullglob
|
|
for f in /etc/lightdm/lightdm.conf.d/*.conf; do
|
|
[[ "$(basename "$f")" == "99-ubuntu2mint.conf" ]] && continue
|
|
if grep -qE '^[[:space:]]*user-session=' "$f" 2>/dev/null; then
|
|
warn "Found existing user-session setting in $f; commenting it out (keeping file)."
|
|
sed -i 's/^[[:space:]]*user-session=/# user-session=/' "$f" || true
|
|
fi
|
|
done
|
|
shopt -u nullglob
|
|
|
|
cat > /etc/lightdm/lightdm.conf.d/99-ubuntu2mint.conf <<EOF
|
|
# Generated by ubuntu-to-mint-convert-v3.sh on $(date -Is)
|
|
[Seat:*]
|
|
greeter-session=slick-greeter
|
|
user-session=${sess}
|
|
EOF
|
|
|
|
# Set default session for users
|
|
for homedir in /home/*; do
|
|
[[ -d "$homedir" ]] || continue
|
|
local user
|
|
user="$(basename "$homedir")"
|
|
[[ "$user" == "lost+found" ]] && continue
|
|
|
|
local dmrc="${homedir}/.dmrc"
|
|
if [[ ! -f "$dmrc" ]]; then
|
|
cat > "$dmrc" <<EOF
|
|
[Desktop]
|
|
Session=${sess}
|
|
EOF
|
|
chown "${user}:${user}" "$dmrc" 2>/dev/null || true
|
|
chmod 644 "$dmrc" || true
|
|
continue
|
|
fi
|
|
|
|
if grep -q '^\[Desktop\]' "$dmrc"; then
|
|
if grep -q '^Session=' "$dmrc"; then
|
|
sed -i "s/^Session=.*/Session=${sess}/" "$dmrc" || true
|
|
else
|
|
sed -i "/^\[Desktop\]/a Session=${sess}" "$dmrc" || true
|
|
fi
|
|
else
|
|
printf "\n[Desktop]\nSession=%s\n" "$sess" >> "$dmrc"
|
|
fi
|
|
chown "${user}:${user}" "$dmrc" 2>/dev/null || true
|
|
chmod 644 "$dmrc" || true
|
|
done
|
|
|
|
# This is the baked-in boot behavior you asked for:
|
|
ensure_lightdm_on_boot
|
|
|
|
ok "LightDM defaults applied; default session set to ${sess}."
|
|
}
|
|
|
|
# =========================
|
|
# Mintupdate icon conflict mitigation
|
|
# =========================
|
|
apply_mintupdate_icon_diversion() {
|
|
local f="/usr/share/icons/hicolor/16x16/apps/software-properties.png"
|
|
if [[ -f "$f" ]]; then
|
|
if dpkg -S "$f" 2>/dev/null | grep -q '^software-properties-gtk:'; then
|
|
if ! dpkg-divert --list "$f" 2>/dev/null | grep -q "$f"; then
|
|
warn "Applying dpkg-divert to avoid mintupdate vs software-properties-gtk file conflict: $f"
|
|
dpkg-divert --package ubuntu2mint --add --rename --divert "${f}.ubuntu2mint" "$f" || true
|
|
fi
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# =========================
|
|
# Post-install sanity checks
|
|
# =========================
|
|
post_install_sanity() {
|
|
local out_file="$1"
|
|
[[ -n "$out_file" ]] || out_file="/tmp/post-convert-validation.txt"
|
|
|
|
{
|
|
echo "Post-conversion validation ($(date -Is))"
|
|
echo "======================================"
|
|
echo
|
|
echo "OS release:"
|
|
cat /etc/os-release || true
|
|
echo
|
|
echo "Keyring contains ${MINT_KEYID}:"
|
|
if [[ -f "$SYSTEM_KEYRING" ]] && gpg_key_file_has_keyid "$SYSTEM_KEYRING" "$MINT_KEYID"; then
|
|
echo "OK"
|
|
else
|
|
echo "FAIL"
|
|
fi
|
|
echo
|
|
echo "LightDM boot settings:"
|
|
echo "default-display-manager: $(cat /etc/X11/default-display-manager 2>/dev/null || echo MISSING)"
|
|
echo "lightdm enabled: $(systemctl is-enabled lightdm 2>/dev/null || echo unknown)"
|
|
echo "gdm3 enabled: $(systemctl is-enabled gdm3 2>/dev/null || echo not-installed)"
|
|
echo "default target: $(systemctl get-default 2>/dev/null || echo unknown)"
|
|
echo
|
|
} > "$out_file"
|
|
|
|
ok "Post-conversion validation written: $out_file"
|
|
}
|
|
|
|
# =========================
|
|
# Plan mode (temp APT root)
|
|
# =========================
|
|
apt_tmp_run() {
|
|
local tmpapt="$1"; shift
|
|
local -a extra=(-o "Dir=${tmpapt}"
|
|
-o "Dir::State::status=/var/lib/dpkg/status"
|
|
-o "Dir::Etc::sourcelist=${tmpapt}/etc/apt/sources.list"
|
|
-o "Dir::Etc::sourceparts=${tmpapt}/etc/apt/sources.list.d"
|
|
-o "Dir::Etc::preferencesparts=${tmpapt}/etc/apt/preferences.d"
|
|
-o "Dir::Cache=${tmpapt}/var/cache/apt"
|
|
-o "Dir::State=${tmpapt}/var/lib/apt"
|
|
-o "Dir::State::Lists=${tmpapt}/var/lib/apt/lists")
|
|
apt-get "${extra[@]}" "$@"
|
|
}
|
|
|
|
plan_mode() {
|
|
need_root
|
|
ensure_no_apt_locks
|
|
detect_os
|
|
|
|
info "Running plan mode (dry-run) with temporary APT root..."
|
|
|
|
local tmpapt
|
|
tmpapt="$(mktemp -d)"
|
|
mkdir -p "${tmpapt}/etc/apt/sources.list.d" "${tmpapt}/etc/apt/preferences.d" "${tmpapt}/var/lib/apt/lists/partial" "${tmpapt}/var/cache/apt/archives/partial" "${tmpapt}/usr/share/keyrings"
|
|
|
|
local tmp_keyring="${tmpapt}/usr/share/keyrings/linuxmint-repo.gpg"
|
|
mint_keyring_build_from_linuxmint_keyring_deb "$tmp_keyring"
|
|
|
|
detect_ubuntu_mirrors
|
|
|
|
cat > "${tmpapt}/etc/apt/sources.list" <<EOF
|
|
deb [signed-by=${tmp_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
|
|
|
|
cat > "${tmpapt}/etc/apt/preferences.d/50-linuxmint-conversion.pref" <<'EOF'
|
|
Package: *
|
|
Pin: origin "packages.linuxmint.com"
|
|
Pin-Priority: 500
|
|
|
|
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* xapps* slick-greeter* lightdm* pix* xviewer* mint-themes* mint-y-icons* mint-x-icons*
|
|
Pin: origin "packages.linuxmint.com"
|
|
Pin-Priority: 700
|
|
EOF
|
|
|
|
info "APT update (plan)..."
|
|
apt_tmp_run "$tmpapt" update -y >/dev/null
|
|
|
|
local meta=""
|
|
case "$EDITION" in
|
|
cinnamon) meta="mint-meta-cinnamon" ;;
|
|
mate) meta="mint-meta-mate" ;;
|
|
xfce) meta="mint-meta-xfce" ;;
|
|
esac
|
|
|
|
local plan_out="${LOG_DIR}/plan-$(date +%Y%m%d-%H%M%S).txt"
|
|
info "Simulating install (plan) -> ${plan_out}"
|
|
|
|
set +e
|
|
apt_tmp_run "$tmpapt" -s install $(apt_opts_common) \
|
|
"$meta" mint-meta-core mintsystem mintupdate mintsources mint-meta-codecs \
|
|
2>&1 | tee "$plan_out"
|
|
local rc=${PIPESTATUS[0]}
|
|
set -e
|
|
|
|
if [[ $rc -ne 0 ]]; then
|
|
warn "Plan simulation failed (exit $rc). Review: $plan_out"
|
|
rm -rf "$tmpapt"
|
|
exit $rc
|
|
fi
|
|
|
|
ok "Plan completed successfully. Review: $plan_out"
|
|
rm -rf "$tmpapt"
|
|
}
|
|
|
|
# =========================
|
|
# Doctor
|
|
# =========================
|
|
doctor() {
|
|
need_root
|
|
ensure_no_apt_locks
|
|
detect_os
|
|
|
|
info "Doctor checks..."
|
|
apt_fix_broken
|
|
|
|
local holds
|
|
holds="$(apt-mark showhold || true)"
|
|
if [[ -n "$holds" ]]; then
|
|
warn "Held packages detected (could interfere with conversion):"
|
|
echo "$holds"
|
|
else
|
|
ok "No held packages detected."
|
|
fi
|
|
|
|
ok "Doctor complete."
|
|
}
|
|
|
|
# =========================
|
|
# Convert
|
|
# =========================
|
|
show_disclaimer_and_require_ack() {
|
|
if [[ "$I_ACCEPT_RISK" != "yes" ]]; then
|
|
die "convert requires --i-accept-the-risk"
|
|
fi
|
|
|
|
if ! is_tty; then
|
|
if [[ "$ASSUME_YES" == "yes" ]]; then
|
|
warn "Non-interactive session detected; proceeding due to --yes and --i-accept-the-risk."
|
|
return 0
|
|
fi
|
|
die "convert in non-interactive mode requires --yes (in addition to --i-accept-the-risk)"
|
|
fi
|
|
|
|
if [[ "$ASSUME_YES" == "yes" ]]; then
|
|
warn "Skipping interactive disclaimer prompt due to --yes."
|
|
return 0
|
|
fi
|
|
|
|
echo
|
|
echo "${RED}${BOLD}*** UNSUPPORTED / HIGH-RISK MIGRATION ***${RESET}"
|
|
echo "${RED}${BOLD}This is an IN-PLACE Ubuntu -> Mint-like conversion and is NOT supported.${RESET}"
|
|
echo "${RED}${BOLD}It may break VPN/EDR/MDM, compliance posture, and system stability.${RESET}"
|
|
echo "${RED}${BOLD}A CLEAN INSTALL is strongly recommended instead.${RESET}"
|
|
echo
|
|
echo "Type: ${BOLD}I UNDERSTAND${RESET} to continue, or anything else to abort:"
|
|
read -r ack
|
|
[[ "$ack" == "I UNDERSTAND" ]] || die "User aborted."
|
|
}
|
|
|
|
convert() {
|
|
need_root
|
|
ensure_no_apt_locks
|
|
detect_os
|
|
show_disclaimer_and_require_ack
|
|
|
|
apt_fix_broken
|
|
|
|
local backup_dir
|
|
backup_dir="$(backup_system_state)"
|
|
disable_thirdparty_sources_system "$backup_dir"
|
|
timeshift_snapshot_best_effort
|
|
|
|
mint_repo_key_install_system
|
|
write_mint_sources_system
|
|
write_mint_pinning_system
|
|
|
|
info "APT update..."
|
|
DEBIAN_FRONTEND=noninteractive apt-get $(apt_opts_common) update
|
|
|
|
local meta=""
|
|
case "$EDITION" in
|
|
cinnamon) meta="mint-meta-cinnamon" ;;
|
|
mate) meta="mint-meta-mate" ;;
|
|
xfce) meta="mint-meta-xfce" ;;
|
|
esac
|
|
|
|
apply_mintupdate_icon_diversion
|
|
|
|
info "Installing Mint meta packages and tooling..."
|
|
DEBIAN_FRONTEND=noninteractive apt-get $(apt_opts_common) install \
|
|
"$meta" mint-meta-core mintsystem mintupdate mintsources mint-meta-codecs
|
|
|
|
if [[ "$PRESERVE_SNAP" == "yes" ]]; then
|
|
info "Preserving snap support (best-effort)..."
|
|
DEBIAN_FRONTEND=noninteractive apt-get $(apt_opts_common) install snapd || true
|
|
fi
|
|
|
|
info "Reinstalling x11-common (fixes Xsession has_option issues)..."
|
|
DEBIAN_FRONTEND=noninteractive apt-get $(apt_opts_common) install --reinstall x11-common || true
|
|
|
|
info "Ensuring GNOME RandR runtime libs present (best-effort)..."
|
|
DEBIAN_FRONTEND=noninteractive apt-get $(apt_opts_common) install \
|
|
libgnome-rr-4-2t64 libgnome-desktop-4-2t64 libgnome-bg-4-2t64 || true
|
|
|
|
ensure_lightdm_defaults
|
|
|
|
mkdir -p "$backup_dir"
|
|
post_install_sanity "${backup_dir}/post-convert-validation.txt" || true
|
|
|
|
ok "Conversion steps completed."
|
|
info "Backup dir: ${backup_dir}"
|
|
info "Recommended next steps:"
|
|
echo " 1) Reboot"
|
|
echo " 2) At LightDM, select '${EDITION}' session if needed"
|
|
echo " 3) Validate VPN/EDR/MDM tooling and compliance"
|
|
}
|
|
|
|
# =========================
|
|
# Parse args in any order
|
|
# =========================
|
|
parse_args_any_order() {
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
doctor|plan|convert|rollback)
|
|
if [[ -n "$CMD" ]]; then
|
|
die "Multiple commands specified: '${CMD}' and '$1'"
|
|
fi
|
|
CMD="$1"
|
|
shift 1
|
|
if [[ "$CMD" == "rollback" ]]; then
|
|
if [[ $# -gt 0 && "${1:-}" != --* ]]; then
|
|
ROLLBACK_DIR="$1"
|
|
shift 1
|
|
fi
|
|
fi
|
|
;;
|
|
--edition) EDITION="${2:-}"; shift 2;;
|
|
--target) TARGET_MINT="${2:-}"; shift 2;;
|
|
--mint-mirror) MINT_MIRROR="${2:-}"; shift 2;;
|
|
--keep-ppas) KEEP_PPAS="yes"; shift 1;;
|
|
--preserve-snap) PRESERVE_SNAP="yes"; shift 1;;
|
|
--with-recommends) WITH_RECOMMENDS="yes"; shift 1;;
|
|
--yes) ASSUME_YES="yes"; shift 1;;
|
|
--i-accept-the-risk) I_ACCEPT_RISK="yes"; shift 1;;
|
|
-h|--help|help) usage; exit 0;;
|
|
*) die "Unknown argument: $1" ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# =========================
|
|
# Main
|
|
# =========================
|
|
main() {
|
|
parse_args_any_order "$@"
|
|
[[ -n "$CMD" ]] || { usage; exit 1; }
|
|
|
|
case "$CMD" in
|
|
doctor) doctor ;;
|
|
plan) plan_mode ;;
|
|
convert) convert ;;
|
|
rollback)
|
|
[[ -n "$ROLLBACK_DIR" ]] || die "rollback requires a directory argument"
|
|
rollback "$ROLLBACK_DIR"
|
|
;;
|
|
*) die "Unknown command: $CMD" ;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|