RReporet
Catalog

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 bash

Pipes remote code to root. Read the source on this page first. Fetch only: curl -fsSL https://<host>/setup.sh

Open raw filehttps://<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}