mirror of
https://github.com/stornic56/debianito-post-install.git
synced 2026-10-31 13:08:06 +00:00
Integrity fixes: atomic writes, rc checks, curl
- Verified the repository backup rc in migrate.sh. `_persistent_backup_repos()` now aborts before any destructive step when the tar archive is missing or corrupt, removing the invalid backup itself. - Verified the restore rc in migrate.sh. `_restore_backup()` now reports failure instead of printing a green "Backup restored" message, and its caller distinguishes a restored backup from a failed restore. - Made the upgrade phases fail honestly. `apt upgrade` and `apt full-upgrade` now abort with `apt --fix-broken install` + `dpkg --configure -a` instructions instead of reporting "completed successfully" over a partially-upgraded system. - Made repository source writes atomic. Both writers tee into a `.tmp` file and move into place, and the `/etc/apt/sources.list` removal now happens after the write. - Corrected the SID warning text in migrate.sh to reflect that security fixes arrive via package uploads rather than a nonexistent sid-security suite. - Added exit-path cleanup traps for the temp `.deb` files: `trap 'rm -f "$deb_path"' RETURN` in `heroic.sh`and `tools.sh`. - Tightened Heroic download handling: the API fetch uses `curl -fsS --connect-timeout 10 --max-time 30`, the download uses `curl -fsSL --max-time 300`, and the `.deb` is validated with both `dpkg-deb --info` and `dpkg-deb --contents`; `sudo apt install` is guarded with `if !` before the temporary file is deleted. - Tightened the OpenRGB install path: the Codeberg API fetch uses `curl -fsS --connect-timeout 10 --max-time 30` and the download `curl -fsSL --max-time 300`, with the same dual `dpkg-deb` integrity check as the Heroic fallback. - Guarded the OpenRGB post-install side effects: the script now aborts after removing the failed `.deb` if `sudo apt install` returns non-zero, so `modprobe i2c-dev`, `/etc/modules`, the `i2c` group membership, `udevadm`, and the `setcap` step no longer run when the package is absent.
This commit is contained in:
+57
-38
@@ -16,33 +16,35 @@ TUI_ALTO_LISTA=10
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
MODULES_DIR="${SCRIPT_DIR}/modules"
|
MODULES_DIR="${SCRIPT_DIR}/modules"
|
||||||
|
|
||||||
|
# Boot profiling: DEBIANITO_PROFILE=1 stamps each boot
|
||||||
|
# step to stderr for before/after comparison.
|
||||||
|
_boot_mark() {
|
||||||
|
[ "${DEBIANITO_PROFILE:-0}" = "1" ] || return 0
|
||||||
|
printf '[%s] %s\n' "$(date +%s%N)" "$*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
_boot_mark "boot: start (before module sources)"
|
||||||
|
|
||||||
|
# Eager core only: utils.sh holds every boot function
|
||||||
|
# and TUI helper; hw_detect.sh declares the global
|
||||||
|
# device arrays (PCI_NET_DEVS, ...) that lazily-loaded
|
||||||
|
# modules read under `set -u`. All other modules are
|
||||||
|
# sourced on demand via _load_module() in main_menu().
|
||||||
source "${MODULES_DIR}/utils.sh"
|
source "${MODULES_DIR}/utils.sh"
|
||||||
source "${MODULES_DIR}/hw_detect.sh"
|
source "${MODULES_DIR}/hw_detect.sh"
|
||||||
[ -f "${MODULES_DIR}/sysinfo.sh" ] && source "${MODULES_DIR}/sysinfo.sh"
|
|
||||||
source "${MODULES_DIR}/sudo_config.sh"
|
|
||||||
source "${MODULES_DIR}/repos/repo_detect.sh"
|
|
||||||
source "${MODULES_DIR}/repos.sh"
|
|
||||||
[ -f "${MODULES_DIR}/firmware.sh" ] && source "${MODULES_DIR}/firmware.sh"
|
|
||||||
[ -f "${MODULES_DIR}/bluetooth.sh" ] && source "${MODULES_DIR}/bluetooth.sh"
|
|
||||||
[ -f "${MODULES_DIR}/gpu.sh" ] && source "${MODULES_DIR}/gpu.sh"
|
|
||||||
[ -f "${MODULES_DIR}/kernel.sh" ] && source "${MODULES_DIR}/kernel.sh"
|
|
||||||
[ -f "${MODULES_DIR}/gaming.sh" ] && source "${MODULES_DIR}/gaming.sh"
|
|
||||||
[ -f "${MODULES_DIR}/extras.sh" ] && source "${MODULES_DIR}/extras.sh"
|
|
||||||
[ -f "${MODULES_DIR}/zram.sh" ] && source "${MODULES_DIR}/zram.sh"
|
|
||||||
[ -f "${MODULES_DIR}/extras/java.sh" ] && source "${MODULES_DIR}/extras/java.sh"
|
|
||||||
[ -f "${MODULES_DIR}/rescue.sh" ] && source "${MODULES_DIR}/rescue.sh"
|
|
||||||
[ -f "${MODULES_DIR}/swap.sh" ] && source "${MODULES_DIR}/swap.sh"
|
|
||||||
[ -f "${MODULES_DIR}/desktop_display.sh" ] && source "${MODULES_DIR}/desktop_display.sh"
|
|
||||||
[ -f "${MODULES_DIR}/system/system_prefs.sh" ] && source "${MODULES_DIR}/system/system_prefs.sh"
|
|
||||||
[ -f "${MODULES_DIR}/system/audio.sh" ] && source "${MODULES_DIR}/system/audio.sh"
|
|
||||||
|
|
||||||
# ── Bullseye-specific modules (loaded only on Debian 11) ──
|
# ── Bullseye-specific modules (loaded only on Debian 11) ──
|
||||||
|
# Eager-conditional: the menu dispatch branches on
|
||||||
|
# `type install_*_bullseye`, so these must be defined
|
||||||
|
# before main_menu() runs.
|
||||||
if [ -d "${MODULES_DIR}/bullseye" ]; then
|
if [ -d "${MODULES_DIR}/bullseye" ]; then
|
||||||
[ -f "${MODULES_DIR}/bullseye/legacy.sh" ] && source "${MODULES_DIR}/bullseye/legacy.sh"
|
[ -f "${MODULES_DIR}/bullseye/legacy.sh" ] && source "${MODULES_DIR}/bullseye/legacy.sh"
|
||||||
[ -f "${MODULES_DIR}/bullseye/repos.sh" ] && source "${MODULES_DIR}/bullseye/repos.sh"
|
[ -f "${MODULES_DIR}/bullseye/repos.sh" ] && source "${MODULES_DIR}/bullseye/repos.sh"
|
||||||
[ -f "${MODULES_DIR}/bullseye/extras.sh" ] && source "${MODULES_DIR}/bullseye/extras.sh"
|
[ -f "${MODULES_DIR}/bullseye/extras.sh" ] && source "${MODULES_DIR}/bullseye/extras.sh"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
_boot_mark "boot: core modules loaded"
|
||||||
|
|
||||||
# ── Interrupt safety: restore repository state on Ctrl+C / TERM ──
|
# ── Interrupt safety: restore repository state on Ctrl+C / TERM ──
|
||||||
_on_interrupt() {
|
_on_interrupt() {
|
||||||
echo -e "${RED}[!] Interrupted. Restoring repository state if possible...${NC}"
|
echo -e "${RED}[!] Interrupted. Restoring repository state if possible...${NC}"
|
||||||
@@ -94,9 +96,16 @@ main_menu() {
|
|||||||
clear
|
clear
|
||||||
|
|
||||||
case "$choice" in
|
case "$choice" in
|
||||||
1) _show_sysinfo ;;
|
1)
|
||||||
2) config_sudo || true ;;
|
_load_module sysinfo || continue
|
||||||
|
_show_sysinfo
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
_load_module sudo_config || continue
|
||||||
|
config_sudo || true
|
||||||
|
;;
|
||||||
3)
|
3)
|
||||||
|
_load_module system_prefs || continue
|
||||||
_system_preferences_menu
|
_system_preferences_menu
|
||||||
STATE_REFRESHED=true
|
STATE_REFRESHED=true
|
||||||
;;
|
;;
|
||||||
@@ -104,19 +113,26 @@ main_menu() {
|
|||||||
if [ "$DEBIAN_VERSION" = "11" ] && type configure_repos_bullseye &>/dev/null; then
|
if [ "$DEBIAN_VERSION" = "11" ] && type configure_repos_bullseye &>/dev/null; then
|
||||||
configure_repos_bullseye || true
|
configure_repos_bullseye || true
|
||||||
else
|
else
|
||||||
|
_load_module repos || continue
|
||||||
configure_repos || true
|
configure_repos || true
|
||||||
fi
|
fi
|
||||||
STATE_REFRESHED=true
|
STATE_REFRESHED=true
|
||||||
;;
|
;;
|
||||||
5)
|
5)
|
||||||
if [ "$DEBIAN_VERSION" = "11" ] && type install_firmware_bullseye &>/dev/null; then
|
if [ "$DEBIAN_VERSION" = "11" ] && type install_firmware_bullseye &>/dev/null; then
|
||||||
|
# Bullseye flow calls _handle_wireless
|
||||||
|
# from firmware.sh: load the module
|
||||||
|
# (cascades repo_detect, bluetooth, repos).
|
||||||
|
_load_module firmware || continue
|
||||||
install_firmware_bullseye || true
|
install_firmware_bullseye || true
|
||||||
else
|
else
|
||||||
|
_load_module firmware || continue
|
||||||
install_firmware || true
|
install_firmware || true
|
||||||
fi
|
fi
|
||||||
STATE_REFRESHED=true
|
STATE_REFRESHED=true
|
||||||
;;
|
;;
|
||||||
6)
|
6)
|
||||||
|
_load_module gpu || continue
|
||||||
local gpu_sub
|
local gpu_sub
|
||||||
gpu_sub=$(_menu "Graphics Drivers" "" 12 50 2 \
|
gpu_sub=$(_menu "Graphics Drivers" "" 12 50 2 \
|
||||||
"1" "Radeon/Intel Mesa" \
|
"1" "Radeon/Intel Mesa" \
|
||||||
@@ -130,38 +146,54 @@ main_menu() {
|
|||||||
STATE_REFRESHED=true
|
STATE_REFRESHED=true
|
||||||
;;
|
;;
|
||||||
7)
|
7)
|
||||||
|
_load_module kernel || continue
|
||||||
show_kernel_menu || true
|
show_kernel_menu || true
|
||||||
STATE_REFRESHED=true
|
STATE_REFRESHED=true
|
||||||
;;
|
;;
|
||||||
8)
|
8)
|
||||||
if [ "$DEBIAN_VERSION" = "11" ] && type install_gaming_bullseye &>/dev/null; then
|
if [ "$DEBIAN_VERSION" = "11" ] && type install_gaming_bullseye &>/dev/null; then
|
||||||
|
# Bullseye flow calls the gaming bundle
|
||||||
|
# (ensure_contrib_repo, install_steam,
|
||||||
|
# install_mangohud, ...) and java.sh:
|
||||||
|
# load the bundle (cascades repo_detect,
|
||||||
|
# java, repos).
|
||||||
|
_load_module gaming || continue
|
||||||
install_gaming_bullseye || true
|
install_gaming_bullseye || true
|
||||||
else
|
else
|
||||||
|
_load_module gaming || continue
|
||||||
install_gaming || true
|
install_gaming || true
|
||||||
fi
|
fi
|
||||||
STATE_REFRESHED=true
|
STATE_REFRESHED=true
|
||||||
;;
|
;;
|
||||||
9)
|
9)
|
||||||
|
_load_module zram || continue
|
||||||
zram_menu || true
|
zram_menu || true
|
||||||
STATE_REFRESHED=true
|
STATE_REFRESHED=true
|
||||||
;;
|
;;
|
||||||
10)
|
10)
|
||||||
|
_load_module swap || continue
|
||||||
manage_swap || true
|
manage_swap || true
|
||||||
STATE_REFRESHED=true
|
STATE_REFRESHED=true
|
||||||
;;
|
;;
|
||||||
11)
|
11)
|
||||||
if [ "$DEBIAN_VERSION" = "11" ] && type install_extras_bullseye &>/dev/null; then
|
if [ "$DEBIAN_VERSION" = "11" ] && type install_extras_bullseye &>/dev/null; then
|
||||||
|
# Bullseye flow calls _install_dev_java
|
||||||
|
# from extras/java.sh: load it explicitly.
|
||||||
|
_load_module java || continue
|
||||||
install_extras_bullseye || true
|
install_extras_bullseye || true
|
||||||
else
|
else
|
||||||
|
_load_module extras || continue
|
||||||
install_extras || true
|
install_extras || true
|
||||||
fi
|
fi
|
||||||
STATE_REFRESHED=true
|
STATE_REFRESHED=true
|
||||||
;;
|
;;
|
||||||
12)
|
12)
|
||||||
|
_load_module rescue || continue
|
||||||
rescue_boot || true
|
rescue_boot || true
|
||||||
STATE_REFRESHED=true
|
STATE_REFRESHED=true
|
||||||
;;
|
;;
|
||||||
13)
|
13)
|
||||||
|
_load_module desktop_display || continue
|
||||||
manage_desktop_display || true
|
manage_desktop_display || true
|
||||||
STATE_REFRESHED=true
|
STATE_REFRESHED=true
|
||||||
;;
|
;;
|
||||||
@@ -176,6 +208,7 @@ main_menu() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_boot_mark "boot: pre-flight checks"
|
||||||
check_root
|
check_root
|
||||||
check_sudo
|
check_sudo
|
||||||
if ! command -v whiptail >/dev/null 2>&1; then
|
if ! command -v whiptail >/dev/null 2>&1; then
|
||||||
@@ -187,27 +220,12 @@ if ! command -v whiptail >/dev/null 2>&1; then
|
|||||||
echo -e "${RED} The TUI menu requires it; install manually and re-run.${NC}" >&2
|
echo -e "${RED} The TUI menu requires it; install manually and re-run.${NC}" >&2
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
if ! _check_network; then
|
|
||||||
echo -e "${YELLOW}──────────────────────────────────────────${NC}"
|
|
||||||
echo -e "${YELLOW} No internet connectivity detected.${NC}"
|
|
||||||
echo -e "${YELLOW} Package installation will fail without network.${NC}"
|
|
||||||
echo -e "${YELLOW} You can use: System Info, User Privileges, and${NC}"
|
|
||||||
echo -e "${YELLOW} other offline features.${NC}"
|
|
||||||
echo -e "${YELLOW}──────────────────────────────────────────${NC}"
|
|
||||||
fi
|
|
||||||
_ensure_time_synced
|
|
||||||
|
|
||||||
|
# Hardware detection is deferred on demand via
|
||||||
|
# _ensure_state_detected(); only the OS version is
|
||||||
|
# eager because the menu dispatch branches on it.
|
||||||
|
_boot_mark "boot: os detection"
|
||||||
detect_debian_version
|
detect_debian_version
|
||||||
detect_cpu_ram
|
|
||||||
detect_kernel
|
|
||||||
_init_lspci_cache
|
|
||||||
detect_gpu
|
|
||||||
detect_network
|
|
||||||
detect_displayserver
|
|
||||||
detect_storage
|
|
||||||
detect_desktop_environment
|
|
||||||
_configure_lightdm
|
|
||||||
detect_audio_server
|
|
||||||
|
|
||||||
# ── Bullseye-specific init (archive phase) ──
|
# ── Bullseye-specific init (archive phase) ──
|
||||||
if [ "$DEBIAN_VERSION" = "11" ] && type check_bullseye_archive_phase &>/dev/null; then
|
if [ "$DEBIAN_VERSION" = "11" ] && type check_bullseye_archive_phase &>/dev/null; then
|
||||||
@@ -219,4 +237,5 @@ if ! command -v whiptail >/dev/null 2>&1; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
_boot_mark "boot: entering main menu"
|
||||||
main_menu
|
main_menu
|
||||||
|
|||||||
+1
-1
@@ -43,7 +43,7 @@ The installation process follows a strict atomic pipeline defined in `install_fi
|
|||||||
|
|
||||||
Broadcom chipsets require proprietary handling because no open-source driver covers most `14e4:*` devices on modern kernels. The current implementation in `firmware.sh:_handle_wireless` uses a **single-path DKMS flow** (`broadcom-sta-dkms` + `wl` module) — not a 3-tier fallback:
|
Broadcom chipsets require proprietary handling because no open-source driver covers most `14e4:*` devices on modern kernels. The current implementation in `firmware.sh:_handle_wireless` uses a **single-path DKMS flow** (`broadcom-sta-dkms` + `wl` module) — not a 3-tier fallback:
|
||||||
|
|
||||||
1. **Device detection** — Iterates `PCI_NET_DEVS` (parsed from `lspci -nn` at startup) and extracts the Broadcom ID `14e4:XXXX`. Non-Broadcom devices are skipped.
|
1. **Device detection** — Iterates `PCI_NET_DEVS` (parsed from `lspci -nn` by the on-demand detection battery) and extracts the Broadcom ID `14e4:XXXX`. Non-Broadcom devices are skipped.
|
||||||
2. **Dependency guard** — Verifies `linux-headers-amd64` and `dkms` are available via `apt-cache show`. If missing, shows: `"linux-headers-amd64 or dkms are not available in your repositories."`
|
2. **Dependency guard** — Verifies `linux-headers-amd64` and `dkms` are available via `apt-cache show`. If missing, shows: `"linux-headers-amd64 or dkms are not available in your repositories."`
|
||||||
3. **User confirmation** — `whiptail --yesno "Install broadcom-sta-dkms, dkms, and wireless-tools?"`
|
3. **User confirmation** — `whiptail --yesno "Install broadcom-sta-dkms, dkms, and wireless-tools?"`
|
||||||
4. **Step-by-step install** (allows partial failure without aborting the whole module):
|
4. **Step-by-step install** (allows partial failure without aborting the whole module):
|
||||||
|
|||||||
+1
-1
@@ -112,7 +112,7 @@ Key details:
|
|||||||
| Mechanism | How It Works |
|
| Mechanism | How It Works |
|
||||||
| ----------- | -------------- |
|
| ----------- | -------------- |
|
||||||
| **Pre-flight `is_backports_enabled`** | `utils.sh:is_backports_enabled` greps `/etc/apt/sources.list` and `/etc/apt/sources.list.d/*.sources` / `*.list` for `trixie-backports`. Prevents `apt -t` from failing with `E: Release not found`. |
|
| **Pre-flight `is_backports_enabled`** | `utils.sh:is_backports_enabled` greps `/etc/apt/sources.list` and `/etc/apt/sources.list.d/*.sources` / `*.list` for `trixie-backports`. Prevents `apt -t` from failing with `E: Release not found`. |
|
||||||
| **NVIDIA RT warning** | `utils.sh:GPU_TYPE` is set at startup by `detect_gpu` (lspci). RT kernels change scheduling semantics; some `nvidia.ko` builds reject `PREEMPT_RT`. |
|
| **NVIDIA RT warning** | `utils.sh:GPU_TYPE` is set on demand by `detect_gpu` (lspci, via `_ensure_state_detected`). RT kernels change scheduling semantics; some `nvidia.ko` builds reject `PREEMPT_RT`. |
|
||||||
| **Atomic image+headers** | Both packages in one `apt` call. If headers are missing, `dkms` will fail at boot — so they are never installed separately. |
|
| **Atomic image+headers** | Both packages in one `apt` call. If headers are missing, `dkms` will fail at boot — so they are never installed separately. |
|
||||||
| **Fallback preservation** | `apt` never removes the running kernel. `/boot` retains `vmlinuz-*` and `initrd.img-*` for both. GRUB keeps both entries; if the new kernel panics, select the old one. |
|
| **Fallback preservation** | `apt` never removes the running kernel. `/boot` retains `vmlinuz-*` and `initrd.img-*` for both. GRUB keeps both entries; if the new kernel panics, select the old one. |
|
||||||
| **Version-aware messaging** | The confirmation dialog always shows the exact version string (`apt-cache madison` / `apt-cache show`) so users know they are not reinstalling the same package. |
|
| **Version-aware messaging** | The confirmation dialog always shows the exact version string (`apt-cache madison` / `apt-cache show`) so users know they are not reinstalling the same package. |
|
||||||
|
|||||||
+3
-3
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
### 1. What Does This Component Do?
|
### 1. What Does This Component Do?
|
||||||
|
|
||||||
This component serves as the **System Abstraction Layer** and diagnostic engine of the Debianito script. It is not merely a display utility; it acts as the foundational state initializer that runs prior to the main menu loop (`main_menu`). Its primary function is to perform pre-flight hardware enumeration, OS validation, and environment checks in "cold" mode (before any configuration changes are made).
|
This component serves as the **System Abstraction Layer** and diagnostic engine of the Debianito script. It is not merely a display utility; it acts as the foundational state initializer for the other menu options. Its primary function is to perform pre-flight hardware enumeration, OS validation, and environment checks in "cold" mode (before any configuration changes are made).
|
||||||
|
|
||||||
**Pre-flight initialization** (`debianito.sh` startup sequence):
|
**Initialization** (boot checks in `debianito.sh`, hardware state on demand via `_ensure_state_detected`):
|
||||||
|
|
||||||
1. **Dependency auto-install**: The script checks for `whiptail` and `lsb-release`, installing them automatically if missing — ensuring the TUI and version detection work on minimal systems.
|
1. **Dependency auto-install**: The script checks for `whiptail` and `lsb-release`, installing them automatically if missing — ensuring the TUI and version detection work on minimal systems.
|
||||||
2. **`lspci` guard and cache**: `command -v lspci &>/dev/null` validates availability before execution. If present, the full `lspci -nn` output is captured once into the global variable `LSPCI_OUTPUT`. All subsequent GPU, Ethernet, and network detection reads from this cache — avoiding redundant `lspci` invocations and ensuring consistent data throughout the session.
|
2. **`lspci` guard and cache**: `command -v lspci &>/dev/null` validates availability before execution. If present, the full `lspci -nn` output is captured once into the global variable `LSPCI_OUTPUT`. All subsequent GPU, Ethernet, and network detection reads from this cache — avoiding redundant `lspci` invocations and ensuring consistent data throughout the session.
|
||||||
@@ -22,7 +22,7 @@ The following table details the native Linux tools and file descriptors utilized
|
|||||||
| **OS Version** | `lsb_release -cs`, `/etc/os-release` | Parses `VERSION_CODENAME` to determine Debian release (Bullseye, Bookworm, Trixie). Critical for selecting correct repository backports. |
|
| **OS Version** | `lsb_release -cs`, `/etc/os-release` | Parses `VERSION_CODENAME` to determine Debian release (Bullseye, Bookworm, Trixie). Critical for selecting correct repository backports. |
|
||||||
| **CPU Info** | `/proc/cpuinfo` | Reads `model name` and counts cores/threads. Provides cosmetic summary without needing heavy tools like `lscpu`. |
|
| **CPU Info** | `/proc/cpuinfo` | Reads `model name` and counts cores/threads. Provides cosmetic summary without needing heavy tools like `lscpu`. |
|
||||||
| **Memory** | `/proc/meminfo` | Extracts `MemTotal` to calculate RAM in GB. Used for compatibility warnings with specific software packages. |
|
| **Memory** | `/proc/meminfo` | Extracts `MemTotal` to calculate RAM in GB. Used for compatibility warnings with specific software packages. |
|
||||||
| **GPU Detection** | `command -v lspci` guard → cached `LSPCI_OUTPUT` | `lspci -nn` output is cached in the global variable `LSPCI_OUTPUT` at startup. All subsequent GPU detection (PCI/USB buses, Ethernet, network) reads from this cache instead of re-running `lspci`. Identifies VGA/3D controllers via PCI IDs (`10de` for NVIDIA). Checks driver versions via `dpkg` if `nvidia-smi` fails. |
|
| **GPU Detection** | `command -v lspci` guard → cached `LSPCI_OUTPUT` | `lspci -nn` output is cached in the global variable `LSPCI_OUTPUT` on first hardware access (`_ensure_state_detected`). All subsequent GPU detection (PCI/USB buses, Ethernet, network) reads from this cache instead of re-running `lspci`. Identifies VGA/3D controllers via PCI IDs (`10de` for NVIDIA). Checks driver versions via `dpkg` if `nvidia-smi` fails. |
|
||||||
| **Network (Eth)** | `ip -o link show` | Enumerates Ethernet interfaces, state (UP/DOWN), and IP addresses using the `iproute2` suite. |
|
| **Network (Eth)** | `ip -o link show` | Enumerates Ethernet interfaces, state (UP/DOWN), and IP addresses using the `iproute2` suite. |
|
||||||
| **Network (Wi-Fi)** | `iwgetid`, `lspci` | Identifies wireless chipsets via PCI and retrieves SSID/Connection status for network diagnostics. |
|
| **Network (Wi-Fi)** | `iwgetid`, `lspci` | Identifies wireless chipsets via PCI and retrieves SSID/Connection status for network diagnostics. |
|
||||||
| **Storage** | `lsblk -d -o NAME,SIZE,ROTA` | Distinguishes between NVMe (`nvme`), SSD (RoT=0), and HDD (RoT=1) to provide storage topology summary. |
|
| **Storage** | `lsblk -d -o NAME,SIZE,ROTA` | Distinguishes between NVMe (`nvme`), SSD (RoT=0), and HDD (RoT=1) to provide storage topology summary. |
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ Timezone: America/Santiago
|
|||||||
- Ensures `systemd-timesyncd` is installed, enabled and restarted
|
- Ensures `systemd-timesyncd` is installed, enabled and restarted
|
||||||
- Waits 2 seconds and checks `timedatectl show --property=NTPSynchronized --value` for `yes`
|
- Waits 2 seconds and checks `timedatectl show --property=NTPSynchronized --value` for `yes`
|
||||||
|
|
||||||
**Why it matters:** An incorrect clock breaks APT GPG verification (`Release file is not valid yet`) and TLS certificates. The script runs a similar check automatically at startup (`_ensure_time_synced` in `debianito.sh` line 108), but this menu allows the user to fix timezone manually without re-running the whole script.
|
**Why it matters:** An incorrect clock breaks APT GPG verification (`Release file is not valid yet`) and TLS certificates. The script runs a similar check automatically before the first `apt update` (`_ensure_time_synced` is called from `_ensure_apt_updated` in `utils.sh`), but this menu allows the user to fix timezone manually without re-running the whole script.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -191,7 +191,7 @@ Each package is installed via `_run_install <pkg>` which confirms the version vi
|
|||||||
- **Firmware (`firmware.sh`)** — Bluetooth firmware packages (`firmware-iwlwifi`, etc.) are separate from the Bluetooth *stack* (`bluez`). This menu complements firmware by ensuring the audio side of Bluetooth (PipeWire codecs) is ready.
|
- **Firmware (`firmware.sh`)** — Bluetooth firmware packages (`firmware-iwlwifi`, etc.) are separate from the Bluetooth *stack* (`bluez`). This menu complements firmware by ensuring the audio side of Bluetooth (PipeWire codecs) is ready.
|
||||||
- **Desktop & Display (`desktop_display.sh`)** — XFCE/LXDE installations often need audio configured afterwards. Running System Preferences → Audio after a new desktop ensures the correct mixer is available.
|
- **Desktop & Display (`desktop_display.sh`)** — XFCE/LXDE installations often need audio configured afterwards. Running System Preferences → Audio after a new desktop ensures the correct mixer is available.
|
||||||
- **Gaming (`gaming.sh`)** — Many games require PipeWire/PulseAudio for voice chat. The gaming module assumes audio is already functional.
|
- **Gaming (`gaming.sh`)** — Many games require PipeWire/PulseAudio for voice chat. The gaming module assumes audio is already functional.
|
||||||
- **Time sync (`utils.sh:_ensure_time_synced`)** — Also called automatically at script startup. This menu is the manual override.
|
- **Time sync (`utils.sh:_ensure_time_synced`)** — Also called automatically before the first `apt update`. This menu is the manual override.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -131,6 +131,10 @@ install_nvidia_bullseye() {
|
|||||||
# 32-bit Mesa/NVIDIA + gamemode + mangohud + goverlay + lutris
|
# 32-bit Mesa/NVIDIA + gamemode + mangohud + goverlay + lutris
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
install_gaming_bullseye() {
|
install_gaming_bullseye() {
|
||||||
|
# Hardware state is detected on demand; GPU_TYPE is
|
||||||
|
# consulted below for the 32-bit driver choice.
|
||||||
|
_ensure_state_detected
|
||||||
|
|
||||||
echo -e "${YELLOW}Gaming setup (Bullseye)...${NC}"
|
echo -e "${YELLOW}Gaming setup (Bullseye)...${NC}"
|
||||||
|
|
||||||
local choices
|
local choices
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
manage_desktop_display() {
|
manage_desktop_display() {
|
||||||
|
# Hardware state is detected on demand; HAS_NVIDIA
|
||||||
|
# (GDM3 Wayland option) is populated by the battery.
|
||||||
|
_ensure_state_detected
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
local choice
|
local choice
|
||||||
choice=$(_menu "Desktop & Display" \
|
choice=$(_menu "Desktop & Display" \
|
||||||
|
|||||||
@@ -346,6 +346,10 @@ _ensure_nonfree_repo() {
|
|||||||
|
|
||||||
# ── Main entry point ──
|
# ── Main entry point ──
|
||||||
install_firmware() {
|
install_firmware() {
|
||||||
|
# Hardware state is detected on demand; the device
|
||||||
|
# arrays and DESKTOP_ENV/AUDIO_SERVER are read below.
|
||||||
|
_ensure_state_detected
|
||||||
|
|
||||||
echo -e "${YELLOW}Base firmware check...${NC}"
|
echo -e "${YELLOW}Base firmware check...${NC}"
|
||||||
|
|
||||||
if ! _ensure_nonfree_repo; then
|
if ! _ensure_nonfree_repo; then
|
||||||
|
|||||||
@@ -69,6 +69,10 @@ ensure_contrib_repo() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
install_gaming() {
|
install_gaming() {
|
||||||
|
# Hardware state is detected on demand; GPU_TYPE is
|
||||||
|
# consulted below for the NVIDIA hints.
|
||||||
|
_ensure_state_detected
|
||||||
|
|
||||||
echo -e "${YELLOW}Gaming setup...${NC}"
|
echo -e "${YELLOW}Gaming setup...${NC}"
|
||||||
|
|
||||||
# 1. Single checklist with ALL options (including i386 toggle)
|
# 1. Single checklist with ALL options (including i386 toggle)
|
||||||
|
|||||||
@@ -4,12 +4,18 @@
|
|||||||
install_heroic() {
|
install_heroic() {
|
||||||
local heroic_deb
|
local heroic_deb
|
||||||
heroic_deb=$(mktemp "${TMPDIR:-/tmp}/heroic-XXXXXX.deb") || return 1
|
heroic_deb=$(mktemp "${TMPDIR:-/tmp}/heroic-XXXXXX.deb") || return 1
|
||||||
|
# Remove the temp file on every exit path (RETURN
|
||||||
|
# traps are not inherited by called functions).
|
||||||
|
trap 'rm -f "$heroic_deb"' RETURN
|
||||||
local ua="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
local ua="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||||
|
|
||||||
_run_cmd "Heroic" "sudo apt install -y curl jq" "Installing dependencies..."
|
_run_cmd "Heroic" "sudo apt install -y curl jq" "Installing dependencies..."
|
||||||
|
|
||||||
local json
|
local json
|
||||||
json=$(curl -s --connect-timeout 10 -H "User-Agent: $ua" \
|
# -f: HTTP >= 400 must fail; --max-time bounds the
|
||||||
|
# whole transfer (connect-timeout only covers the
|
||||||
|
# connection phase).
|
||||||
|
json=$(curl -fsS --connect-timeout 10 --max-time 30 -H "User-Agent: $ua" \
|
||||||
"https://api.github.com/repos/Heroic-Games-Launcher/HeroicGamesLauncher/releases/latest") || {
|
"https://api.github.com/repos/Heroic-Games-Launcher/HeroicGamesLauncher/releases/latest") || {
|
||||||
_msg "Heroic Error" "Could not fetch release data from GitHub API." 8 60
|
_msg "Heroic Error" "Could not fetch release data from GitHub API." 8 60
|
||||||
return 1
|
return 1
|
||||||
@@ -28,16 +34,23 @@ install_heroic() {
|
|||||||
# Strict URL validation before interpolating into a shell command (_run_cmd uses bash -c)
|
# Strict URL validation before interpolating into a shell command (_run_cmd uses bash -c)
|
||||||
[[ "$deb_url" =~ ^https://[A-Za-z0-9./_-]+\.deb$ ]] || { _msg "Error" "Invalid download URL: $deb_url"; return 1; }
|
[[ "$deb_url" =~ ^https://[A-Za-z0-9./_-]+\.deb$ ]] || { _msg "Error" "Invalid download URL: $deb_url"; return 1; }
|
||||||
|
|
||||||
_run_cmd "Heroic" "curl -fsSL -H 'User-Agent: $ua' -o '$heroic_deb' '$deb_url'" "Downloading Heroic..."
|
_run_cmd "Heroic" "curl -fsSL --max-time 300 -H 'User-Agent: $ua' -o '$heroic_deb' '$deb_url'" "Downloading Heroic..."
|
||||||
|
|
||||||
if ! dpkg-deb --info "$heroic_deb" >/dev/null 2>&1; then
|
# --info reads only the control archive; --contents
|
||||||
|
# also walks the data archive, catching deb files
|
||||||
|
# truncated after the control section.
|
||||||
|
if ! dpkg-deb --info "$heroic_deb" >/dev/null 2>&1 || \
|
||||||
|
! dpkg-deb --contents "$heroic_deb" >/dev/null 2>&1; then
|
||||||
_msg "Heroic Error" "Downloaded .deb is corrupted or truncated.\n\nRemoving file." 10 60
|
_msg "Heroic Error" "Downloaded .deb is corrupted or truncated.\n\nRemoving file." 10 60
|
||||||
rm -f "$heroic_deb"
|
rm -f "$heroic_deb"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e "${GREEN}Package integrity verified.${NC}"
|
echo -e "${GREEN}Package integrity verified.${NC}"
|
||||||
_run_cmd "Heroic" "sudo apt install -y '$heroic_deb'" "Installing Heroic..."
|
if ! _run_cmd "Heroic" "sudo apt install -y '$heroic_deb'" "Installing Heroic..."; then
|
||||||
|
rm -f "$heroic_deb"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
rm -f "$heroic_deb"
|
rm -f "$heroic_deb"
|
||||||
echo -e "${GREEN}Heroic Games Launcher installed.${NC}"
|
echo -e "${GREEN}Heroic Games Launcher installed.${NC}"
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-4
@@ -38,12 +38,16 @@ install_openrgb() {
|
|||||||
|
|
||||||
local deb_path
|
local deb_path
|
||||||
deb_path=$(mktemp "${TMPDIR:-/tmp}/openrgb-XXXXXX.deb") || return 1
|
deb_path=$(mktemp "${TMPDIR:-/tmp}/openrgb-XXXXXX.deb") || return 1
|
||||||
|
# Remove the temp file on every exit path.
|
||||||
|
trap 'rm -f "$deb_path"' RETURN
|
||||||
local ua="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
local ua="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||||
|
|
||||||
_run_cmd "OpenRGB" "sudo apt install -y curl jq" "Installing dependencies..."
|
_run_cmd "OpenRGB" "sudo apt install -y curl jq" "Installing dependencies..."
|
||||||
|
|
||||||
local json=""
|
local json=""
|
||||||
json=$(curl -s --connect-timeout 10 \
|
# -f: HTTP >= 400 must fail; --max-time bounds the
|
||||||
|
# whole transfer.
|
||||||
|
json=$(curl -fsS --connect-timeout 10 --max-time 30 \
|
||||||
"https://codeberg.org/api/v1/repos/OpenRGB/OpenRGB/releases?limit=1") || {
|
"https://codeberg.org/api/v1/repos/OpenRGB/OpenRGB/releases?limit=1") || {
|
||||||
echo -e "${RED}Could not fetch OpenRGB releases from Codeberg API.${NC}"
|
echo -e "${RED}Could not fetch OpenRGB releases from Codeberg API.${NC}"
|
||||||
return 1
|
return 1
|
||||||
@@ -66,7 +70,7 @@ install_openrgb() {
|
|||||||
# Strict URL validation before interpolating into a shell command (_run_cmd uses bash -c)
|
# Strict URL validation before interpolating into a shell command (_run_cmd uses bash -c)
|
||||||
[[ "$deb_url" =~ ^https://[A-Za-z0-9./_-]+\.deb$ ]] || { _msg "Error" "Invalid download URL: $deb_url"; return 1; }
|
[[ "$deb_url" =~ ^https://[A-Za-z0-9./_-]+\.deb$ ]] || { _msg "Error" "Invalid download URL: $deb_url"; return 1; }
|
||||||
|
|
||||||
_run_cmd "OpenRGB" "curl -fsSL -o '${deb_path}' -A '${ua}' '${deb_url}'" "Downloading OpenRGB..."
|
_run_cmd "OpenRGB" "curl -fsSL --max-time 300 -o '${deb_path}' -A '${ua}' '${deb_url}'" "Downloading OpenRGB..."
|
||||||
|
|
||||||
if [ -n "$sha256" ]; then
|
if [ -n "$sha256" ]; then
|
||||||
if ! echo "$sha256 $deb_path" | sha256sum -c --strict; then
|
if ! echo "$sha256 $deb_path" | sha256sum -c --strict; then
|
||||||
@@ -76,7 +80,11 @@ install_openrgb() {
|
|||||||
fi
|
fi
|
||||||
echo -e "${GREEN}SHA256 verified.${NC}"
|
echo -e "${GREEN}SHA256 verified.${NC}"
|
||||||
else
|
else
|
||||||
if ! dpkg-deb --info "$deb_path" >/dev/null 2>&1; then
|
# --contents also walks the data archive,
|
||||||
|
# catching deb files truncated after the
|
||||||
|
# control section.
|
||||||
|
if ! dpkg-deb --info "$deb_path" >/dev/null 2>&1 || \
|
||||||
|
! dpkg-deb --contents "$deb_path" >/dev/null 2>&1; then
|
||||||
echo -e "${RED}Downloaded .deb is corrupted. Removing.${NC}"
|
echo -e "${RED}Downloaded .deb is corrupted. Removing.${NC}"
|
||||||
rm -f "$deb_path"
|
rm -f "$deb_path"
|
||||||
return 1
|
return 1
|
||||||
@@ -84,7 +92,14 @@ install_openrgb() {
|
|||||||
echo -e "${YELLOW}No SHA256 in API, validated via dpkg-deb.${NC}"
|
echo -e "${YELLOW}No SHA256 in API, validated via dpkg-deb.${NC}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sudo apt install -y "$deb_path"
|
# The post-install steps (i2c group, udev rules)
|
||||||
|
# must only run if the package was actually
|
||||||
|
# installed; apt can fail on missing dependencies.
|
||||||
|
if ! sudo apt install -y "$deb_path"; then
|
||||||
|
rm -f "$deb_path"
|
||||||
|
echo -e "${RED}OpenRGB installation failed (dependency or dpkg error).${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
rm -f "$deb_path"
|
rm -f "$deb_path"
|
||||||
|
|
||||||
sudo modprobe i2c-dev
|
sudo modprobe i2c-dev
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ _warn_nvidia_gnome_wayland() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_install_amd_intel_stack() {
|
_install_amd_intel_stack() {
|
||||||
|
# Hardware state is detected on demand; GPU_TYPE and
|
||||||
|
# LSPCI_OUTPUT must be populated before planning.
|
||||||
|
_ensure_state_detected
|
||||||
|
|
||||||
if [ "$GPU_TYPE" = "unknown" ] || [ -z "$GPU_TYPE" ]; then
|
if [ "$GPU_TYPE" = "unknown" ] || [ -z "$GPU_TYPE" ]; then
|
||||||
local mesa_pkgs=(mesa-vulkan-drivers libgl1-mesa-dri libglx-mesa0 libegl-mesa0 mesa-va-drivers)
|
local mesa_pkgs=(mesa-vulkan-drivers libgl1-mesa-dri libglx-mesa0 libegl-mesa0 mesa-va-drivers)
|
||||||
local ref_ver
|
local ref_ver
|
||||||
@@ -174,6 +178,10 @@ _apply_amd_gcn_grub_fix() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_install_nvidia_stack() {
|
_install_nvidia_stack() {
|
||||||
|
# Hardware state is detected on demand; HAS_NVIDIA and
|
||||||
|
# LSPCI_OUTPUT must be populated before planning.
|
||||||
|
_ensure_state_detected
|
||||||
|
|
||||||
if ! $HAS_NVIDIA; then
|
if ! $HAS_NVIDIA; then
|
||||||
_msg "NVIDIA Not Found" "No NVIDIA GPU was detected.\n\nPlease check your hardware and try again." 10 60
|
_msg "NVIDIA Not Found" "No NVIDIA GPU was detected.\n\nPlease check your hardware and try again." 10 60
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ show_kernel_menu() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_install_kernel_package() {
|
_install_kernel_package() {
|
||||||
|
# Hardware state is detected on demand; GPU_TYPE drives
|
||||||
|
# the NVIDIA DKMS/RT warnings below.
|
||||||
|
_ensure_state_detected
|
||||||
|
|
||||||
local pkg_base="$1"
|
local pkg_base="$1"
|
||||||
local flavor="$2"
|
local flavor="$2"
|
||||||
local bpo_flag="$3"
|
local bpo_flag="$3"
|
||||||
|
|||||||
+68
-16
@@ -13,7 +13,13 @@ _persistent_backup_repos() {
|
|||||||
[ -f /etc/apt/sources.list ] && files+=("/etc/apt/sources.list")
|
[ -f /etc/apt/sources.list ] && files+=("/etc/apt/sources.list")
|
||||||
[ -d /etc/apt/sources.list.d ] && files+=("/etc/apt/sources.list.d")
|
[ -d /etc/apt/sources.list.d ] && files+=("/etc/apt/sources.list.d")
|
||||||
[ ${#files[@]} -eq 0 ] && return 1
|
[ ${#files[@]} -eq 0 ] && return 1
|
||||||
sudo tar czf "$_MIGRATE_BACKUP" "${files[@]}" 2>/dev/null
|
# The rollback promise depends on this archive:
|
||||||
|
# a failed backup must abort before anything is
|
||||||
|
# deleted or rewritten.
|
||||||
|
if ! sudo tar czf "$_MIGRATE_BACKUP" "${files[@]}" 2>/dev/null; then
|
||||||
|
rm -f "$_MIGRATE_BACKUP"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
echo "Backup: $_MIGRATE_BACKUP"
|
echo "Backup: $_MIGRATE_BACKUP"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +29,13 @@ _restore_backup() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
echo -e "${YELLOW}Restoring repository backup...${NC}"
|
echo -e "${YELLOW}Restoring repository backup...${NC}"
|
||||||
sudo tar xzf "$_MIGRATE_BACKUP" -C / 2>/dev/null
|
if ! sudo tar xzf "$_MIGRATE_BACKUP" -C / 2>/dev/null; then
|
||||||
|
_msg_red "Restore Failed" \
|
||||||
|
"Extraction of $_MIGRATE_BACKUP failed.\n\
|
||||||
|
/etc/apt sources may be missing or inconsistent.\n\n\
|
||||||
|
Restore manually:\n sudo tar xzf $_MIGRATE_BACKUP -C /" 12 70
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
echo -e "${GREEN}Backup restored from $_MIGRATE_BACKUP${NC}"
|
echo -e "${GREEN}Backup restored from $_MIGRATE_BACKUP${NC}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +76,14 @@ _write_deb822_branch() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
sudo mkdir -p /etc/apt/sources.list.d
|
sudo mkdir -p /etc/apt/sources.list.d
|
||||||
echo -e "$main_content" | sudo tee "$main_file" >/dev/null
|
# Stage to a temp file and move atomically: an
|
||||||
|
# interrupted write never leaves a partial sources.
|
||||||
|
if ! echo -e "$main_content" | sudo tee "${main_file}.tmp" >/dev/null; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! sudo mv "${main_file}.tmp" "$main_file"; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
echo "Wrote $main_file"
|
echo "Wrote $main_file"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +99,14 @@ _write_classic_branch() {
|
|||||||
main_content+="deb https://security.debian.org/debian-security ${target}-security main contrib non-free non-free-firmware\n"
|
main_content+="deb https://security.debian.org/debian-security ${target}-security main contrib non-free non-free-firmware\n"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e "$main_content" | sudo tee "$main_file" >/dev/null
|
# Stage to a temp file and move atomically: an
|
||||||
|
# interrupted write never leaves a partial sources.
|
||||||
|
if ! echo -e "$main_content" | sudo tee "${main_file}.tmp" >/dev/null; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! sudo mv "${main_file}.tmp" "$main_file"; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
echo "Wrote $main_file"
|
echo "Wrote $main_file"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +119,8 @@ Risks include:\n\
|
|||||||
• NVIDIA / DKMS drivers may break\n\
|
• NVIDIA / DKMS drivers may break\n\
|
||||||
• System may fail to boot after reboot\n\
|
• System may fail to boot after reboot\n\
|
||||||
• Some packages may be removed or replaced\n\
|
• Some packages may be removed or replaced\n\
|
||||||
• SID (unstable) receives NO security updates\n\n\
|
• SID (unstable) has no release cycle; security\n\
|
||||||
|
fixes arrive via package uploads, not DSAs\n\n\
|
||||||
A full persistent backup will be saved to /var/backups/\n\
|
A full persistent backup will be saved to /var/backups/\n\
|
||||||
so you can restore if things go wrong." 16 70
|
so you can restore if things go wrong." 16 70
|
||||||
|
|
||||||
@@ -154,16 +181,20 @@ so you can restore if things go wrong." 16 70
|
|||||||
[ -f /etc/apt/sources.list.d/debian-backports.sources ] && sudo rm -f /etc/apt/sources.list.d/debian-backports.sources
|
[ -f /etc/apt/sources.list.d/debian-backports.sources ] && sudo rm -f /etc/apt/sources.list.d/debian-backports.sources
|
||||||
[ -f /etc/apt/sources.list.d/debian-backports.list ] && sudo rm -f /etc/apt/sources.list.d/debian-backports.list
|
[ -f /etc/apt/sources.list.d/debian-backports.list ] && sudo rm -f /etc/apt/sources.list.d/debian-backports.list
|
||||||
|
|
||||||
# 4c. Remove any old classic source files to avoid conflicts
|
# 4c. Write new sources
|
||||||
[ -f /etc/apt/sources.list ] && sudo rm -f /etc/apt/sources.list
|
|
||||||
|
|
||||||
# 4d. Write new sources
|
|
||||||
if ! _write_branch_sources "$target"; then
|
if ! _write_branch_sources "$target"; then
|
||||||
echo -e "${RED}[-]${NC} Failed to write new sources. Restoring backup..."
|
echo -e "${RED}[-]${NC} Failed to write new sources. Restoring backup..."
|
||||||
_restore_backup || true
|
_restore_backup || true
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 4d. Remove the old classic file AFTER the new
|
||||||
|
# sources are live: an interrupt between steps
|
||||||
|
# leaves both files (recoverable), not none.
|
||||||
|
# Only needed on the deb822 path; the classic
|
||||||
|
# writer replaced sources.list atomically.
|
||||||
|
[ "$DEBIAN_VERSION" = "13" ] && [ -f /etc/apt/sources.list ] && sudo rm -f /etc/apt/sources.list
|
||||||
|
|
||||||
# 4e. SID guardrails: install bug alerts before upgrade
|
# 4e. SID guardrails: install bug alerts before upgrade
|
||||||
if [ "$target" = "sid" ]; then
|
if [ "$target" = "sid" ]; then
|
||||||
echo -e "${YELLOW}Installing apt-listbugs and apt-listchanges (SID guardrails)...${NC}"
|
echo -e "${YELLOW}Installing apt-listbugs and apt-listchanges (SID guardrails)...${NC}"
|
||||||
@@ -175,19 +206,40 @@ so you can restore if things go wrong." 16 70
|
|||||||
echo -e "${YELLOW}Running apt update...${NC}"
|
echo -e "${YELLOW}Running apt update...${NC}"
|
||||||
if ! sudo apt update; then
|
if ! sudo apt update; then
|
||||||
echo -e "${RED}apt update failed. Restoring backup...${NC}"
|
echo -e "${RED}apt update failed. Restoring backup...${NC}"
|
||||||
_restore_backup
|
if _restore_backup; then
|
||||||
_msg_red "Migration Failed" \
|
_msg_red "Migration Failed" \
|
||||||
"apt update failed. Backup has been restored from:\n\
|
"apt update failed. Backup has been restored from:\n\
|
||||||
$_MIGRATE_BACKUP\n\n\
|
$_MIGRATE_BACKUP\n\n\
|
||||||
Your system should be back to its previous state.\n\
|
Your system should be back to its previous state.\n\
|
||||||
Run 'sudo apt update' manually to verify." 12 70
|
Run 'sudo apt update' manually to verify." 12 70
|
||||||
return
|
else
|
||||||
|
_msg_red "Migration Failed" \
|
||||||
|
"apt update failed AND the backup could not be\n\
|
||||||
|
restored from $_MIGRATE_BACKUP.\n\n\
|
||||||
|
Restore it manually:\n sudo tar xzf $_MIGRATE_BACKUP -C /\n\
|
||||||
|
then run: sudo apt update" 12 70
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 4g. Full upgrade
|
# 4g. Full upgrade
|
||||||
_run_cmd "Upgrade" "sudo apt upgrade -y" "Upgrading packages..."
|
# A failed upgrade leaves packages half-installed.
|
||||||
_run_cmd "Full-Upgrade" "sudo apt full-upgrade -y" "Running full-upgrade..."
|
# Restoring only the sources would be worse (stable
|
||||||
_run_cmd "Autoremove" "sudo apt autoremove -y" "Removing obsolete packages..."
|
# sources + testing packages), so stop with an
|
||||||
|
# honest report and manual-recovery instructions.
|
||||||
|
if ! _run_cmd "Upgrade" "sudo apt upgrade -y" "Upgrading packages..."; then
|
||||||
|
_msg_red "Migration Incomplete" \
|
||||||
|
"Package upgrade failed. The system is partially\n\
|
||||||
|
upgraded to ${target}. Do NOT reboot yet.\n\n\
|
||||||
|
Repair with:\n sudo apt --fix-broken install\n sudo dpkg --configure -a\n\n\
|
||||||
|
To roll back the branch manually, restore:\n sudo tar xzf $_MIGRATE_BACKUP -C /" 14 70
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! _run_cmd "Full-Upgrade" "sudo apt full-upgrade -y" "Running full-upgrade..."; then
|
||||||
|
_msg_red "Migration Incomplete" "full-upgrade failed. Repair with:\n sudo apt --fix-broken install\nBackup: $_MIGRATE_BACKUP" 10 70
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
_run_cmd "Autoremove" "sudo apt autoremove -y" "Removing obsolete packages..." || true
|
||||||
|
|
||||||
# 4h. Re-run detection to reflect new branch
|
# 4h. Re-run detection to reflect new branch
|
||||||
echo -e "${YELLOW}Re-running system detection for new branch...${NC}"
|
echo -e "${YELLOW}Re-running system detection for new branch...${NC}"
|
||||||
|
|||||||
@@ -5,12 +5,13 @@
|
|||||||
# Any active (non-commented) source of any format, in any file?
|
# Any active (non-commented) source of any format, in any file?
|
||||||
# Returns: 0 if at least one active source exists, 1 otherwise
|
# Returns: 0 if at least one active source exists, 1 otherwise
|
||||||
has_active_deb_sources() {
|
has_active_deb_sources() {
|
||||||
|
local apt_dir="${APT_DIR:-/etc/apt}"
|
||||||
local f
|
local f
|
||||||
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*.list; do
|
for f in "$apt_dir/sources.list" "$apt_dir/sources.list.d/"*.list; do
|
||||||
[ -f "$f" ] || continue
|
[ -f "$f" ] || continue
|
||||||
grep -qE '^[^#]*\bdeb\b' "$f" 2>/dev/null && return 0
|
grep -qE '^[^#]*\bdeb\b' "$f" 2>/dev/null && return 0
|
||||||
done
|
done
|
||||||
for f in /etc/apt/sources.list.d/*.sources; do
|
for f in "$apt_dir/sources.list.d/"*.sources; do
|
||||||
[ -f "$f" ] || continue
|
[ -f "$f" ] || continue
|
||||||
grep -qE '^Types:.*\bdeb\b' "$f" 2>/dev/null && \
|
grep -qE '^Types:.*\bdeb\b' "$f" 2>/dev/null && \
|
||||||
grep -qE '^URIs:' "$f" 2>/dev/null && return 0
|
grep -qE '^URIs:' "$f" 2>/dev/null && return 0
|
||||||
@@ -22,10 +23,11 @@ has_active_deb_sources() {
|
|||||||
# DEB822 is only valid on Debian 13 (Trixie); Debian 11/12 are classic-only.
|
# DEB822 is only valid on Debian 13 (Trixie); Debian 11/12 are classic-only.
|
||||||
# Returns: "deb822", "classic", or "none"
|
# Returns: "deb822", "classic", or "none"
|
||||||
detect_repo_format() {
|
detect_repo_format() {
|
||||||
if [ "$DEBIAN_VERSION" = "13" ] && [ -f /etc/apt/sources.list.d/debian.sources ] && \
|
local apt_dir="${APT_DIR:-/etc/apt}"
|
||||||
grep -qE '^Types:.*\bdeb\b' /etc/apt/sources.list.d/debian.sources 2>/dev/null; then
|
if [ "$DEBIAN_VERSION" = "13" ] && [ -f "$apt_dir/sources.list.d/debian.sources" ] && \
|
||||||
|
grep -qE '^Types:.*\bdeb\b' "$apt_dir/sources.list.d/debian.sources" 2>/dev/null; then
|
||||||
echo "deb822"
|
echo "deb822"
|
||||||
elif [ -f /etc/apt/sources.list ] && grep -qE '^[^#]*\bdeb\b' /etc/apt/sources.list 2>/dev/null; then
|
elif [ -f "$apt_dir/sources.list" ] && grep -qE '^[^#]*\bdeb\b' "$apt_dir/sources.list" 2>/dev/null; then
|
||||||
echo "classic"
|
echo "classic"
|
||||||
else
|
else
|
||||||
echo "none"
|
echo "none"
|
||||||
@@ -35,17 +37,18 @@ detect_repo_format() {
|
|||||||
# Detect the components currently active in the main repo file
|
# Detect the components currently active in the main repo file
|
||||||
# Returns: components list (e.g. "main contrib non-free non-free-firmware")
|
# Returns: components list (e.g. "main contrib non-free non-free-firmware")
|
||||||
detect_active_components() {
|
detect_active_components() {
|
||||||
if [ "$DEBIAN_VERSION" = "13" ] && [ -f /etc/apt/sources.list.d/debian.sources ]; then
|
local apt_dir="${APT_DIR:-/etc/apt}"
|
||||||
|
if [ "$DEBIAN_VERSION" = "13" ] && [ -f "$apt_dir/sources.list.d/debian.sources" ]; then
|
||||||
local comps
|
local comps
|
||||||
comps=$(grep "^Components:" /etc/apt/sources.list.d/debian.sources 2>/dev/null | head -1 | cut -d: -f2- | sed 's/^[[:space:]]*//')
|
comps=$(grep "^Components:" "$apt_dir/sources.list.d/debian.sources" 2>/dev/null | head -1 | cut -d: -f2- | sed 's/^[[:space:]]*//')
|
||||||
if [ -n "$comps" ]; then
|
if [ -n "$comps" ]; then
|
||||||
echo "$comps"
|
echo "$comps"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
if [ -f /etc/apt/sources.list ]; then
|
if [ -f "$apt_dir/sources.list" ]; then
|
||||||
local comps
|
local comps
|
||||||
comps=$(grep "^[^#]*deb .* main" /etc/apt/sources.list 2>/dev/null | head -1 | sed 's/.*main\s*//')
|
comps=$(grep "^[^#]*deb .* main" "$apt_dir/sources.list" 2>/dev/null | head -1 | sed 's/.*main\s*//')
|
||||||
if [ -n "$comps" ]; then
|
if [ -n "$comps" ]; then
|
||||||
echo "main $comps"
|
echo "main $comps"
|
||||||
return
|
return
|
||||||
@@ -61,17 +64,18 @@ detect_active_components() {
|
|||||||
# Check whether backports are currently enabled (any format, any file)
|
# Check whether backports are currently enabled (any format, any file)
|
||||||
# Returns: 0 if enabled, 1 otherwise
|
# Returns: 0 if enabled, 1 otherwise
|
||||||
detect_backports_status() {
|
detect_backports_status() {
|
||||||
|
local apt_dir="${APT_DIR:-/etc/apt}"
|
||||||
local codename="$1"
|
local codename="$1"
|
||||||
|
|
||||||
if [ "$DEBIAN_VERSION" = "13" ] && [ -f /etc/apt/sources.list.d/debian.sources ]; then
|
if [ "$DEBIAN_VERSION" = "13" ] && [ -f "$apt_dir/sources.list.d/debian.sources" ]; then
|
||||||
grep -qE "^Suites:.*${codename}-backports" /etc/apt/sources.list.d/debian.sources 2>/dev/null && return 0
|
grep -qE "^Suites:.*${codename}-backports" "$apt_dir/sources.list.d/debian.sources" 2>/dev/null && return 0
|
||||||
fi
|
fi
|
||||||
if [ -f /etc/apt/sources.list ]; then
|
if [ -f "$apt_dir/sources.list" ]; then
|
||||||
grep -qE "^[^#]*${codename}-backports" /etc/apt/sources.list 2>/dev/null && return 0
|
grep -qE "^[^#]*${codename}-backports" "$apt_dir/sources.list" 2>/dev/null && return 0
|
||||||
fi
|
fi
|
||||||
if [ -d /etc/apt/sources.list.d ]; then
|
if [ -d "$apt_dir/sources.list.d" ]; then
|
||||||
grep -qrE "^Suites:.*${codename}-backports" /etc/apt/sources.list.d/*.sources 2>/dev/null && return 0
|
grep -qrE "^Suites:.*${codename}-backports" "$apt_dir/sources.list.d/"*.sources 2>/dev/null && return 0
|
||||||
grep -qrE "^[^#]*${codename}-backports" /etc/apt/sources.list.d/*.list 2>/dev/null && return 0
|
grep -qrE "^[^#]*${codename}-backports" "$apt_dir/sources.list.d/"*.list 2>/dev/null && return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
return 1
|
return 1
|
||||||
@@ -84,24 +88,25 @@ detect_backports_status() {
|
|||||||
# "embedded-classic" (inside sources.list),
|
# "embedded-classic" (inside sources.list),
|
||||||
# "none"
|
# "none"
|
||||||
detect_backports_location() {
|
detect_backports_location() {
|
||||||
|
local apt_dir="${APT_DIR:-/etc/apt}"
|
||||||
local codename="$1"
|
local codename="$1"
|
||||||
|
|
||||||
if [ "$DEBIAN_VERSION" = "13" ] && [ -f /etc/apt/sources.list.d/debian-backports.sources ] && \
|
if [ "$DEBIAN_VERSION" = "13" ] && [ -f "$apt_dir/sources.list.d/debian-backports.sources" ] && \
|
||||||
grep -qE "^Suites:.*${codename}-backports" /etc/apt/sources.list.d/debian-backports.sources 2>/dev/null; then
|
grep -qE "^Suites:.*${codename}-backports" "$apt_dir/sources.list.d/debian-backports.sources" 2>/dev/null; then
|
||||||
echo "standalone-deb822"
|
echo "standalone-deb822"
|
||||||
elif [ -f /etc/apt/sources.list.d/debian-backports.list ] && \
|
elif [ -f "$apt_dir/sources.list.d/debian-backports.list" ] && \
|
||||||
grep -qE "^[^#]*${codename}-backports" /etc/apt/sources.list.d/debian-backports.list 2>/dev/null; then
|
grep -qE "^[^#]*${codename}-backports" "$apt_dir/sources.list.d/debian-backports.list" 2>/dev/null; then
|
||||||
echo "standalone-classic"
|
echo "standalone-classic"
|
||||||
elif [ "$DEBIAN_VERSION" = "13" ] && [ -f /etc/apt/sources.list.d/debian.sources ] && \
|
elif [ "$DEBIAN_VERSION" = "13" ] && [ -f "$apt_dir/sources.list.d/debian.sources" ] && \
|
||||||
grep -qE "^Suites:.*${codename}-backports" /etc/apt/sources.list.d/debian.sources 2>/dev/null; then
|
grep -qE "^Suites:.*${codename}-backports" "$apt_dir/sources.list.d/debian.sources" 2>/dev/null; then
|
||||||
echo "embedded-deb822"
|
echo "embedded-deb822"
|
||||||
elif [ -f /etc/apt/sources.list ] && \
|
elif [ -f "$apt_dir/sources.list" ] && \
|
||||||
grep -qE "^[^#]*${codename}-backports" /etc/apt/sources.list 2>/dev/null; then
|
grep -qE "^[^#]*${codename}-backports" "$apt_dir/sources.list" 2>/dev/null; then
|
||||||
echo "embedded-classic"
|
echo "embedded-classic"
|
||||||
elif [ -d /etc/apt/sources.list.d ]; then
|
elif [ -d "$apt_dir/sources.list.d" ]; then
|
||||||
if grep -qrE "^Suites:.*${codename}-backports" /etc/apt/sources.list.d/*.sources 2>/dev/null; then
|
if grep -qrE "^Suites:.*${codename}-backports" "$apt_dir/sources.list.d/"*.sources 2>/dev/null; then
|
||||||
echo "embedded-deb822"
|
echo "embedded-deb822"
|
||||||
elif grep -qrE "^[^#]*${codename}-backports" /etc/apt/sources.list.d/*.list 2>/dev/null; then
|
elif grep -qrE "^[^#]*${codename}-backports" "$apt_dir/sources.list.d/"*.list 2>/dev/null; then
|
||||||
echo "embedded-classic"
|
echo "embedded-classic"
|
||||||
else
|
else
|
||||||
echo "none"
|
echo "none"
|
||||||
|
|||||||
@@ -154,6 +154,10 @@ _swap_set_swappiness() {
|
|||||||
# ── Entry point ──
|
# ── Entry point ──
|
||||||
|
|
||||||
manage_swap() {
|
manage_swap() {
|
||||||
|
# Hardware state is detected on demand; RAM_GB
|
||||||
|
# drives the size recommendation.
|
||||||
|
_ensure_state_detected
|
||||||
|
|
||||||
exec 9>"$SWAP_LOCK"
|
exec 9>"$SWAP_LOCK"
|
||||||
flock -n 9 || {
|
flock -n 9 || {
|
||||||
_msg "Busy" "Another swap operation is already running." 8 55
|
_msg "Busy" "Another swap operation is already running." 8 55
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
_show_sysinfo() {
|
_show_sysinfo() {
|
||||||
|
# Hardware state is detected on demand; populate it
|
||||||
|
# before reading any DETECT_* global.
|
||||||
|
_ensure_state_detected
|
||||||
|
|
||||||
local msg=""
|
local msg=""
|
||||||
|
|
||||||
# ── OS Block ──
|
# ── OS Block ──
|
||||||
|
|||||||
+127
-46
@@ -28,6 +28,75 @@ APT_UPDATED=0
|
|||||||
# Cached output of `lspci -nn` for the whole session (populated once via _init_lspci_cache)
|
# Cached output of `lspci -nn` for the whole session (populated once via _init_lspci_cache)
|
||||||
LSPCI_OUTPUT=""
|
LSPCI_OUTPUT=""
|
||||||
|
|
||||||
|
# Deferred hardware state: the detection battery runs on
|
||||||
|
# first need (_ensure_state_detected) instead of at boot.
|
||||||
|
STATE_DETECTED=0
|
||||||
|
|
||||||
|
# Network check result cache: 0 = online, 1 = offline,
|
||||||
|
# empty = not checked yet (checked on first use).
|
||||||
|
_NET_STATUS=""
|
||||||
|
|
||||||
|
# Lazy module registry (presence of a key = loaded).
|
||||||
|
# Populated by _load_module() so every module is
|
||||||
|
# sourced at most once per session.
|
||||||
|
declare -A _LOADED_MODULES=()
|
||||||
|
|
||||||
|
# ── Lazy module loading ──────────────────────────
|
||||||
|
# Sources a module on first use and remembers it in
|
||||||
|
# _LOADED_MODULES. Call at the top of a main_menu()
|
||||||
|
# case branch, before calling into the module:
|
||||||
|
# _load_module <name> || continue
|
||||||
|
# Cross-module dependencies (functions called at
|
||||||
|
# runtime that live in another module) are declared
|
||||||
|
# in the deps case below, so a module never calls
|
||||||
|
# an unloaded function.
|
||||||
|
_load_module() {
|
||||||
|
# Defensive: an empty name would produce a bad
|
||||||
|
# array subscript under `set -u`.
|
||||||
|
local name="${1:-}"
|
||||||
|
[ -n "$name" ] || return 1
|
||||||
|
|
||||||
|
# Already loaded in this session?
|
||||||
|
[ -n "${_LOADED_MODULES[$name]+x}" ] && return 0
|
||||||
|
|
||||||
|
# Module file path. Most modules live directly in
|
||||||
|
# modules/; a few have a dedicated subdirectory.
|
||||||
|
local path
|
||||||
|
case "$name" in
|
||||||
|
system_prefs) path="${MODULES_DIR}/system/system_prefs.sh" ;;
|
||||||
|
audio) path="${MODULES_DIR}/system/audio.sh" ;;
|
||||||
|
java) path="${MODULES_DIR}/extras/java.sh" ;;
|
||||||
|
repo_detect) path="${MODULES_DIR}/repos/repo_detect.sh" ;;
|
||||||
|
*) path="${MODULES_DIR}/${name}.sh" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Cross-module dependencies, loaded first.
|
||||||
|
local -a deps=()
|
||||||
|
case "$name" in
|
||||||
|
system_prefs) deps=(audio) ;;
|
||||||
|
repos) deps=(repo_detect) ;;
|
||||||
|
firmware) deps=(repo_detect bluetooth repos) ;;
|
||||||
|
gaming) deps=(repo_detect java repos) ;;
|
||||||
|
extras) deps=(java) ;;
|
||||||
|
desktop_display) deps=(repos) ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
local dep
|
||||||
|
for dep in "${deps[@]}"; do
|
||||||
|
_load_module "$dep" || return 1
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ! -f "$path" ]; then
|
||||||
|
echo -e "${RED}[-] Module not found: ${path}${NC}" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$path"
|
||||||
|
_LOADED_MODULES["$name"]=1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# --------------------------
|
# --------------------------
|
||||||
# Pre-flight checks
|
# Pre-flight checks
|
||||||
# --------------------------
|
# --------------------------
|
||||||
@@ -487,21 +556,22 @@ is_backports_enabled() {
|
|||||||
|
|
||||||
local c_pattern="^[^#]*${codename}-backports[[:space:]]+"
|
local c_pattern="^[^#]*${codename}-backports[[:space:]]+"
|
||||||
local d_pattern="Suites:.*${codename}-backports"
|
local d_pattern="Suites:.*${codename}-backports"
|
||||||
|
local apt_dir="${APT_DIR:-/etc/apt}"
|
||||||
|
|
||||||
# Classic embedded (sources.list)
|
# Classic embedded (sources.list)
|
||||||
if [ -f /etc/apt/sources.list ] && grep -Eq "$c_pattern" /etc/apt/sources.list 2>/dev/null; then
|
if [ -f "$apt_dir/sources.list" ] && grep -Eq "$c_pattern" "$apt_dir/sources.list" 2>/dev/null; then
|
||||||
echo true
|
echo true
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Classic standalone (any .list file in sources.list.d)
|
# Classic standalone (any .list file in sources.list.d)
|
||||||
if [ -d /etc/apt/sources.list.d ] && grep -qrE "$c_pattern" /etc/apt/sources.list.d/*.list 2>/dev/null; then
|
if [ -d "$apt_dir/sources.list.d" ] && grep -qrE "$c_pattern" "$apt_dir/sources.list.d/"*.list 2>/dev/null; then
|
||||||
echo true
|
echo true
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Deb822 any .sources file
|
# Deb822 any .sources file
|
||||||
if [ -d /etc/apt/sources.list.d ] && grep -qr "$d_pattern" /etc/apt/sources.list.d/*.sources 2>/dev/null; then
|
if [ -d "$apt_dir/sources.list.d" ] && grep -qr "$d_pattern" "$apt_dir/sources.list.d/"*.sources 2>/dev/null; then
|
||||||
echo true
|
echo true
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
@@ -738,19 +808,29 @@ _detect_lang_pkg() {
|
|||||||
_check_network() {
|
_check_network() {
|
||||||
local target="${1:-deb.debian.org}"
|
local target="${1:-deb.debian.org}"
|
||||||
|
|
||||||
if command -v ping &>/dev/null; then
|
# Single bounded probe (worst case 3 s; the old
|
||||||
ping -c 1 -W 3 "$target" &>/dev/null && return 0
|
# ping/wget/curl cascade could block up to 13 s).
|
||||||
fi
|
[[ "$target" =~ ^[a-zA-Z0-9.-]+$ ]] || return 1
|
||||||
|
timeout 3 bash -c "</dev/tcp/${target}/80" &>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
if command -v wget &>/dev/null; then
|
# ── On-demand network check (non-blocking boot) ──
|
||||||
wget -q --timeout=5 --spider "http://${target}" &>/dev/null && return 0
|
# The connectivity banner moved from boot to the first
|
||||||
|
# apt operation. _NET_STATUS caches the result so the
|
||||||
|
# probe runs at most once per session.
|
||||||
|
_require_network() {
|
||||||
|
if [ -z "$_NET_STATUS" ]; then
|
||||||
|
if _check_network; then
|
||||||
|
_NET_STATUS=0
|
||||||
|
else
|
||||||
|
_NET_STATUS=1
|
||||||
|
echo -e "${YELLOW}──────────────────────────────────────────${NC}"
|
||||||
|
echo -e "${YELLOW} No internet connectivity detected.${NC}"
|
||||||
|
echo -e "${YELLOW} Package installation will fail without network.${NC}"
|
||||||
|
echo -e "${YELLOW}──────────────────────────────────────────${NC}"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
return "$_NET_STATUS"
|
||||||
if command -v curl &>/dev/null; then
|
|
||||||
curl -s --connect-timeout 5 -o /dev/null "http://${target}" &>/dev/null && return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ----------------------------------
|
# ----------------------------------
|
||||||
@@ -763,6 +843,15 @@ _ensure_apt_updated() {
|
|||||||
echo -e "${GREEN}[+]${NC} APT package lists already refreshed this session."
|
echo -e "${GREEN}[+]${NC} APT package lists already refreshed this session."
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Advisory: probe the network once (cached in
|
||||||
|
# _NET_STATUS) before attempting an apt operation.
|
||||||
|
_require_network || true
|
||||||
|
|
||||||
|
# A correct clock is required for HTTPS certificate
|
||||||
|
# validation; only runs when apt is actually used.
|
||||||
|
_ensure_time_synced
|
||||||
|
|
||||||
echo -e "${GREEN}[+]${NC} Refreshing APT package lists..."
|
echo -e "${GREEN}[+]${NC} Refreshing APT package lists..."
|
||||||
if sudo apt-get update; then
|
if sudo apt-get update; then
|
||||||
APT_UPDATED=1
|
APT_UPDATED=1
|
||||||
@@ -772,41 +861,33 @@ _ensure_apt_updated() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# ----------------------------------
|
# ── Deferred hardware state (lazy pre-flight) ──
|
||||||
# LightDM configuration
|
# The detection battery runs on first need instead of at
|
||||||
# ----------------------------------
|
# boot. Call _ensure_state_detected() before reading any
|
||||||
_configure_lightdm() {
|
# DETECT_* global (LSPCI_OUTPUT, GPU_*, ETH_NAMES,
|
||||||
command -v lightdm &>/dev/null || return 0
|
# DESKTOP_ENV, AUDIO_SERVER, ...).
|
||||||
|
_run_detection_battery() {
|
||||||
if _confirm "LightDM" "Configure LightDM to show the user list on the login screen?\n\nThis disables greeter-hide-users."; then
|
_init_lspci_cache
|
||||||
if ! is_installed lightdm-gtk-greeter-settings; then
|
detect_gpu
|
||||||
echo -e "${YELLOW}Installing lightdm-gtk-greeter-settings...${NC}"
|
detect_network
|
||||||
if ! sudo DEBIAN_FRONTEND=noninteractive apt install -y lightdm-gtk-greeter-settings; then
|
detect_displayserver
|
||||||
echo -e "${YELLOW}lightdm-gtk-greeter-settings could not be installed — LightDM configuration skipped.${NC}"
|
detect_storage
|
||||||
return 0
|
detect_desktop_environment
|
||||||
fi
|
detect_audio_server
|
||||||
fi
|
detect_cpu_ram
|
||||||
|
detect_kernel
|
||||||
local conf_dir="/etc/lightdm/lightdm.conf.d"
|
}
|
||||||
local conf_file="${conf_dir}/99-show-users.conf"
|
_ensure_state_detected() {
|
||||||
|
[ "$STATE_DETECTED" -eq 1 ] && return 0
|
||||||
if [ -f "$conf_file" ] && grep -q '^greeter-hide-users=false' "$conf_file"; then
|
_run_detection_battery
|
||||||
return
|
STATE_DETECTED=1
|
||||||
fi
|
|
||||||
|
|
||||||
sudo mkdir -p "$conf_dir"
|
|
||||||
printf '[Seat:*]\ngreeter-hide-users=false\n' | sudo tee "$conf_file" >/dev/null
|
|
||||||
echo -e "${GREEN}LightDM configured to show user list.${NC}"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Lazy system state refresh ──
|
# ── Lazy system state refresh ──
|
||||||
refresh_system_state() {
|
refresh_system_state() {
|
||||||
detect_debian_version
|
detect_debian_version
|
||||||
detect_gpu
|
# Forced re-run: a menu action may have changed the
|
||||||
detect_cpu_ram
|
# hardware or repo state since the first detection.
|
||||||
detect_network
|
_run_detection_battery
|
||||||
detect_desktop_environment
|
STATE_DETECTED=1
|
||||||
detect_displayserver
|
|
||||||
detect_audio_server
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
# zram.sh — ZRAM submenu: view, create/reconfigure, remove
|
# zram.sh — ZRAM submenu: view, create/reconfigure, remove
|
||||||
|
|
||||||
zram_menu() {
|
zram_menu() {
|
||||||
|
# Hardware state is detected on demand; RAM_KB
|
||||||
|
# and RAM_SUMMARY are populated by the battery.
|
||||||
|
_ensure_state_detected
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
local choice
|
local choice
|
||||||
choice=$(_menu "ZRAM Configuration" \
|
choice=$(_menu "ZRAM Configuration" \
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# repo_detect.bats — Offline tests for modules/repos/repo_detect.sh
|
||||||
|
# and is_backports_enabled() from modules/utils.sh.
|
||||||
|
#
|
||||||
|
# No `set -euo pipefail` here: BATS controls error handling
|
||||||
|
# per test.
|
||||||
|
#
|
||||||
|
# The modules under test read ${APT_DIR:-/etc/apt} (the APT_DIR
|
||||||
|
# indirection added for testability). mock_reset() points APT_DIR
|
||||||
|
# at a fresh empty $MOCK_APT_ROOT per test, and every test builds
|
||||||
|
# its own fixture tree inside it — the real /etc/apt is never
|
||||||
|
# read, so the suite is 100% offline and hermetic.
|
||||||
|
#
|
||||||
|
# Every function under test answers via stdout (echo
|
||||||
|
# true/false/deb822/...), so `run` + `$output` is the natural
|
||||||
|
# assertion style.
|
||||||
|
|
||||||
|
load test_helper.bash
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
mock_reset
|
||||||
|
load_utils
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown() {
|
||||||
|
if [ -n "${MOCK_SUDO_LOG:-}" ]; then
|
||||||
|
rm -f "$MOCK_SUDO_LOG"
|
||||||
|
fi
|
||||||
|
if [ -n "${MOCK_APT_ROOT:-}" ]; then
|
||||||
|
rm -rf "$MOCK_APT_ROOT"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Fixture helper ───────────────────────────────────
|
||||||
|
# Write one fixture file inside the mock apt root, with one
|
||||||
|
# content line per argument:
|
||||||
|
# _fixt_file sources.list "deb ... bookworm main" ...
|
||||||
|
# _fixt_file sources.list.d/debian.sources "Types: deb" ...
|
||||||
|
_fixt_file() {
|
||||||
|
local rel="$1"
|
||||||
|
shift
|
||||||
|
mkdir -p "${MOCK_APT_ROOT}/$(dirname "$rel")"
|
||||||
|
printf '%s\n' "$@" >"${MOCK_APT_ROOT}/${rel}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── is_backports_enabled (modules/utils.sh) ──────────
|
||||||
|
@test "is_backports_enabled returns true with backports in sources.list" {
|
||||||
|
_fixt_file sources.list \
|
||||||
|
"deb http://deb.debian.org/debian bookworm main" \
|
||||||
|
"deb http://deb.debian.org/debian bookworm-updates main" \
|
||||||
|
"deb http://deb.debian.org/debian bookworm-backports main"
|
||||||
|
run is_backports_enabled
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ "$output" = "true" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "is_backports_enabled returns false without backports" {
|
||||||
|
_fixt_file sources.list \
|
||||||
|
"deb http://deb.debian.org/debian bookworm main" \
|
||||||
|
"deb http://deb.debian.org/debian bookworm-updates main"
|
||||||
|
run is_backports_enabled
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ "$output" = "false" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "is_backports_enabled returns true with a standalone backports .list file" {
|
||||||
|
_fixt_file sources.list.d/debian-backports.list \
|
||||||
|
"deb http://deb.debian.org/debian bookworm-backports main"
|
||||||
|
run is_backports_enabled
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ "$output" = "true" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "is_backports_enabled returns true with a deb822 backports suite" {
|
||||||
|
_fixt_file sources.list.d/debian-backports.sources \
|
||||||
|
"Types: deb" \
|
||||||
|
"URIs: http://deb.debian.org/debian" \
|
||||||
|
"Suites: bookworm-backports" \
|
||||||
|
"Components: main" \
|
||||||
|
"Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg"
|
||||||
|
run is_backports_enabled
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ "$output" = "true" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── detect_repo_format ───────────────────────────────
|
||||||
|
@test "detect_repo_format returns deb822 for a debian.sources file" {
|
||||||
|
# DEB822 is only valid on Debian 13 (Trixie).
|
||||||
|
DEBIAN_VERSION="13"
|
||||||
|
_fixt_file sources.list.d/debian.sources \
|
||||||
|
"Types: deb" \
|
||||||
|
"URIs: http://deb.debian.org/debian" \
|
||||||
|
"Suites: bookworm bookworm-updates" \
|
||||||
|
"Components: main contrib non-free non-free-firmware" \
|
||||||
|
"Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg"
|
||||||
|
run detect_repo_format
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ "$output" = "deb822" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "detect_repo_format returns classic for a classic sources.list" {
|
||||||
|
_fixt_file sources.list \
|
||||||
|
"deb http://deb.debian.org/debian bookworm main" \
|
||||||
|
"deb http://deb.debian.org/debian bookworm-updates main"
|
||||||
|
run detect_repo_format
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ "$output" = "classic" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "detect_repo_format returns none for an empty apt root" {
|
||||||
|
run detect_repo_format
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ "$output" = "none" ]
|
||||||
|
}
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# test_helper.bash — Setup and offline system mocks for the
|
||||||
|
# debianito BATS test suite.
|
||||||
|
#
|
||||||
|
# Layout
|
||||||
|
# ------
|
||||||
|
# load_utils() Safe loading of the modules under test.
|
||||||
|
# _mock_*() Mock implementations (one per command).
|
||||||
|
# _install_mocks() Serializes the _mock_* functions into
|
||||||
|
# standalone executables under $MOCK_BIN_DIR.
|
||||||
|
# mock_reset() Per-test fixture reset + PATH setup.
|
||||||
|
#
|
||||||
|
# Why PATH-based mock scripts instead of plain shell functions?
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# _init_lspci_cache() runs `timeout 2 lspci -nn` and
|
||||||
|
# detect_network() runs `timeout 2 ip -o link show`,
|
||||||
|
# `timeout 2 ip -4 -o addr show <iface>` and
|
||||||
|
# `timeout 2 iwgetid -r <iface>`. timeout(1) execs its
|
||||||
|
# target as a CHILD PROCESS, and a child process cannot see
|
||||||
|
# the shell functions of its parent. The _mock_* functions
|
||||||
|
# are therefore written to disk as real executables
|
||||||
|
# (self-contained: shebang + `declare -f` output + dispatch
|
||||||
|
# line) and $MOCK_BIN_DIR is prepended to PATH, so both
|
||||||
|
# direct calls and timeout-wrapped calls hit the mocks.
|
||||||
|
#
|
||||||
|
# Everything here is 100% offline: no network, no apt
|
||||||
|
# update, and no real package is installed or removed.
|
||||||
|
#
|
||||||
|
# shellcheck disable=SC2148
|
||||||
|
|
||||||
|
# ── Locations ──────────────────────────────────────────────────
|
||||||
|
[ -n "${BATS_TEST_DIRNAME:-}" ] || {
|
||||||
|
echo "test_helper.bash must be loaded from a BATS test file" >&2
|
||||||
|
return 1 2>/dev/null || exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)"
|
||||||
|
export TEST_PROJECT_ROOT
|
||||||
|
|
||||||
|
# Holds the generated mock executables (created once per
|
||||||
|
# load; the OS cleans /tmp, so no per-test cleanup needed).
|
||||||
|
MOCK_BIN_DIR="$(mktemp -d "${TMPDIR:-/tmp}/debianito-mocks.XXXXXX")"
|
||||||
|
export MOCK_BIN_DIR
|
||||||
|
|
||||||
|
# ── Safe module loading ────────────────────────────────────────
|
||||||
|
# Contract (verified against the current sources):
|
||||||
|
# * modules/utils.sh and modules/repos/repo_detect.sh
|
||||||
|
# contain ONLY function definitions and variable
|
||||||
|
# assignments — no top-level command execution — so
|
||||||
|
# sourcing them is side-effect free.
|
||||||
|
# * debianito.sh itself must NEVER be sourced from tests:
|
||||||
|
# it executes the module chain and the menu at top
|
||||||
|
# level.
|
||||||
|
load_utils() {
|
||||||
|
local f
|
||||||
|
for f in modules/utils.sh modules/repos/repo_detect.sh; do
|
||||||
|
if [ ! -f "${TEST_PROJECT_ROOT}/${f}" ]; then
|
||||||
|
echo "load_utils: missing ${TEST_PROJECT_ROOT}/${f}" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# shellcheck source=modules/utils.sh
|
||||||
|
source "${TEST_PROJECT_ROOT}/modules/utils.sh"
|
||||||
|
# shellcheck source=modules/repos/repo_detect.sh
|
||||||
|
source "${TEST_PROJECT_ROOT}/modules/repos/repo_detect.sh"
|
||||||
|
|
||||||
|
# Note: sourcing from inside a function makes the
|
||||||
|
# top-level `declare -a ETH_NAMES=()` & co. of utils.sh
|
||||||
|
# function-local; they vanish when load_utils() returns.
|
||||||
|
# That is harmless — the functions under test assign
|
||||||
|
# those arrays as globals at call time (e.g. the BH-013
|
||||||
|
# reset at the top of detect_network()), so their
|
||||||
|
# behaviour is unchanged.
|
||||||
|
:
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Globals that debianito.sh normally defines ───────────────
|
||||||
|
# The modules consume them at call time only. Colour codes
|
||||||
|
# are kept EMPTY on purpose so output assertions stay clean.
|
||||||
|
export RED="" GREEN="" YELLOW="" CYAN="" NC=""
|
||||||
|
export DEBIAN_CODENAME="bookworm"
|
||||||
|
export DEBIAN_VERSION="12"
|
||||||
|
export MODULES_DIR="${TEST_PROJECT_ROOT}/modules"
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════════════════════
|
||||||
|
# Mock implementations
|
||||||
|
#
|
||||||
|
# Each mock reads its canned data from MOCK_* environment
|
||||||
|
# variables, so a test injects predictable output with a
|
||||||
|
# plain assignment (the vars are exported by mock_reset, and
|
||||||
|
# assigning to an exported var keeps the export attribute):
|
||||||
|
#
|
||||||
|
# MOCK_DPKG_INSTALLED space-separated "installed" packages
|
||||||
|
# MOCK_DPKG_VERSION version reported for installed pkgs
|
||||||
|
# MOCK_APT_CACHE_POLICY_OUTPUT `apt-cache policy <pkg>` output
|
||||||
|
# MOCK_APT_CACHE_MADISON_OUTPUT `apt-cache madison <pkg>` output
|
||||||
|
# MOCK_APT_CACHE_SEARCH_OUTPUT `apt-cache search <re>` output
|
||||||
|
# MOCK_LSPCI_OUTPUT `lspci -nn` output
|
||||||
|
# MOCK_LSUSB_OUTPUT `lsusb` output
|
||||||
|
# MOCK_IP_LINK_OUTPUT `ip -o link show` output
|
||||||
|
# MOCK_IP_ADDR_OUTPUT "iface cidr" lines for
|
||||||
|
# `ip -4 -o addr show <iface>`
|
||||||
|
# MOCK_SSID_OUTPUT `iwgetid -r <iface>` output
|
||||||
|
# MOCK_TIMEZONE_OUTPUT `timedatectl show -p Timezone --value`
|
||||||
|
# MOCK_NTP_SYNC_OUTPUT `timedatectl show --property=NTPSynchronized --value`
|
||||||
|
# MOCK_WHIPTAIL_OUTPUT / _RC whiptail stdout / exit code
|
||||||
|
# MOCK_SUDO_DRY_RUN 1 = log-only sudo (see _mock_sudo)
|
||||||
|
# MOCK_SUDO_LOG log file for dry-run sudo
|
||||||
|
# MOCK_APT_ROOT per-test fixture mirror of /etc/apt,
|
||||||
|
# exported to the code under test as
|
||||||
|
# APT_DIR (see the APT_DIR
|
||||||
|
# indirection in utils.sh and
|
||||||
|
# repos/repo_detect.sh)
|
||||||
|
# ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
# ── dpkg(1) ───────────────────────────────────────────────────
|
||||||
|
# `dpkg -l <pkg>` prints an "ii" status line and exits 0
|
||||||
|
# when <pkg> is listed in $MOCK_DPKG_INSTALLED; prints
|
||||||
|
# nothing and exits 1 otherwise (real dpkg -l reports
|
||||||
|
# "no packages found" with status 1). is_installed()
|
||||||
|
# greps for a leading "ii", so the line prefix is what
|
||||||
|
# matters — both are implemented faithfully anyway.
|
||||||
|
_mock_dpkg() {
|
||||||
|
local pkg="${@: -1}" # last argument
|
||||||
|
local installed
|
||||||
|
for installed in ${MOCK_DPKG_INSTALLED:-}; do
|
||||||
|
if [ "$installed" = "$pkg" ]; then
|
||||||
|
printf 'ii %s %s all mock package\n' \
|
||||||
|
"$pkg" "${MOCK_DPKG_VERSION:-1.0.0-mock}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── apt-cache(8) ──────────────────────────────────────────────
|
||||||
|
# Canned output per subcommand. The policy fixture needs at
|
||||||
|
# least 3 lines: _get_pkg_version() parses field 2 of line
|
||||||
|
# 3 (the "Candidate:" line in real output).
|
||||||
|
_mock_apt_cache() {
|
||||||
|
local sub="${1:-}"
|
||||||
|
[ $# -gt 0 ] && shift
|
||||||
|
case "$sub" in
|
||||||
|
policy)
|
||||||
|
[ -n "${MOCK_APT_CACHE_POLICY_OUTPUT:-}" ] &&
|
||||||
|
printf '%s\n' "${MOCK_APT_CACHE_POLICY_OUTPUT}"
|
||||||
|
;;
|
||||||
|
madison)
|
||||||
|
[ -n "${MOCK_APT_CACHE_MADISON_OUTPUT:-}" ] &&
|
||||||
|
printf '%s\n' "${MOCK_APT_CACHE_MADISON_OUTPUT}"
|
||||||
|
;;
|
||||||
|
search)
|
||||||
|
[ -n "${MOCK_APT_CACHE_SEARCH_OUTPUT:-}" ] &&
|
||||||
|
printf '%s\n' "${MOCK_APT_CACHE_SEARCH_OUTPUT}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── lspci(8) ──────────────────────────────────────────────────
|
||||||
|
# Prints $MOCK_LSPCI_OUTPUT verbatim; arguments (-nn) are
|
||||||
|
# ignored. Served through the PATH wrapper because
|
||||||
|
# _init_lspci_cache() captures `timeout 2 lspci -nn`.
|
||||||
|
_mock_lspci() {
|
||||||
|
printf '%s' "${MOCK_LSPCI_OUTPUT:-}"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── lsusb(8) ──────────────────────────────────────────────────
|
||||||
|
# Prints $MOCK_LSUSB_OUTPUT verbatim. detect_network()
|
||||||
|
# uses it as the USB-WiFi fallback (layer 4) when no PCI
|
||||||
|
# wireless device exists.
|
||||||
|
_mock_lsusb() {
|
||||||
|
printf '%s' "${MOCK_LSUSB_OUTPUT:-}"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── ip(8) ─────────────────────────────────────────────────────
|
||||||
|
# Just enough for detect_network():
|
||||||
|
# ip -o link show → one line per interface
|
||||||
|
# ($MOCK_IP_LINK_OUTPUT)
|
||||||
|
# ip -4 -o addr show <iface> → a full "ip -o" line whose
|
||||||
|
# 4th whitespace field is
|
||||||
|
# the IPv4 CIDR of <iface>,
|
||||||
|
# looked up from the
|
||||||
|
# "iface cidr" lines of
|
||||||
|
# $MOCK_IP_ADDR_OUTPUT
|
||||||
|
_mock_ip() {
|
||||||
|
case "$1 $2" in
|
||||||
|
"-o link")
|
||||||
|
# Real `ip` terminates its output with a newline;
|
||||||
|
# `while read` loops (detect_network) drop the last
|
||||||
|
# line of an unterminated stream, so the mock must
|
||||||
|
# terminate it too.
|
||||||
|
if [ -n "${MOCK_IP_LINK_OUTPUT:-}" ]; then
|
||||||
|
printf '%s\n' "${MOCK_IP_LINK_OUTPUT:-}"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
"-4 -o")
|
||||||
|
local iface="${5:-}" name cidr
|
||||||
|
[ -z "$iface" ] && return 0
|
||||||
|
while IFS=' ' read -r name cidr; do
|
||||||
|
if [ "$name" = "$iface" ]; then
|
||||||
|
printf '2: %s inet %s brd 192.168.1.255 scope global dynamic %s\n' \
|
||||||
|
"$iface" "$cidr" "$iface"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done <<EOF
|
||||||
|
${MOCK_IP_ADDR_OUTPUT:-}
|
||||||
|
EOF
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── iwgetid(1) ────────────────────────────────────────────────
|
||||||
|
# Prints $MOCK_SSID_OUTPUT. detect_network() calls it only
|
||||||
|
# for interfaces whose link state is UP.
|
||||||
|
_mock_iwgetid() {
|
||||||
|
printf '%s' "${MOCK_SSID_OUTPUT:-}"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── timedatectl(1) ────────────────────────────────────────────
|
||||||
|
# Just enough for _ensure_time_synced():
|
||||||
|
# show -p Timezone --value → $MOCK_TIMEZONE_OUTPUT
|
||||||
|
# show --property=NTPSynchronized --value → $MOCK_NTP_SYNC_OUTPUT
|
||||||
|
_mock_timedatectl() {
|
||||||
|
case "$1 $2" in
|
||||||
|
"show -p")
|
||||||
|
printf '%s\n' "${MOCK_TIMEZONE_OUTPUT:-UTC}"
|
||||||
|
;;
|
||||||
|
"show --property="*)
|
||||||
|
printf '%s\n' "${MOCK_NTP_SYNC_OUTPUT:-no}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── sudo(8) ───────────────────────────────────────────────────
|
||||||
|
# Spec behaviour: execute the command it receives, without
|
||||||
|
# asking for a password. sudo's own flags (-v, -n, -H, …)
|
||||||
|
# are stripped; the rest runs through env(1) so that
|
||||||
|
# `sudo VAR=val cmd` prefixes work too.
|
||||||
|
#
|
||||||
|
# ⚠ WARNING: the command executes FOR REAL. Only exercise
|
||||||
|
# sudo code paths with benign commands (file copies, echo,
|
||||||
|
# true, …). Set MOCK_SUDO_DRY_RUN=1 to switch to
|
||||||
|
# logging-only mode: the invocation is appended to
|
||||||
|
# $MOCK_SUDO_LOG and the mock exits 0 without executing
|
||||||
|
# anything — the safe choice for tests that touch
|
||||||
|
# privileged code paths.
|
||||||
|
_mock_sudo() {
|
||||||
|
if [ "${MOCK_SUDO_DRY_RUN:-0}" = "1" ]; then
|
||||||
|
printf '%s\n' "$*" >>"${MOCK_SUDO_LOG:-/tmp/debianito-sudo-mock.log}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
-*) shift ;;
|
||||||
|
*) break ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[ $# -eq 0 ] && return 0 # e.g. `sudo -v`
|
||||||
|
env "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── whiptail(1) ───────────────────────────────────────────────
|
||||||
|
# Prints $MOCK_WHIPTAIL_OUTPUT on stdout and exits with
|
||||||
|
# $MOCK_WHIPTAIL_RC (default 0 = "Yes" for confirm
|
||||||
|
# dialogs). Not used by the current tests; future-proofs
|
||||||
|
# the _confirm / _menu / _checklist helpers.
|
||||||
|
_mock_whiptail() {
|
||||||
|
printf '%s' "${MOCK_WHIPTAIL_OUTPUT:-}"
|
||||||
|
return "${MOCK_WHIPTAIL_RC:-0}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Mock installation ─────────────────────────────────────────
|
||||||
|
# Serializes every _mock_* function into a standalone
|
||||||
|
# executable under $MOCK_BIN_DIR (shebang + `declare -f`
|
||||||
|
# output + dispatch line). See the header comment for why
|
||||||
|
# PATH scripts are required.
|
||||||
|
_install_mocks() {
|
||||||
|
local name func
|
||||||
|
for name in dpkg apt-cache lspci lsusb ip iwgetid timedatectl sudo whiptail; do
|
||||||
|
func="_mock_${name//-/_}" # apt-cache → _mock_apt_cache
|
||||||
|
if ! declare -f "$func" >/dev/null; then
|
||||||
|
echo "test_helper.bash: mock function ${func} is not defined" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
{
|
||||||
|
printf '#!/usr/bin/env bash\n'
|
||||||
|
declare -f "$func"
|
||||||
|
printf '%s "$@"\n' "$func"
|
||||||
|
} >"${MOCK_BIN_DIR}/${name}"
|
||||||
|
chmod +x "${MOCK_BIN_DIR}/${name}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Per-test reset ────────────────────────────────────────────
|
||||||
|
# Restores every fixture to a predictable empty state and
|
||||||
|
# puts the mock bin dir first on PATH. Call from setup() in
|
||||||
|
# each .bats file: BATS runs every test in a fresh subshell,
|
||||||
|
# so PATH changes never leak between tests.
|
||||||
|
mock_reset() {
|
||||||
|
# dpkg(1)
|
||||||
|
export MOCK_DPKG_INSTALLED=""
|
||||||
|
export MOCK_DPKG_VERSION="1.0.0-mock"
|
||||||
|
|
||||||
|
# apt-cache(8)
|
||||||
|
export MOCK_APT_CACHE_POLICY_OUTPUT=""
|
||||||
|
export MOCK_APT_CACHE_MADISON_OUTPUT=""
|
||||||
|
export MOCK_APT_CACHE_SEARCH_OUTPUT=""
|
||||||
|
|
||||||
|
# hardware
|
||||||
|
export MOCK_LSPCI_OUTPUT=""
|
||||||
|
export MOCK_LSUSB_OUTPUT=""
|
||||||
|
export MOCK_IP_LINK_OUTPUT=""
|
||||||
|
export MOCK_IP_ADDR_OUTPUT=""
|
||||||
|
export MOCK_SSID_OUTPUT=""
|
||||||
|
|
||||||
|
# systemd
|
||||||
|
export MOCK_TIMEZONE_OUTPUT="UTC"
|
||||||
|
export MOCK_NTP_SYNC_OUTPUT="no"
|
||||||
|
|
||||||
|
# dialogs
|
||||||
|
export MOCK_WHIPTAIL_OUTPUT=""
|
||||||
|
export MOCK_WHIPTAIL_RC="0"
|
||||||
|
|
||||||
|
# sudo(8)
|
||||||
|
export MOCK_SUDO_DRY_RUN="0"
|
||||||
|
export MOCK_SUDO_LOG="$(mktemp)"
|
||||||
|
|
||||||
|
# /etc/apt(5) fixture mirror. The modules under test read
|
||||||
|
# ${APT_DIR:-/etc/apt} (APT_DIR indirection added for
|
||||||
|
# testability), so tests build fixture trees inside
|
||||||
|
# $MOCK_APT_ROOT instead of touching the real /etc/apt.
|
||||||
|
# Fresh per test: fixtures never leak between tests.
|
||||||
|
export MOCK_APT_ROOT="$(mktemp -d)"
|
||||||
|
export APT_DIR="${MOCK_APT_ROOT}"
|
||||||
|
|
||||||
|
export PATH="${MOCK_BIN_DIR}:${PATH}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate the mock executables once, when this helper is
|
||||||
|
# loaded.
|
||||||
|
_install_mocks
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# utils.bats — Offline tests for modules/utils.sh.
|
||||||
|
#
|
||||||
|
# No `set -euo pipefail` here: BATS controls error handling
|
||||||
|
# per test — any failing command in a test body fails the
|
||||||
|
# test, and `run` already isolates the commands under test.
|
||||||
|
#
|
||||||
|
# `run` executes its command in a SUBSHELL, so global side
|
||||||
|
# effects would be lost. The state-mutating functions
|
||||||
|
# (_init_lspci_cache, detect_network) are therefore called
|
||||||
|
# DIRECTLY and the globals they set (LSPCI_OUTPUT, the
|
||||||
|
# ETH_* / WIFI_* arrays) are asserted afterwards.
|
||||||
|
|
||||||
|
load test_helper.bash
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
mock_reset
|
||||||
|
load_utils
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown() {
|
||||||
|
if [ -n "${MOCK_SUDO_LOG:-}" ]; then
|
||||||
|
rm -f "$MOCK_SUDO_LOG"
|
||||||
|
fi
|
||||||
|
if [ -n "${MOCK_APT_ROOT:-}" ]; then
|
||||||
|
rm -rf "$MOCK_APT_ROOT"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Shared fixtures ─────────────────────────────────────
|
||||||
|
# Hardware view for the detect_network tests: one Intel
|
||||||
|
# Ethernet controller, one Intel Wi-Fi 6 controller, and
|
||||||
|
# `ip -o link show` output with lo (UNKNOWN), eth0 (UP)
|
||||||
|
# and wlan0 (UP, with IPv4 address and SSID).
|
||||||
|
_setup_detect_network_fixtures() {
|
||||||
|
MOCK_LSPCI_OUTPUT="00:19.0 Ethernet controller [0200]: Intel Corporation Ethernet Connection (7) I219-LM [8086:1533] (rev 10)
|
||||||
|
01:00.0 Network controller [0280]: Intel Corporation Wi-Fi 6 AX200 [8086:2725] (rev 1a)"
|
||||||
|
MOCK_IP_LINK_OUTPUT="1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
|
||||||
|
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
|
||||||
|
3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000"
|
||||||
|
MOCK_IP_ADDR_OUTPUT="eth0 192.168.1.10/24
|
||||||
|
wlan0 10.0.0.5/24"
|
||||||
|
MOCK_SSID_OUTPUT="CasaWiFi"
|
||||||
|
|
||||||
|
# Populate the global cache from the lspci mock
|
||||||
|
# (exercises the timeout-wrapped PATH mock chain).
|
||||||
|
LSPCI_OUTPUT=""
|
||||||
|
_init_lspci_cache
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── is_installed ────────────────────────────────────────
|
||||||
|
@test "is_installed returns 0 for an installed package" {
|
||||||
|
MOCK_DPKG_INSTALLED="curl wget"
|
||||||
|
run is_installed curl
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "is_installed returns 1 for a missing package" {
|
||||||
|
MOCK_DPKG_INSTALLED="curl wget"
|
||||||
|
run is_installed vim
|
||||||
|
[ "$status" -eq 1 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── _get_pkg_version ────────────────────────────────────
|
||||||
|
@test "_get_pkg_version parses the candidate from policy line 3" {
|
||||||
|
MOCK_APT_CACHE_POLICY_OUTPUT="curl:
|
||||||
|
Installed: 1.2.3-1
|
||||||
|
Candidate: 1.2.3"
|
||||||
|
run _get_pkg_version curl
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ "$output" = "1.2.3" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── _get_backports_version ──────────────────────────────
|
||||||
|
@test "_get_backports_version returns the backports version" {
|
||||||
|
MOCK_APT_CACHE_MADISON_OUTPUT=" curl | 1.2.3-1 | https://deb.debian.org/debian bookworm/main amd64 Packages
|
||||||
|
curl | 1.2.3-1~bpo12+1 | https://deb.debian.org/debian bookworm-backports/main amd64 Packages"
|
||||||
|
run _get_backports_version curl bookworm
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ "$output" = "1.2.3-1~bpo12+1" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "_get_backports_version defaults the codename to DEBIAN_CODENAME" {
|
||||||
|
MOCK_APT_CACHE_MADISON_OUTPUT=" curl | 1.2.3-1~bpo12+1 | https://deb.debian.org/debian bookworm-backports/main amd64 Packages"
|
||||||
|
run _get_backports_version curl
|
||||||
|
[ "$output" = "1.2.3-1~bpo12+1" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "_get_backports_version prints nothing without a backports entry" {
|
||||||
|
MOCK_APT_CACHE_MADISON_OUTPUT=" curl | 1.2.3-1 | https://deb.debian.org/debian bookworm/main amd64 Packages"
|
||||||
|
run _get_backports_version curl bookworm
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ -z "$output" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── _state ──────────────────────────────────────────────
|
||||||
|
@test "_state prints ON for an installed package" {
|
||||||
|
MOCK_DPKG_INSTALLED="curl"
|
||||||
|
run _state curl
|
||||||
|
[ "$output" = "ON" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "_state prints OFF for a missing package" {
|
||||||
|
MOCK_DPKG_INSTALLED=""
|
||||||
|
run _state curl
|
||||||
|
[ "$output" = "OFF" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── detect_network ──────────────────────────────────────
|
||||||
|
@test "detect_network fills ETH_NAMES and WIFI_NAMES from mocked hardware" {
|
||||||
|
_setup_detect_network_fixtures
|
||||||
|
detect_network
|
||||||
|
|
||||||
|
# Ethernet (PCI) interface
|
||||||
|
[ "${#ETH_NAMES[@]}" -eq 1 ]
|
||||||
|
[ "${ETH_NAMES[0]}" = "eth0" ]
|
||||||
|
[ "${ETH_STATES[0]}" = "UP" ]
|
||||||
|
[ "${ETH_IPS[0]}" = "192.168.1.10/24" ]
|
||||||
|
[ "$ETH_DESC" = "Intel Corporation Ethernet Connection (7) I219-LM" ]
|
||||||
|
|
||||||
|
# WiFi (PCI) interface
|
||||||
|
[ "${#WIFI_NAMES[@]}" -eq 1 ]
|
||||||
|
[ "${WIFI_NAMES[0]}" = "wlan0" ]
|
||||||
|
[ "${WIFI_STATES[0]}" = "UP" ]
|
||||||
|
[ "${WIFI_IPS[0]}" = "10.0.0.5/24" ]
|
||||||
|
[ "${WIFI_SSIDS[0]}" = "CasaWiFi" ]
|
||||||
|
[ "$WIFI_DESC" = "Intel Corporation Wi-Fi 6 AX200" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "detect_network does not duplicate arrays when called twice (BH-013)" {
|
||||||
|
_setup_detect_network_fixtures
|
||||||
|
|
||||||
|
detect_network
|
||||||
|
local eth_count=${#ETH_NAMES[@]}
|
||||||
|
local wifi_count=${#WIFI_NAMES[@]}
|
||||||
|
|
||||||
|
detect_network
|
||||||
|
|
||||||
|
# BH-013: the arrays must be reset and refilled, never appended to.
|
||||||
|
[ "${#ETH_NAMES[@]}" -eq "$eth_count" ]
|
||||||
|
[ "${#WIFI_NAMES[@]}" -eq "$wifi_count" ]
|
||||||
|
[ "${#ETH_NAMES[@]}" -eq 1 ]
|
||||||
|
[ "${#WIFI_NAMES[@]}" -eq 1 ]
|
||||||
|
[ "${ETH_NAMES[0]}" = "eth0" ]
|
||||||
|
[ "${WIFI_NAMES[0]}" = "wlan0" ]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user