setup.sh
debian-os-setup
Runs debian-os-setup phases 1–3 from curl | sudo bash. Standard mode installs a thin GNOME list, GRUB colors, Nautilus templates, then hands over to NetworkManager (enable only) and graphical.target. --dev adds the C toolchain, suite Python, NodeSource 24, and rustup. --dev-all also adds clang, aarch64 cross gcc, Python DS/debug, and Debian packaging. Firmware stays opt-in. Phase 4 (GDM login, GNOME Settings, Astro) is not automated.
- v0.9.0requires rootidempotentDebian 12 / 13Updated 6 Sept 2026bootstrapruntime
SHA-256 computing…
Run command
This pipes remote code to root. Read the source below first. Extra flags go to bash, not curl.
curl -fsSL https://<host>/setup.sh | sudo bashPipes remote code to root. Read the source on this page first. Fetch only: curl -fsSL https://<host>/setup.sh
What it does
- Install the thin GNOME apt list (does not enable or start gdm3)
- On --dev: C toolchain, suite Python, NodeSource Node 24, rustup as the login user
- On --dev-all: also clang, aarch64 cross gcc, Python DS/debug, and packaging
- Set GRUB_COLOR_NORMAL / HIGHLIGHT and write four files under ~/Templates
- Handover: graphical.target, loopback-only /etc/network/interfaces (backup once), enable NetworkManager without --now
- Reboot after a mutating handover unless --no-reboot
What it does not do
- Automate phase 4 (GDM login, GNOME Settings, Astro)
- Start GDM or NetworkManager before the handover reboot
- Accept Ubuntu or any ID other than debian
- pip install into /usr, npm install -g, or curl | bash without sudo
- Silently add the non-free-firmware apt component
Prerequisites
- Debian (ID=debian in /etc/os-release)
- Root via curl | sudo bash, or a saved file then sudo bash
- Network on this TTY (ifupdown still has the link)
- Pass extra flags to bash, not curl: bash -s -- --dev
Side effects
- apt-get install of the selected fences; extra NodeSource repo and rustup fetch on --dev / --dev-all
- Rewrites /etc/network/interfaces (keeps interfaces.bak once) and /etc/default/grub
- systemctl set-default graphical.target; systemctl enable NetworkManager (no --now)
- Writes /var/lib/debian-os-setup/state; may reboot
Package files
setup.sh
878 lines
1#!/usr/bin/env bash2# debian-os-setup 0.9.0 — phases 1–3 of debian-os-setup.md (not GDM login / Settings / Astro).3# Class: pipeable4# Brace group: bash slurps the whole file from a pipe before running (curl | sudo bash).5{6set -euo pipefail7export DEBIAN_FRONTEND=noninteractive8 9VERSION="0.9.0"10STATE_DIR="/var/lib/debian-os-setup"11STATE_FILE="${STATE_DIR}/state"12NODESOURCE_SETUP_URL="https://deb.nodesource.com/setup_24.x"13 14GNOME_PKGS=(15 gnome-shell gdm3 gnome-terminal gnome-text-editor gnome-calculator16 gnome-system-monitor network-manager network-manager-gnome gnome-keyring17 curl amberol mpv loupe krita cifs-utils git18)19C_PKGS=(build-essential pkgconf cmake meson ninja-build)20CLANG_PKGS=(clang clang-tidy clang-format)21CROSS_PKGS=(gcc-aarch64-linux-gnu)22PYTHON_PKGS=(23 python3 python3-venv python3-dev python3-pip python3-setuptools python3-wheel24)25PYTHON_HELPER_PKGS=(dh-python python3-bandit)26PYTHON_DS_PKGS=(27 python3-numpy python3-pandas python3-scipy python3-matplotlib python3-seaborn28 python3-cartopy python3-xarray python3-geopandas python3-pyzipper python3-sklearn29 python3-plotly python3-pydantic python3-markdown python3-pypandoc python3-ipywidgets30 jupyter-nbconvert31)32PYTHON_DEBUG_PKGS=(33 python3-debugpy python3-ipython python3-jupyter-core python3-ipykernel python3-pytest34)35NODE_APT_PKGS=(nodejs npm dh-nodejs pkg-js-tools)36RUSTUP_PKGS=(rustup libssl-dev)37RUST_APT_PKGS=(rustc cargo dh-cargo debcargo libssl-dev)38PACKAGING_PKGS=(39 dpkg-dev fakeroot debhelper dh-make debmake devscripts quilt lintian sbuild40 mmdebstrap autopkgtest piuparts adequate duck blhc diffoscope git-buildpackage41 git-debpush dgit dput-ng reportbug desktop-file-utils appstream debian-policy42 debmake-doc gnupg43)44FIRMWARE_PKGS=(firmware-linux-free firmware-linux-nonfree)45 46GNOME=147C_TOOLCHAIN=048CLANG=049CROSS=050PYTHON=051PYTHON_DS=052PYTHON_DEBUG=053NODE="none"54RUST="none"55PACKAGING=056FIRMWARE=057GRUB=158TEMPLATES=159HANDOVER=160REBOOT=161DRY_RUN=062AFTER_LOGIN=063DEV=064DEV_ALL=065C_TOOLCHAIN_SET=066PYTHON_SET=067NODE_SET=068RUST_SET=069HANDOVER_MUTATED=070 71if [[ -n "${BASH_SOURCE[0]:-}" && -f "${BASH_SOURCE[0]}" ]]; then72 SCRIPT_PATH=$(readlink -f "${BASH_SOURCE[0]}")73else74 SCRIPT_PATH=""75fi76 77err() { printf '%s\n' "$*" >&2; }78log() { printf '%s\n' "$*"; }79 80die_usage() {81 err "error: $*"82 err "Try --help for flags and invoke."83 exit 184}85 86die_run() {87 err "error: $*"88 exit 289}90 91usage() {92 cat <<EOF93debian-os-setup ${VERSION} — automate debian-os-setup.md phases 1–3 (not phase 4).94 95Invoke (root from the start; flags after bash -s --, not on curl):96 curl -fsSL https://debian.reporet.com/setup.sh | sudo bash97 curl -fsSL https://debian.reporet.com/setup.sh | sudo bash -s -- --dev98 curl -fsSL https://debian.reporet.com/setup.sh | sudo bash -s -- --dev-all99 curl -fsSL https://debian.reporet.com/setup.sh | sudo bash -s -- --no-reboot100 101 curl -fsSL https://debian.reporet.com/setup.sh -o /tmp/debian-os-setup.sh102 sudo bash /tmp/debian-os-setup.sh --dev-all103 104 sudo bash scripts/setup/setup.sh105 sudo bash scripts/setup/setup.sh --dev-all106 107Do not use curl | bash without sudo (cannot re-exec a pipe).108Do not write curl …/setup.sh --dev | sudo bash (curl consumes --dev).109 110Modes:111 standard (default) GNOME, GRUB colors, templates, handover; no language toolchains112 --dev standard plus C toolchain, Python, NodeSource 24, rustup113 --dev-all --dev plus clang, aarch64 cross, Python DS/debug, packaging114 (firmware stays opt-in; all apt still runs this TTY, no reboot oneshot)115 116Options:117 --dev Developer preset (C, Python, NodeSource 24, rustup)118 --dev-all Full developer preset except firmware119 --gnome / --no-gnome Minimal GNOME apt list (default: on)120 --c-toolchain / --no-c-toolchain build-essential pkgconf cmake meson ninja-build121 (default: off; on with --dev / --dev-all)122 --clang clang clang-tidy clang-format123 (default: off; on with --dev-all)124 --cross gcc-aarch64-linux-gnu (default: off; on with --dev-all)125 --python / --no-python Python interpreter + dh-python/bandit126 (default: off; on with --dev / --dev-all)127 --python-ds Optional DS/ML apt list (default: off; on with --dev-all)128 --python-debug Optional debug apt list (default: off; on with --dev-all)129 --node=nodesource|apt|none Node path (default: none; nodesource with --dev / --dev-all)130 --rust=rustup|apt|none Rust path (default: none; rustup with --dev / --dev-all)131 --packaging Debian packaging apt list (default: off; on with --dev-all)132 --firmware firmware-linux-free/nonfree if sources allow (default: off)133 --grub-colors / --no-grub-colors /etc/default/grub colors + update-grub (default: on)134 --templates / --no-templates Login-user ~/Templates files (default: on)135 --handover / --no-handover graphical.target, loopback-only interfaces, enable NM (default: on)136 --reboot / --no-reboot Reboot if handover changed this run (default: on if handover ran)137 --dry-run Print commands; no apt, writes, or reboot138 --after-login Verify-only (versions, get-default, NM); no installs139 --help Print this text and exit 0140 141Exit codes: 0 success (including scheduled reboot); 1 usage/preflight; 2 runtime.142Canonical lists and NM/GDM rules: debian-os-setup.md143EOF144}145 146set_node() {147 local v=$1148 case "$v" in149 nodesource | apt | none) ;;150 *) die_usage "invalid --node=${v} (want nodesource|apt|none)" ;;151 esac152 if ((NODE_SET)) && [[ "$NODE" != "$v" ]]; then153 die_usage "conflicting --node values (${NODE} vs ${v})"154 fi155 NODE=$v156 NODE_SET=1157}158 159set_rust() {160 local v=$1161 case "$v" in162 rustup | apt | none) ;;163 *) die_usage "invalid --rust=${v} (want rustup|apt|none)" ;;164 esac165 if ((RUST_SET)) && [[ "$RUST" != "$v" ]]; then166 die_usage "conflicting --rust values (${RUST} vs ${v})"167 fi168 RUST=$v169 RUST_SET=1170}171 172apply_mode_defaults() {173 if ((DEV == 0 && DEV_ALL == 0)); then174 return 0175 fi176 ((C_TOOLCHAIN_SET)) || C_TOOLCHAIN=1177 ((PYTHON_SET)) || PYTHON=1178 ((NODE_SET)) || NODE=nodesource179 ((RUST_SET)) || RUST=rustup180 if ((DEV_ALL)); then181 CLANG=1182 CROSS=1183 PYTHON_DS=1184 PYTHON_DEBUG=1185 PACKAGING=1186 fi187}188 189parse_args() {190 while [[ $# -gt 0 ]]; do191 case "$1" in192 --help)193 usage194 exit 0195 ;;196 --dry-run) DRY_RUN=1 ;;197 --after-login) AFTER_LOGIN=1 ;;198 --dev) DEV=1 ;;199 --dev-all)200 DEV=1201 DEV_ALL=1202 ;;203 --gnome) GNOME=1 ;;204 --no-gnome) GNOME=0 ;;205 --c-toolchain)206 C_TOOLCHAIN=1207 C_TOOLCHAIN_SET=1208 ;;209 --no-c-toolchain)210 C_TOOLCHAIN=0211 C_TOOLCHAIN_SET=1212 ;;213 --clang) CLANG=1 ;;214 --cross) CROSS=1 ;;215 --python)216 PYTHON=1217 PYTHON_SET=1218 ;;219 --no-python)220 PYTHON=0221 PYTHON_SET=1222 ;;223 --python-ds) PYTHON_DS=1 ;;224 --python-debug) PYTHON_DEBUG=1 ;;225 --packaging) PACKAGING=1 ;;226 --firmware) FIRMWARE=1 ;;227 --grub-colors) GRUB=1 ;;228 --no-grub-colors) GRUB=0 ;;229 --templates) TEMPLATES=1 ;;230 --no-templates) TEMPLATES=0 ;;231 --handover) HANDOVER=1 ;;232 --no-handover) HANDOVER=0 ;;233 --reboot) REBOOT=1 ;;234 --no-reboot) REBOOT=0 ;;235 --node=*) set_node "${1#--node=}" ;;236 --node)237 [[ $# -ge 2 ]] || die_usage "--node requires nodesource|apt|none"238 set_node "$2"239 shift240 ;;241 --rust=*) set_rust "${1#--rust=}" ;;242 --rust)243 [[ $# -ge 2 ]] || die_usage "--rust requires rustup|apt|none"244 set_rust "$2"245 shift246 ;;247 --*) die_usage "unknown option: $1" ;;248 *) die_usage "unexpected argument: $1" ;;249 esac250 shift251 done252}253 254login_user() {255 if [[ -n "${SUDO_USER:-}" && "${SUDO_USER}" != root ]]; then256 printf '%s\n' "$SUDO_USER"257 return 0258 fi259 local u260 u=$(logname 2>/dev/null || true)261 if [[ -n "$u" && "$u" != root ]]; then262 printf '%s\n' "$u"263 return 0264 fi265 if [[ -n "${USER:-}" && "${USER}" != root ]]; then266 printf '%s\n' "$USER"267 return 0268 fi269 printf 'root\n'270}271 272login_home() {273 local u home274 u=$(login_user)275 home=$(getent passwd "$u" | awk -F: '{print $6}')276 if [[ -n "$home" ]]; then277 printf '%s\n' "$home"278 return 0279 fi280 printf '/home/%s\n' "$u"281}282 283run_as_login() {284 local user285 user=$(login_user)286 if [[ "$user" == root ]] || [[ "$(id -un)" == "$user" ]]; then287 "$@"288 return289 fi290 sudo -u "$user" -H -- "$@"291}292 293pkg_installed() {294 local st295 st=$(dpkg-query -W -f='${Status}' "$1" 2>/dev/null) || return 1296 [[ "$st" == "install ok installed" ]]297}298 299all_installed() {300 local p301 for p in "$@"; do302 pkg_installed "$p" || return 1303 done304 return 0305}306 307apt_install() {308 if [[ $# -eq 0 ]]; then309 return 0310 fi311 if all_installed "$@"; then312 log "Already installed: $*"313 return 0314 fi315 if ((DRY_RUN)); then316 printf 'dry-run: apt-get install -y'317 printf ' %q' "$@"318 printf '\n'319 return 0320 fi321 apt-get install -y "$@" </dev/null || die_run "apt-get install failed"322}323 324need_package_phase() {325 if ((GNOME || C_TOOLCHAIN || CLANG || CROSS || PYTHON || PYTHON_DS || PYTHON_DEBUG || PACKAGING || FIRMWARE)); then326 return 0327 fi328 [[ "$NODE" != none || "$RUST" != none ]]329}330 331only_firmware_requested() {332 ((FIRMWARE)) || return 1333 ((GNOME == 0 && C_TOOLCHAIN == 0 && CLANG == 0 && CROSS == 0 && PYTHON == 0 && PYTHON_DS == 0 && PYTHON_DEBUG == 0 && PACKAGING == 0 && GRUB == 0 && TEMPLATES == 0 && HANDOVER == 0)) || return 1334 [[ "$NODE" == none && "$RUST" == none ]]335}336 337sources_have_nonfree_firmware() {338 grep -Rqs -- 'non-free-firmware' /etc/apt/sources.list /etc/apt/sources.list.d339}340 341have_network() {342 if command -v ip >/dev/null 2>&1 && ip route show default 2>/dev/null | grep -q .; then343 return 0344 fi345 local d iface carrier346 for d in /sys/class/net/*; do347 [[ -e "$d" ]] || continue348 iface=$(basename "$d")349 [[ "$iface" != lo ]] || continue350 carrier=$(cat "$d/carrier" 2>/dev/null || printf '0')351 if [[ "$carrier" == 1 ]]; then352 return 0353 fi354 done355 return 1356}357 358node_is_v24() {359 local v360 command -v node >/dev/null 2>&1 || return 1361 v=$(node --version 2>/dev/null || true)362 [[ "$v" == v24* ]]363}364 365interfaces_is_loopback_only() {366 local f=/etc/network/interfaces367 local got368 [[ -f "$f" ]] || return 1369 got=$(grep -vE '^[[:space:]]*(#|$)' "$f" | sed 's/[[:space:]]*$//')370 [[ "$got" == $'auto lo\niface lo inet loopback' ]]371}372 373record_state() {374 local fence=$1375 ((DRY_RUN)) && return 0376 ((EUID == 0)) || return 0377 mkdir -p "$STATE_DIR"378 if [[ -f "$STATE_FILE" ]] && grep -qx "$fence" "$STATE_FILE"; then379 return 0380 fi381 printf '%s\n' "$fence" >>"$STATE_FILE"382}383 384preflight() {385 if [[ ! -f /etc/os-release ]]; then386 die_usage "cannot read /etc/os-release; Debian is required"387 fi388 # ID=debian only; Ubuntu and derivatives set a different ID.389 # shellcheck source=/dev/null390 . /etc/os-release391 if [[ "${ID:-}" != debian ]]; then392 die_usage "this script requires Debian (ID=debian); refusing ID=${ID:-unknown} (Ubuntu is not supported)"393 fi394 export DEBIAN_FRONTEND=noninteractive395 if ((AFTER_LOGIN)); then396 return 0397 fi398 if ! have_network; then399 if ((DRY_RUN)); then400 err "warn: no default route or carrier; dry-run continues"401 else402 die_usage "no network (no default route). Keep ifupdown until every download finishes"403 fi404 fi405}406 407apt_update_once() {408 if ((DRY_RUN)); then409 printf 'dry-run: apt-get update\n'410 return 0411 fi412 apt-get update </dev/null || die_run "apt-get update failed"413}414 415install_gnome() {416 ((GNOME)) || return 0417 log "Fence: GNOME (will not enable or start gdm3)"418 apt_install "${GNOME_PKGS[@]}"419 record_state gnome420}421 422install_c_toolchain() {423 ((C_TOOLCHAIN)) || return 0424 log "Fence: C/C++ toolchain"425 apt_install "${C_PKGS[@]}"426 record_state c-toolchain427}428 429install_clang() {430 ((CLANG)) || return 0431 log "Fence: clang"432 apt_install "${CLANG_PKGS[@]}"433 record_state clang434}435 436install_cross() {437 ((CROSS)) || return 0438 log "Fence: aarch64 cross gcc"439 apt_install "${CROSS_PKGS[@]}"440 record_state cross441}442 443install_python() {444 ((PYTHON)) || return 0445 log "Fence: Python (apt only; no pip into /usr)"446 apt_install "${PYTHON_PKGS[@]}"447 apt_install "${PYTHON_HELPER_PKGS[@]}"448 record_state python449}450 451install_python_ds() {452 ((PYTHON_DS)) || return 0453 log "Fence: optional Python DS/ML"454 apt_install "${PYTHON_DS_PKGS[@]}"455 record_state python-ds456}457 458install_python_debug() {459 ((PYTHON_DEBUG)) || return 0460 log "Fence: optional Python debug"461 apt_install "${PYTHON_DEBUG_PKGS[@]}"462 record_state python-debug463}464 465install_node() {466 local ns_tmp ver467 case "$NODE" in468 none)469 if ((NODE_SET)); then470 log "Fence: Node skipped (--node=none)"471 fi472 return 0473 ;;474 apt)475 log "Fence: Node from Debian apt"476 apt_install "${NODE_APT_PKGS[@]}"477 record_state node-apt478 ;;479 nodesource)480 log "Fence: Node 24 via NodeSource extra repo"481 if node_is_v24; then482 log "node $(node --version) already present; skipping NodeSource setup"483 record_state node-nodesource484 return 0485 fi486 if ! command -v curl >/dev/null 2>&1; then487 apt_install curl488 fi489 if ((DRY_RUN)); then490 printf 'dry-run: curl -fsSL %s -o <tempfile>\n' "$NODESOURCE_SETUP_URL"491 printf 'dry-run: bash <tempfile>\n'492 printf 'dry-run: apt-get install -y nodejs\n'493 return 0494 fi495 ns_tmp=$(mktemp)496 if ! curl -fsSL "$NODESOURCE_SETUP_URL" -o "$ns_tmp"; then497 rm -f "$ns_tmp"498 die_run "failed to download NodeSource setup_24.x"499 fi500 if ! bash "$ns_tmp" </dev/null; then501 rm -f "$ns_tmp"502 die_run "NodeSource setup_24.x failed"503 fi504 rm -f "$ns_tmp"505 apt-get install -y nodejs </dev/null || die_run "apt-get install nodejs failed"506 command -v node >/dev/null 2>&1 || die_run "node missing after NodeSource install"507 ver=$(node --version)508 if [[ "$ver" != v24* ]]; then509 die_run "expected node v24.x, got ${ver}"510 fi511 log "node ${ver}"512 record_state node-nodesource513 ;;514 esac515}516 517install_rust() {518 local user519 user=$(login_user)520 case "$RUST" in521 none)522 if ((RUST_SET)); then523 log "Fence: Rust skipped (--rust=none)"524 fi525 return 0526 ;;527 apt)528 log "Fence: Rust from Debian apt (do not prepend ~/.cargo/bin)"529 apt_install "${RUST_APT_PKGS[@]}"530 record_state rust-apt531 ;;532 rustup)533 log "Fence: rustup as login user ${user}"534 apt_install "${RUSTUP_PKGS[@]}"535 if ((DRY_RUN)); then536 printf 'dry-run: sudo -u %q -H rustup default stable\n' "$user"537 return 0538 fi539 run_as_login rustup default stable || die_run "rustup default stable failed for ${user}"540 record_state rust-rustup541 ;;542 esac543}544 545install_packaging() {546 ((PACKAGING)) || return 0547 log "Fence: Debian packaging toolchain"548 apt_install "${PACKAGING_PKGS[@]}"549 record_state packaging550}551 552install_firmware() {553 ((FIRMWARE)) || return 0554 log "Fence: optional firmware"555 if ! sources_have_nonfree_firmware; then556 err "warn: APT sources lack non-free-firmware; skipping firmware packages"557 err "Add that component, run apt-get update once, and retry --firmware (debian-os-setup.md)."558 return 0559 fi560 if all_installed "${FIRMWARE_PKGS[@]}"; then561 log "Already installed: ${FIRMWARE_PKGS[*]}"562 record_state firmware563 return 0564 fi565 if ((DRY_RUN)); then566 printf 'dry-run: apt-get install -y'567 printf ' %q' "${FIRMWARE_PKGS[@]}"568 printf '\n'569 return 0570 fi571 if ! apt-get install -y "${FIRMWARE_PKGS[@]}" </dev/null; then572 err "Firmware packages missing. Add non-free-firmware to APT sources if needed, apt-get update once, retry --firmware."573 if only_firmware_requested; then574 die_run "firmware fence failed"575 fi576 return 0577 fi578 record_state firmware579}580 581configure_grub() {582 ((GRUB)) || return 0583 local file=/etc/default/grub584 log "Fence: GRUB colors"585 if [[ ! -f "$file" ]]; then586 err "warn: ${file} missing; skipping GRUB colors"587 return 0588 fi589 if grep -qx 'GRUB_COLOR_NORMAL="light-gray/black"' "$file" &&590 grep -qx 'GRUB_COLOR_HIGHLIGHT="white/black"' "$file"; then591 log "GRUB colors already set"592 record_state grub593 return 0594 fi595 if ((DRY_RUN)); then596 printf 'dry-run: set GRUB_COLOR_NORMAL and GRUB_COLOR_HIGHLIGHT in %s\n' "$file"597 printf 'dry-run: update-grub\n'598 return 0599 fi600 if grep -q '^GRUB_COLOR_NORMAL=' "$file"; then601 sed -i 's|^GRUB_COLOR_NORMAL=.*|GRUB_COLOR_NORMAL="light-gray/black"|' "$file"602 elif grep -q '^#GRUB_COLOR_NORMAL=' "$file"; then603 sed -i 's|^#GRUB_COLOR_NORMAL=.*|GRUB_COLOR_NORMAL="light-gray/black"|' "$file"604 else605 printf 'GRUB_COLOR_NORMAL="light-gray/black"\n' >>"$file"606 fi607 if grep -q '^GRUB_COLOR_HIGHLIGHT=' "$file"; then608 sed -i 's|^GRUB_COLOR_HIGHLIGHT=.*|GRUB_COLOR_HIGHLIGHT="white/black"|' "$file"609 elif grep -q '^#GRUB_COLOR_HIGHLIGHT=' "$file"; then610 sed -i 's|^#GRUB_COLOR_HIGHLIGHT=.*|GRUB_COLOR_HIGHLIGHT="white/black"|' "$file"611 else612 printf 'GRUB_COLOR_HIGHLIGHT="white/black"\n' >>"$file"613 fi614 update-grub || die_run "update-grub failed"615 record_state grub616}617 618configure_templates() {619 ((TEMPLATES)) || return 0620 local user home dir621 user=$(login_user)622 home=$(login_home)623 dir="${home}/Templates"624 log "Fence: Nautilus templates in ${dir} (user ${user})"625 if ((DRY_RUN)); then626 printf 'dry-run: mkdir -p %q\n' "$dir"627 printf 'dry-run: touch %q/markdown.md %q/python.py %q/shell.sh %q/text.txt\n' "$dir" "$dir" "$dir" "$dir"628 return 0629 fi630 mkdir -p "$dir" || die_run "mkdir ${dir} failed"631 touch "${dir}/markdown.md" "${dir}/python.py" "${dir}/shell.sh" "${dir}/text.txt" || die_run "touch templates failed"632 if [[ "$user" != root ]]; then633 chown "${user}:" "$dir" "${dir}/markdown.md" "${dir}/python.py" "${dir}/shell.sh" "${dir}/text.txt" || die_run "chown templates to ${user} failed"634 fi635 record_state templates636}637 638write_interfaces_loopback() {639 if ((DRY_RUN)); then640 printf 'dry-run: write loopback-only /etc/network/interfaces\n'641 HANDOVER_MUTATED=1642 return 0643 fi644 tee /etc/network/interfaces >/dev/null <<'EOF'645# The loopback network interface646auto lo647iface lo inet loopback648EOF649}650 651configure_handover() {652 ((HANDOVER)) || return 0653 local def en654 log "Fence: handover (enable NetworkManager, no --now / no restart)"655 if ((DRY_RUN)); then656 printf 'dry-run: systemctl set-default graphical.target\n'657 printf 'dry-run: cp /etc/network/interfaces /etc/network/interfaces.bak # if bak missing\n'658 printf 'dry-run: write loopback-only /etc/network/interfaces\n'659 printf 'dry-run: systemctl enable NetworkManager\n'660 HANDOVER_MUTATED=1661 record_state handover662 return 0663 fi664 def=$(systemctl get-default)665 if [[ "$def" != graphical.target ]]; then666 systemctl set-default graphical.target || die_run "systemctl set-default graphical.target failed"667 HANDOVER_MUTATED=1668 else669 log "graphical.target already default"670 fi671 if [[ -f /etc/network/interfaces && ! -e /etc/network/interfaces.bak ]]; then672 cp /etc/network/interfaces /etc/network/interfaces.bak673 HANDOVER_MUTATED=1674 fi675 if interfaces_is_loopback_only; then676 log "/etc/network/interfaces already loopback-only"677 else678 write_interfaces_loopback679 HANDOVER_MUTATED=1680 fi681 en=$(systemctl is-enabled NetworkManager 2>/dev/null || true)682 if [[ "$en" != enabled ]]; then683 systemctl enable NetworkManager || die_run "systemctl enable NetworkManager failed"684 HANDOVER_MUTATED=1685 else686 log "NetworkManager already enabled"687 fi688 record_state handover689}690 691verify_handover() {692 ((HANDOVER)) || return 0693 local def en694 if ((DRY_RUN)); then695 printf 'dry-run: systemctl get-default # expect graphical.target\n'696 printf 'dry-run: systemctl is-enabled NetworkManager # expect enabled\n'697 printf 'dry-run: check /etc/network/interfaces is loopback-only\n'698 return 0699 fi700 def=$(systemctl get-default)701 [[ "$def" == graphical.target ]] || die_run "get-default is ${def}, expected graphical.target"702 en=$(systemctl is-enabled NetworkManager 2>/dev/null || true)703 [[ "$en" == enabled ]] || die_run "NetworkManager is-enabled=${en}, expected enabled"704 interfaces_is_loopback_only || die_run "/etc/network/interfaces is not loopback-only"705 log "Handover verify: graphical.target, NetworkManager enabled, interfaces loopback-only"706}707 708print_phase4() {709 cat <<'EOF'710 711Phase 4 is not automated (GDM login, GNOME Settings, optional Astro).712After reboot:713 1. Log in at GDM.714 2. Configure wifi or ethernet in GNOME Settings so NetworkManager owns the connection.715 3. Optional Astro only after NM has network, and only on the NodeSource 24 path:716 npm create astro@latest717 Do not npm install -g.718 4. Verify: bash /tmp/debian-os-setup.sh --after-login (or scripts/setup/setup.sh --after-login)719 720Do not start GDM or restart NetworkManager on this TTY before reboot.721EOF722}723 724maybe_reboot() {725 if ((REBOOT == 0 || HANDOVER == 0)); then726 log "Not rebooting (--no-reboot or handover skipped). Reboot when ready: systemctl reboot"727 return 0728 fi729 if ((DRY_RUN)); then730 printf 'dry-run: systemctl reboot # if handover changed this run\n'731 return 0732 fi733 if ((HANDOVER_MUTATED == 0)); then734 log "Handover already in place; not rebooting. Use systemctl reboot when ready."735 return 0736 fi737 log "Rebooting now (handover changed this run)..."738 systemctl reboot739}740 741# rustup's PATH rustc exits 1 when HOME has no toolchain; do not abort --after-login.742try_tool_version() {743 local exe=$1744 shift745 local user resolved out746 user=$(login_user)747 if [[ "$exe" == /* ]]; then748 resolved=$exe749 elif ((EUID == 0)) && [[ "$user" != root ]]; then750 resolved=$(sudo -u "$user" -H -- bash -c 'command -v "$1"' _ "$exe" 2>/dev/null) || return 1751 else752 resolved=$(command -v "$exe" 2>/dev/null) || return 1753 fi754 [[ -n "$resolved" ]] || return 1755 if ((EUID == 0)) && [[ "$user" != root ]]; then756 out=$(sudo -u "$user" -H -- "$resolved" "$@" 2>/dev/null) || return 1757 else758 [[ -x "$resolved" ]] || return 1759 out=$("$resolved" "$@" 2>/dev/null) || return 1760 fi761 [[ -n "$out" ]] || return 1762 printf '%s\n' "$out"763}764 765print_tool_version() {766 local bin=$1767 local extra_bin=${2:-}768 local -a vargs=(--version)769 if [[ "$bin" == rustc ]]; then770 vargs=(--version --verbose)771 fi772 if try_tool_version "$bin" "${vargs[@]}"; then773 return 0774 fi775 if [[ -n "$extra_bin" ]] && try_tool_version "$extra_bin" "${vargs[@]}"; then776 return 0777 fi778 log "${bin}: not installed"779}780 781nm_has_connection() {782 command -v nmcli >/dev/null 2>&1 || return 1783 local out784 out=$(nmcli -t -f TYPE,DEVICE con show --active 2>/dev/null || true)785 [[ -n "$out" ]] || return 1786 printf '%s\n' "$out" | grep -vq '^loopback:'787}788 789after_login_flow() {790 preflight791 local home792 home=$(login_home)793 log "After-login verify (no installs, no handover rewrite, no reboot)"794 print_tool_version python3795 print_tool_version node796 print_tool_version npm797 print_tool_version rustc "${home}/.cargo/bin/rustc"798 if command -v systemctl >/dev/null 2>&1; then799 log "get-default: $(systemctl get-default 2>/dev/null || printf 'unknown')"800 log "NetworkManager: $(systemctl is-enabled NetworkManager 2>/dev/null || printf 'unknown')"801 fi802 if ! nm_has_connection; then803 log "No NetworkManager connection. Configure wifi or ethernet in GNOME Settings so NetworkManager owns the connection."804 fi805}806 807maybe_sudo_reexec() {808 ((EUID == 0)) && return 0809 command -v sudo >/dev/null 2>&1 || die_usage "root or sudo is required"810 if [[ -n "$SCRIPT_PATH" && -f "$SCRIPT_PATH" ]]; then811 exec sudo bash "$SCRIPT_PATH" "$@"812 fi813 die_usage "piped invoke requires root. Use: curl -fsSL https://debian.reporet.com/setup.sh | sudo bash"814}815 816run_setup() {817 preflight818 if ((DEV_ALL)); then819 log "Mode: dev-all"820 elif ((DEV)); then821 log "Mode: dev"822 else823 log "Mode: standard"824 fi825 if need_package_phase; then826 log "Phase 1: packages (apt-get update once)"827 apt_update_once828 install_gnome829 install_c_toolchain830 install_clang831 install_cross832 install_python833 install_python_ds834 install_python_debug835 install_node836 install_rust837 install_packaging838 install_firmware839 else840 log "Phase 1: no package fences requested"841 fi842 log "Phase 2: local config"843 configure_grub844 configure_templates845 log "Phase 3: handover"846 configure_handover847 verify_handover848 print_phase4849 maybe_reboot850}851 852parse_args "$@"853apply_mode_defaults854 855if ((AFTER_LOGIN)); then856 if [[ ! -t 0 ]]; then857 err "error: stdin is not a TTY. Do not use curl | bash (sudo/apt can eat the script)."858 err "Save the file, then: bash setup.sh --after-login"859 exit 1860 fi861 after_login_flow862 exit 0863fi864 865if ((DRY_RUN == 0)) && [[ ! -t 0 ]] && ((EUID != 0)); then866 err "error: stdin is not a TTY and you are not root."867 err "Pipe as root: curl -fsSL https://debian.reporet.com/setup.sh | sudo bash"868 err "Or save a file, then: sudo bash /tmp/debian-os-setup.sh"869 exit 1870fi871 872if ((DRY_RUN == 0)); then873 maybe_sudo_reexec "$@"874fi875 876run_setup877exit 0878}