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:
stornic56
2026-10-04 23:39:40 -05:00
committed by GitHub
parent 9828ea439c
commit 9a96dff6ea
22 changed files with 975 additions and 142 deletions
+57 -38
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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. |
+2 -2
View File
@@ -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.
--- ---
+4
View File
@@ -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
+4
View File
@@ -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" \
+4
View File
@@ -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
+4
View File
@@ -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)
+17 -4
View File
@@ -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
View File
@@ -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
+8
View File
@@ -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
+4
View File
@@ -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
View File
@@ -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}"
+32 -27
View File
@@ -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"
+4
View File
@@ -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
+4
View File
@@ -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
View File
@@ -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
} }
+4
View File
@@ -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" \
+114
View File
@@ -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" ]
}
+348
View File
@@ -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
+146
View File
@@ -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" ]
}