install-wifi.sh
· 9.5 KiB · Bash
Sin formato
#!/usr/bin/env bash
# Adaptive Wi-Fi installer for Debian 12/13 (PCI and USB).
# v0.2.0 - automatic, logged Debian APT component configuration.
# Usage: sudo bash install-wifi.sh [--dry-run] [--force]
set -Eeuo pipefail
export DEBIAN_FRONTEND=noninteractive
DRY=0; FORCE=0
for arg in "$@"; do case "$arg" in --dry-run) DRY=1;; --force) FORCE=1;; -h|--help) echo 'Usage: sudo bash install-wifi.sh [--dry-run] [--force]'; exit 0;; *) echo "Unknown argument: $arg" >&2; exit 2;; esac; done
log(){ printf '[wifi] %s\n' "$*"; }
warn(){ printf '[wifi] WARNING: %s\n' "$*" >&2; }
die(){ printf '[wifi] ERROR: %s\n' "$*" >&2; exit 1; }
run(){ if ((DRY)); then printf '[dry-run]'; printf ' %q' "$@"; printf '\n'; else "$@"; fi; }
[[ $EUID -eq 0 ]] || die 'Run with sudo.'
[[ -r /etc/os-release ]] || die 'Cannot identify distribution.'
# shellcheck source=/dev/null
. /etc/os-release
[[ ${ID:-} == debian ]] || die 'This script supports Debian only.'
case "${VERSION_ID:-}" in 12|13) ;; *) die 'Supported Debian versions: 12 and 13.';; esac
for cmd in lspci lsusb modprobe apt-get awk grep sed python3; do
command -v "$cmd" >/dev/null || { if [[ $cmd == lspci || $cmd == lsusb ]]; then warn "$cmd not installed; detection may be incomplete (install pciutils/usbutils)."; else die "Missing command: $cmd"; fi; }
done
log "Debian ${VERSION_ID}; kernel $(uname -r)"
[[ -d /sys/class/net ]] || die 'Network sysfs is unavailable.'
# Find Wi-Fi interfaces, including interfaces that are currently down.
mapfile -t WIFI_IFACES < <(find /sys/class/net -mindepth 1 -maxdepth 1 -type l -printf '%f\n' | while read -r iface; do [[ -d /sys/class/net/$iface/wireless || -d /sys/class/net/$iface/phy80211 ]] && echo "$iface"; done)
if ((${#WIFI_IFACES[@]})); then
log "Wi-Fi interface(s): ${WIFI_IFACES[*]}"
if (( ! FORCE )); then log 'Driver is exposing a Wi-Fi interface. No packages changed. Use --force to attempt firmware installation.'; exit 0; fi
fi
# Scan PCI wireless network controllers by PCI class 0280 (and 0200 with matching wireless descriptions).
PCI_LINES=''; USB_LINES=''
if command -v lspci >/dev/null; then PCI_LINES=$(lspci -nn 2>/dev/null | grep -Ei 'Network controller|Wireless|802\.11|Wi-Fi' || true); fi
if command -v lsusb >/dev/null; then USB_LINES=$(lsusb 2>/dev/null || true); fi
log 'PCI Wi-Fi candidates:'; [[ -z $PCI_LINES ]] && log ' (none found)' || printf '%s\n' "$PCI_LINES"
log 'USB devices (review for USB Wi-Fi adapters):'; [[ -z $USB_LINES ]] && log ' (none found)' || printf '%s\n' "$USB_LINES"
# Collect possible vendors from PCI network candidates and USB vendor IDs.
PACKAGES=(); NEED_WL=0; UNKNOWN=0
add_pkg(){ local p; for p in "$@"; do local found=0 x; for x in "${PACKAGES[@]:-}"; do [[ $x == "$p" ]] && found=1; done; ((found)) || PACKAGES+=("$p"); done; }
# PCI: match exact Broadcom BCM4360 first; generic vendor families after.
if [[ $PCI_LINES =~ \[14e4:43a0\] ]]; then
log 'Detected Broadcom BCM4360 (14e4:43a0): proprietary wl/DKMS driver required.'
add_pkg broadcom-sta-dkms dkms "linux-headers-$(uname -r)"; NEED_WL=1
fi
if [[ $PCI_LINES =~ \[8086: ]]; then log 'Detected Intel PCI wireless hardware.'; add_pkg firmware-iwlwifi; fi
if [[ $PCI_LINES =~ \[14c3: ]]; then log 'Detected MediaTek PCI wireless hardware.'; add_pkg firmware-misc-nonfree; fi
if [[ $PCI_LINES =~ \[10ec: ]]; then log 'Detected Realtek PCI wireless hardware.'; add_pkg firmware-realtek; fi
if [[ $PCI_LINES =~ \[168c: || $PCI_LINES =~ \[17cb: ]]; then log 'Detected Qualcomm/Atheros PCI wireless hardware.'; add_pkg firmware-atheros; fi
if [[ $PCI_LINES =~ \[14e4: && $NEED_WL -eq 0 ]]; then
warn 'Broadcom PCI device detected but chipset-specific driver choice is uncertain.'
UNKNOWN=1
fi
# USB vendors alone are not enough for reliable chipset/driver choice; firmware candidates are safe,
# but USB IDs can represent non-Wi-Fi devices. Require wireless descriptors in lsusb output.
if [[ $USB_LINES =~ [Ii]ntel.*[Ww]ireless || $USB_LINES =~ [Ww]ireless.*8087: ]]; then add_pkg firmware-iwlwifi; fi
if [[ $USB_LINES =~ [Rr]ealtek.*([Ww]ireless|802\.11|[Ww]LAN) ]]; then add_pkg firmware-realtek; fi
if [[ $USB_LINES =~ [Mm]edia[Tt]ek.*([Ww]ireless|802\.11|[Ww]LAN) ]]; then add_pkg firmware-misc-nonfree; fi
if [[ $USB_LINES =~ [Aa]theros.*([Ww]ireless|802\.11|[Ww]LAN) ]]; then add_pkg firmware-atheros; fi
if ((${#PACKAGES[@]} == 0)); then
warn 'No confidently supported chipset mapping found. No system changes made.'
warn 'Run: lspci -nnk; lsusb; journalctl -k -b | grep -Ei "wifi|wlan|firmware"'
exit 3
fi
((UNKNOWN)) && warn 'At least one adapter requires manual driver verification.'
log "Candidate packages: ${PACKAGES[*]}"
# Enable required components on Debian's own active repositories only.
# Supports both legacy .list and Deb822 .sources formats, including the Debian 13 default.
# Third-party repositories and disabled entries are never modified.
log 'APT repository policy: ensure contrib, non-free, and non-free-firmware on official Debian entries.'
log 'Any changed source file will be backed up alongside the original before modification.'
if ((DRY)); then
log '[dry-run] Would inspect Debian .list/.sources and show required component changes; no files modified.'
else
python3 - <<'PYAPT'
from pathlib import Path
from datetime import datetime
import re
import shutil
import sys
needed = ('main', 'contrib', 'non-free', 'non-free-firmware')
paths = [Path('/etc/apt/sources.list')]
d = Path('/etc/apt/sources.list.d')
if d.is_dir():
paths += sorted(list(d.glob('*.list')) + list(d.glob('*.sources')))
def official(uri):
# Only Debian-owned hosts; never alter PPAs or vendor repositories.
from urllib.parse import urlparse
u = urlparse(uri)
host = (u.hostname or '').lower()
return u.scheme in ('http', 'https') and (host in ('deb.debian.org', 'security.debian.org', 'ftp.debian.org', 'archive.debian.org') or host.endswith('.debian.org'))
def update_legacy(text):
out = []
for line in text.splitlines(keepends=True):
# Keep comments, disabled lines, and cdrom entries unchanged.
m = re.match(r'^(\s*deb(?:-src)?\s+)(?:\[[^]]+\]\s+)?', line)
if not m:
out.append(line); continue
# Extract URI, allowing [options] blocks.
body = line[m.end():]
parts = body.split(None, 2)
if len(parts) < 3 or not official(parts[0]):
out.append(line); continue
uri, suite, rest = parts
if not (suite.startswith(('bookworm', 'trixie'))):
out.append(line); continue
comps, sep, comment = rest.partition('#')
existing = comps.split()
missing = [x for x in needed if x not in existing]
if missing:
newline = '\n' if line.endswith('\n') else ''
suffix = (' #' + comment.rstrip('\n')) if sep else ''
line = line[:m.end()] + uri + ' ' + suite + ' ' + ' '.join(existing + missing) + suffix + newline
out.append(line)
return ''.join(out)
def update_deb822(text):
# Process stanzas, preserving comments and unrelated fields.
blocks = re.split(r'(\n\s*\n)', text)
for i in range(0, len(blocks), 2):
block = blocks[i]
if re.search(r'^Enabled:\s*no\s*$', block, re.I | re.M):
continue
uris = re.search(r'^URIs:\s*(.+)$', block, re.M)
suites = re.search(r'^Suites:\s*(.+)$', block, re.M)
if not uris or not suites:
continue
if not all(official(uri) for uri in uris.group(1).split()):
continue
if not all(s.startswith(('bookworm', 'trixie')) for s in suites.group(1).split()):
continue
comp = re.search(r'^(Components:\s*)([^\n]*)', block, re.M)
if comp:
existing = comp.group(2).split()
missing = [x for x in needed if x not in existing]
if missing:
block = block[:comp.start(2)] + ' '.join(existing + missing) + block[comp.end(2):]
else:
block = block.rstrip('\n') + '\nComponents: ' + ' '.join(needed) + '\n'
blocks[i] = block
return ''.join(blocks)
changed = 0
for path in paths:
if not path.is_file():
continue
original = path.read_text()
updated = update_deb822(original) if path.suffix == '.sources' else update_legacy(original)
if updated == original:
continue
stamp = datetime.now().strftime('%Y%m%d-%H%M%S')
backup = path.with_name(path.name + '.bak-' + stamp)
try:
shutil.copy2(path, backup)
path.write_text(updated)
changed += 1
print(f'[wifi] APT SOURCE UPDATED: {path}')
print(f'[wifi] BACKUP CREATED: {backup}')
print('[wifi] Enabled components: main contrib non-free non-free-firmware')
except OSError as e:
print(f'[wifi] ERROR updating {path}: {e}', file=sys.stderr)
sys.exit(1)
if not changed:
print('[wifi] No Debian APT source changes required (already configured or no eligible official Debian source found).')
PYAPT
fi
log 'Internet or an offline APT mirror is required to install packages.'
run apt-get update
# Check package availability before attempting installation.
if (( ! DRY )); then
for pkg in "${PACKAGES[@]}"; do
if [[ $(apt-cache policy "$pkg" | awk '/Candidate:/{print $2}') == '(none)' ]]; then
die "Package $pkg unavailable. Check APT sources or kernel headers; no driver changes attempted."
fi
done
fi
run apt-get install -y "${PACKAGES[@]}"
if ((NEED_WL)); then
log 'Broadcom wl installed. Loading may require unloading conflicting modules and/or reboot.'
log 'Not unloading modules automatically to avoid disrupting networking.'
fi
log 'Installation finished. Reboot, then verify: lspci -nnk; ip link; nmcli device status'
| 1 | #!/usr/bin/env bash |
| 2 | # Adaptive Wi-Fi installer for Debian 12/13 (PCI and USB). |
| 3 | # v0.2.0 - automatic, logged Debian APT component configuration. |
| 4 | # Usage: sudo bash install-wifi.sh [--dry-run] [--force] |
| 5 | set -Eeuo pipefail |
| 6 | export DEBIAN_FRONTEND=noninteractive |
| 7 | DRY=0; FORCE=0 |
| 8 | for arg in "$@"; do case "$arg" in --dry-run) DRY=1;; --force) FORCE=1;; -h|--help) echo 'Usage: sudo bash install-wifi.sh [--dry-run] [--force]'; exit 0;; *) echo "Unknown argument: $arg" >&2; exit 2;; esac; done |
| 9 | log(){ printf '[wifi] %s\n' "$*"; } |
| 10 | warn(){ printf '[wifi] WARNING: %s\n' "$*" >&2; } |
| 11 | die(){ printf '[wifi] ERROR: %s\n' "$*" >&2; exit 1; } |
| 12 | run(){ if ((DRY)); then printf '[dry-run]'; printf ' %q' "$@"; printf '\n'; else "$@"; fi; } |
| 13 | [[ $EUID -eq 0 ]] || die 'Run with sudo.' |
| 14 | [[ -r /etc/os-release ]] || die 'Cannot identify distribution.' |
| 15 | # shellcheck source=/dev/null |
| 16 | . /etc/os-release |
| 17 | [[ ${ID:-} == debian ]] || die 'This script supports Debian only.' |
| 18 | case "${VERSION_ID:-}" in 12|13) ;; *) die 'Supported Debian versions: 12 and 13.';; esac |
| 19 | for cmd in lspci lsusb modprobe apt-get awk grep sed python3; do |
| 20 | command -v "$cmd" >/dev/null || { if [[ $cmd == lspci || $cmd == lsusb ]]; then warn "$cmd not installed; detection may be incomplete (install pciutils/usbutils)."; else die "Missing command: $cmd"; fi; } |
| 21 | done |
| 22 | log "Debian ${VERSION_ID}; kernel $(uname -r)" |
| 23 | [[ -d /sys/class/net ]] || die 'Network sysfs is unavailable.' |
| 24 | # Find Wi-Fi interfaces, including interfaces that are currently down. |
| 25 | mapfile -t WIFI_IFACES < <(find /sys/class/net -mindepth 1 -maxdepth 1 -type l -printf '%f\n' | while read -r iface; do [[ -d /sys/class/net/$iface/wireless || -d /sys/class/net/$iface/phy80211 ]] && echo "$iface"; done) |
| 26 | if ((${#WIFI_IFACES[@]})); then |
| 27 | log "Wi-Fi interface(s): ${WIFI_IFACES[*]}" |
| 28 | if (( ! FORCE )); then log 'Driver is exposing a Wi-Fi interface. No packages changed. Use --force to attempt firmware installation.'; exit 0; fi |
| 29 | fi |
| 30 | # Scan PCI wireless network controllers by PCI class 0280 (and 0200 with matching wireless descriptions). |
| 31 | PCI_LINES=''; USB_LINES='' |
| 32 | if command -v lspci >/dev/null; then PCI_LINES=$(lspci -nn 2>/dev/null | grep -Ei 'Network controller|Wireless|802\.11|Wi-Fi' || true); fi |
| 33 | if command -v lsusb >/dev/null; then USB_LINES=$(lsusb 2>/dev/null || true); fi |
| 34 | log 'PCI Wi-Fi candidates:'; [[ -z $PCI_LINES ]] && log ' (none found)' || printf '%s\n' "$PCI_LINES" |
| 35 | log 'USB devices (review for USB Wi-Fi adapters):'; [[ -z $USB_LINES ]] && log ' (none found)' || printf '%s\n' "$USB_LINES" |
| 36 | # Collect possible vendors from PCI network candidates and USB vendor IDs. |
| 37 | PACKAGES=(); NEED_WL=0; UNKNOWN=0 |
| 38 | add_pkg(){ local p; for p in "$@"; do local found=0 x; for x in "${PACKAGES[@]:-}"; do [[ $x == "$p" ]] && found=1; done; ((found)) || PACKAGES+=("$p"); done; } |
| 39 | # PCI: match exact Broadcom BCM4360 first; generic vendor families after. |
| 40 | if [[ $PCI_LINES =~ \[14e4:43a0\] ]]; then |
| 41 | log 'Detected Broadcom BCM4360 (14e4:43a0): proprietary wl/DKMS driver required.' |
| 42 | add_pkg broadcom-sta-dkms dkms "linux-headers-$(uname -r)"; NEED_WL=1 |
| 43 | fi |
| 44 | if [[ $PCI_LINES =~ \[8086: ]]; then log 'Detected Intel PCI wireless hardware.'; add_pkg firmware-iwlwifi; fi |
| 45 | if [[ $PCI_LINES =~ \[14c3: ]]; then log 'Detected MediaTek PCI wireless hardware.'; add_pkg firmware-misc-nonfree; fi |
| 46 | if [[ $PCI_LINES =~ \[10ec: ]]; then log 'Detected Realtek PCI wireless hardware.'; add_pkg firmware-realtek; fi |
| 47 | if [[ $PCI_LINES =~ \[168c: || $PCI_LINES =~ \[17cb: ]]; then log 'Detected Qualcomm/Atheros PCI wireless hardware.'; add_pkg firmware-atheros; fi |
| 48 | if [[ $PCI_LINES =~ \[14e4: && $NEED_WL -eq 0 ]]; then |
| 49 | warn 'Broadcom PCI device detected but chipset-specific driver choice is uncertain.' |
| 50 | UNKNOWN=1 |
| 51 | fi |
| 52 | # USB vendors alone are not enough for reliable chipset/driver choice; firmware candidates are safe, |
| 53 | # but USB IDs can represent non-Wi-Fi devices. Require wireless descriptors in lsusb output. |
| 54 | if [[ $USB_LINES =~ [Ii]ntel.*[Ww]ireless || $USB_LINES =~ [Ww]ireless.*8087: ]]; then add_pkg firmware-iwlwifi; fi |
| 55 | if [[ $USB_LINES =~ [Rr]ealtek.*([Ww]ireless|802\.11|[Ww]LAN) ]]; then add_pkg firmware-realtek; fi |
| 56 | if [[ $USB_LINES =~ [Mm]edia[Tt]ek.*([Ww]ireless|802\.11|[Ww]LAN) ]]; then add_pkg firmware-misc-nonfree; fi |
| 57 | if [[ $USB_LINES =~ [Aa]theros.*([Ww]ireless|802\.11|[Ww]LAN) ]]; then add_pkg firmware-atheros; fi |
| 58 | if ((${#PACKAGES[@]} == 0)); then |
| 59 | warn 'No confidently supported chipset mapping found. No system changes made.' |
| 60 | warn 'Run: lspci -nnk; lsusb; journalctl -k -b | grep -Ei "wifi|wlan|firmware"' |
| 61 | exit 3 |
| 62 | fi |
| 63 | ((UNKNOWN)) && warn 'At least one adapter requires manual driver verification.' |
| 64 | log "Candidate packages: ${PACKAGES[*]}" |
| 65 | # Enable required components on Debian's own active repositories only. |
| 66 | # Supports both legacy .list and Deb822 .sources formats, including the Debian 13 default. |
| 67 | # Third-party repositories and disabled entries are never modified. |
| 68 | log 'APT repository policy: ensure contrib, non-free, and non-free-firmware on official Debian entries.' |
| 69 | log 'Any changed source file will be backed up alongside the original before modification.' |
| 70 | if ((DRY)); then |
| 71 | log '[dry-run] Would inspect Debian .list/.sources and show required component changes; no files modified.' |
| 72 | else |
| 73 | python3 - <<'PYAPT' |
| 74 | from pathlib import Path |
| 75 | from datetime import datetime |
| 76 | import re |
| 77 | import shutil |
| 78 | import sys |
| 79 | |
| 80 | needed = ('main', 'contrib', 'non-free', 'non-free-firmware') |
| 81 | paths = [Path('/etc/apt/sources.list')] |
| 82 | d = Path('/etc/apt/sources.list.d') |
| 83 | if d.is_dir(): |
| 84 | paths += sorted(list(d.glob('*.list')) + list(d.glob('*.sources'))) |
| 85 | |
| 86 | def official(uri): |
| 87 | # Only Debian-owned hosts; never alter PPAs or vendor repositories. |
| 88 | from urllib.parse import urlparse |
| 89 | u = urlparse(uri) |
| 90 | host = (u.hostname or '').lower() |
| 91 | return u.scheme in ('http', 'https') and (host in ('deb.debian.org', 'security.debian.org', 'ftp.debian.org', 'archive.debian.org') or host.endswith('.debian.org')) |
| 92 | |
| 93 | def update_legacy(text): |
| 94 | out = [] |
| 95 | for line in text.splitlines(keepends=True): |
| 96 | # Keep comments, disabled lines, and cdrom entries unchanged. |
| 97 | m = re.match(r'^(\s*deb(?:-src)?\s+)(?:\[[^]]+\]\s+)?', line) |
| 98 | if not m: |
| 99 | out.append(line); continue |
| 100 | # Extract URI, allowing [options] blocks. |
| 101 | body = line[m.end():] |
| 102 | parts = body.split(None, 2) |
| 103 | if len(parts) < 3 or not official(parts[0]): |
| 104 | out.append(line); continue |
| 105 | uri, suite, rest = parts |
| 106 | if not (suite.startswith(('bookworm', 'trixie'))): |
| 107 | out.append(line); continue |
| 108 | comps, sep, comment = rest.partition('#') |
| 109 | existing = comps.split() |
| 110 | missing = [x for x in needed if x not in existing] |
| 111 | if missing: |
| 112 | newline = '\n' if line.endswith('\n') else '' |
| 113 | suffix = (' #' + comment.rstrip('\n')) if sep else '' |
| 114 | line = line[:m.end()] + uri + ' ' + suite + ' ' + ' '.join(existing + missing) + suffix + newline |
| 115 | out.append(line) |
| 116 | return ''.join(out) |
| 117 | |
| 118 | def update_deb822(text): |
| 119 | # Process stanzas, preserving comments and unrelated fields. |
| 120 | blocks = re.split(r'(\n\s*\n)', text) |
| 121 | for i in range(0, len(blocks), 2): |
| 122 | block = blocks[i] |
| 123 | if re.search(r'^Enabled:\s*no\s*$', block, re.I | re.M): |
| 124 | continue |
| 125 | uris = re.search(r'^URIs:\s*(.+)$', block, re.M) |
| 126 | suites = re.search(r'^Suites:\s*(.+)$', block, re.M) |
| 127 | if not uris or not suites: |
| 128 | continue |
| 129 | if not all(official(uri) for uri in uris.group(1).split()): |
| 130 | continue |
| 131 | if not all(s.startswith(('bookworm', 'trixie')) for s in suites.group(1).split()): |
| 132 | continue |
| 133 | comp = re.search(r'^(Components:\s*)([^\n]*)', block, re.M) |
| 134 | if comp: |
| 135 | existing = comp.group(2).split() |
| 136 | missing = [x for x in needed if x not in existing] |
| 137 | if missing: |
| 138 | block = block[:comp.start(2)] + ' '.join(existing + missing) + block[comp.end(2):] |
| 139 | else: |
| 140 | block = block.rstrip('\n') + '\nComponents: ' + ' '.join(needed) + '\n' |
| 141 | blocks[i] = block |
| 142 | return ''.join(blocks) |
| 143 | |
| 144 | changed = 0 |
| 145 | for path in paths: |
| 146 | if not path.is_file(): |
| 147 | continue |
| 148 | original = path.read_text() |
| 149 | updated = update_deb822(original) if path.suffix == '.sources' else update_legacy(original) |
| 150 | if updated == original: |
| 151 | continue |
| 152 | stamp = datetime.now().strftime('%Y%m%d-%H%M%S') |
| 153 | backup = path.with_name(path.name + '.bak-' + stamp) |
| 154 | try: |
| 155 | shutil.copy2(path, backup) |
| 156 | path.write_text(updated) |
| 157 | changed += 1 |
| 158 | print(f'[wifi] APT SOURCE UPDATED: {path}') |
| 159 | print(f'[wifi] BACKUP CREATED: {backup}') |
| 160 | print('[wifi] Enabled components: main contrib non-free non-free-firmware') |
| 161 | except OSError as e: |
| 162 | print(f'[wifi] ERROR updating {path}: {e}', file=sys.stderr) |
| 163 | sys.exit(1) |
| 164 | if not changed: |
| 165 | print('[wifi] No Debian APT source changes required (already configured or no eligible official Debian source found).') |
| 166 | PYAPT |
| 167 | fi |
| 168 | log 'Internet or an offline APT mirror is required to install packages.' |
| 169 | run apt-get update |
| 170 | # Check package availability before attempting installation. |
| 171 | if (( ! DRY )); then |
| 172 | for pkg in "${PACKAGES[@]}"; do |
| 173 | if [[ $(apt-cache policy "$pkg" | awk '/Candidate:/{print $2}') == '(none)' ]]; then |
| 174 | die "Package $pkg unavailable. Check APT sources or kernel headers; no driver changes attempted." |
| 175 | fi |
| 176 | done |
| 177 | fi |
| 178 | run apt-get install -y "${PACKAGES[@]}" |
| 179 | if ((NEED_WL)); then |
| 180 | log 'Broadcom wl installed. Loading may require unloading conflicting modules and/or reboot.' |
| 181 | log 'Not unloading modules automatically to avoid disrupting networking.' |
| 182 | fi |
| 183 | log 'Installation finished. Reboot, then verify: lspci -nnk; ip link; nmcli device status' |
| 184 |