#!/usr/bin/env bash # # nosignal.sh — one script: turn a stock Arch ISO into a graphical Arch desktop # installer ISO. The installed system boots into SDDM (themed to match the # desktop — layer changes 16+19; was greetd+tuigreet) and runs Hyprland, # themed with the caelestia dotfiles # (https://github.com/caelestia-dots/caelestia). # # Architecture (Phase 3 — FULLY OFFLINE): the ISO bundles EVERYTHING needed # for the install. At build time, this script: # * builds the caelestia AUR packages (shell/CLI/meta + quickshell-git and # friends) in a devtools clean chroot on the build host, # * downloads the complete pacstrap dependency tree (including every GPU # driver variant and the prebuilt Limine snapshot tools from the omarchy # repo) into a local pacman repo, # * vendors the caelestia dotfiles + quickshell-overview as pinned tarballs, # and places all of it on the ISO9660 (outside the squashfs, visible to the # live environment at /run/archiso/bootmnt/nosignal). The installer pacstraps # entirely from that file:// repo and lays down the whole desktop — dotfiles, # theming, workspace overview, Btrfs+Limine snapshots — at INSTALL TIME in the # chroot. No network is needed at install or on first boot: the machine boots # straight into the themed SDDM greeter and a fully themed desktop. (The old # first-boot build stage — paru bootstrap, nmtui gate, tty1 autologin — is gone.) # # Network IS needed on the BUILD host (AUR builds + repo download), and the # Wi-Fi prompt remains in the installer as an OPTIONAL step purely so the # credentials can be persisted into the installed system. # # NOTE: this is the *ISO builder*. It bundles and installs the upstream # caelestia dotfiles; it is not affiliated with that project. # # Usage: # 1. Drop a stock Arch ISO (https://archlinux.org/download/, file starts # with 'archlinux-') into the same folder as this script. # 2. ./nosignal.sh # 3. Output: nosignal-arch-YYYYMMDD.iso (Ventoy-compatible, dd-bootable). # # The offline payload is cached in ./offline/ — subsequent builds reuse the # built AUR packages and previously downloaded repo packages. Delete that # folder (or individual .built-* stamps in offline/aur/) to force a rebuild. # # Dependencies (build host): xorriso, squashfs-tools, git, sha512sum, sudo, # devtools (mkarchroot/makechrootpkg), pacman-contrib (paccache). set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" WORK="$SCRIPT_DIR/work" OFFLINE="$SCRIPT_DIR/offline" INVOKING_USER="${SUDO_USER:-$(id -un)}" INVOKING_GROUP="$(id -gn "$INVOKING_USER")" OUT_ISO="$SCRIPT_DIR/nosignal-arch-$(date +%Y%m%d).iso" # ---------------------------------------------------------------- pins ------ # The validated combination (VM-certified): caelestia dotfiles + the AUR # release PKGBUILDs current at that commit, quickshell pinned to the commit # the shell was validated against, and the overview sidecar pin. CAELESTIA_DOTS_REPO="https://github.com/caelestia-dots/caelestia.git" CAELESTIA_DOTS_COMMIT="757b7f26b8e637754ddd498671360dd6ba7e89ef" OVERVIEW_REPO="https://github.com/Shanu-Kumawat/quickshell-overview.git" OVERVIEW_COMMIT="74918ba66e0714017a6a5bfb9bf2affa66dabbc1" QUICKSHELL_GIT_COMMIT="b66495f" # short hash is fine for makepkg #commit= # NoSignal's fork of the caelestia shell — we BUILD this instead of the upstream # caelestia-shell so the interface/layout is under our control and never gets # overwritten by an upstream update (the package provides/conflicts caelestia-shell # and is pinned here, so pacman/yay never replaces it). Updates are deliberate: # advance the fork's `nosignal` branch, bump the pin, rebuild. The fork's # packaging/PKGBUILD builds the shell from this exact commit. NOSIGNAL_SHELL_REPO="https://github.com/28allday/nosignal-shell.git" NOSIGNAL_SHELL_COMMIT="0ae859311310661555dae089c91f2c0bfaaec10b" # OS theme wallpapers — the user's own Moebius-style generated art (11 pieces, # 2944x1648, repo-local in assets/wallpapers/; replaced the third-party Last # Horizon set 2026-06-11, which also clears the redistribution-rights concern). # The desktop palette is generated FROM the default wallpaper by caelestia's # dynamic Material scheme at install time. WALLPAPERS_DIR="$SCRIPT_DIR/assets/wallpapers" # foam-sea since change 18 (was wanderer.png). Build-time default only — no # migration, so existing installs keep whatever the user picked. DEFAULT_WALLPAPER="foam-sea.png" # omarchy-send release binary (LocalSend-compatible LAN file transfer TUI). OMARCHY_SEND_REPO="28allday/omarchy-send" OMARCHY_SEND_VERSION="v0.1.11" # NoSignal layer — the "os updates" round developed and live-tested on real # hardware (keybind cheatsheet, bash default shell, nosignal-update, yay+Shelly # store, Omarchy keybindings, omadots polish, hyprmoncfg, settings Updates # page, system polish). The tree is vendored verbatim onto the ISO and baked # into ~/.local/share/nosignal at install time. Dev-only docs (changelog, # builder/test notes) are kept unshipped in the repo's dev-docs/ folder. NOSIGNAL_LAYER_DIR="$SCRIPT_DIR/os updates" # LazyVim starter (omadots' nvim setup, change 6) — pinned, .git stripped. LAZYVIM_REPO="https://github.com/LazyVim/starter" LAZYVIM_COMMIT="803bc181d7c0d6d5eeba9274d9be49b287294d99" # Flathub remote definition (change 4). The .flatpakrepo file embeds the GPG # key, so `flatpak remote-add` from it works OFFLINE in the install chroot. FLATHUB_REPO_URL="https://dl.flathub.org/repo/flathub.flatpakrepo" # ----------------------------------------------------- package manifests ---- # BASE_PKGS is the single source of truth for the non-GPU pacstrap set: it is # written to the ISO as /nosignal/base-packages.list and read back by the # installer (so the download closure and the installed set can never drift). BASE_PKGS=( base linux linux-firmware intel-ucode amd-ucode networkmanager openssh sudo git curl wget vim less base-devel limine efibootmgr zram-generator btrfs-progs snapper snap-pac pipewire pipewire-pulse pipewire-alsa pipewire-jack wireplumber polkit hyprpolkitagent qt5-wayland qt6-wayland xdg-user-dirs xdg-utils hyprland uwsm xdg-desktop-portal-hyprland xdg-desktop-portal-gtk hyprpicker hyprlock hypridle hyprsunset wl-clipboard cliphist inotify-tools grim slurp swappy brightnessctl playerctl pavucontrol # Display manager: SDDM (layer change 16 — DeckShift's desktop<->gaming # session switching rewrites SDDM config; greetd/tuigreet couldn't do this). # xorg-server is for SDDM's X11 GREETER only (DisplayServer=x11, the # reliable path) — the Hyprland session itself stays Wayland. sddm xorg-server plymouth # NB: fish must STAY even though bash is the default shell (NoSignal layer # change 2) — caelestia-meta hard-depends on it. foot stays as a meta dep # too (its menu entries are hidden by the layer's system-polish change). foot fish fastfetch starship btop jq eza trash-cli adw-gtk-theme papirus-icon-theme ttf-jetbrains-mono-nerd ttf-material-symbols-variable ttf-cascadia-code-nerd noto-fonts noto-fonts-emoji kitty-terminfo foot-terminfo ufw caelestia-meta # default terminal (foot stays as caelestia-themed fallback) kitty # everyday apps chromium nautilus gvfs gvfs-mtp mpv imv papers gnome-calculator gnome-text-editor libreoffice-fresh # default music player (layer change 24): CLIAmp, prebuilt in [omarchy] # (no AUR build); pulls ffmpeg + yt-dlp into the closure cliamp # monitor layout/profiles TUI + hotplug daemon (writes the same # ~/.config/hypr/monitors.conf the base config sources; superseded # nwg-displays — keep a SINGLE writer of that file). AUR-built. hyprmoncfg # software installation story: yay (AUR helper, also used by # nosignal-update) + Shelly GUI store (both AUR-built) + flatpak backend flatpak archlinux-appstream-data yay-bin shelly-bin # NoSignal layer tooling: checkupdates (updates panel), desktop notify, # lspci for the Additions page's GPU-detected OBS encoders (change 25 — # present on the test boxes only as an incidental dep; make it explicit) pacman-contrib libnotify pciutils # omadots developer polish (starship/btop/eza already above) tmux neovim fzf bat zoxide # base default dev tooling (layer change 33): GitHub CLI (`gh`) github-cli # bluetooth bluez bluez-utils blueman # laptop: audio firmware + power profiles sof-firmware power-profiles-daemon # printing + network discovery cups system-config-printer avahi nss-mdns # GUI Wi-Fi fallback (non-CLI escape hatch beside the bar's Wi-Fi panel) + # GL/Vulkan info tools so `prime-run glxinfo`/`vulkaninfo` work for verifying # hybrid-GPU offload (hardware-test gap, 2026-06-20) nm-connection-editor mesa-utils vulkan-tools ) # Every GPU driver variant goes into the offline repo; the installer detects # the hardware and installs only the matching subset. KEEP IN SYNC with the # GPU detection block inside write_installer below. GPU_ALL_PKGS=( mesa vulkan-icd-loader nvidia-open-dkms nvidia-utils egl-wayland libva-nvidia-driver nvidia-settings linux-headers nvidia-prime # prime-run offload (hybrid laptops) vulkan-radeon vulkan-intel intel-media-driver qemu-guest-agent spice-vdagent vulkan-swrast ) # Prebuilt binaries from the omarchy repo (skips their GraalVM AUR build). LIMINE_TOOLS=(limine-snapper-sync limine-mkinitcpio-hook) # AUR packages built in the clean chroot, in dependency order (each build can # resolve the previously built ones through the local bootstrap repo). # caelestia-meta is NOT here — it builds from the dotfiles clone on the host. AUR_BUILD_ORDER=( app2unit libcava python-materialyoucolor ttf-rubik-vf qtengine quickshell-git caelestia-cli nosignal-shell # NoSignal layer round — independent of the caelestia chain (yay-bin and # shelly-bin repackage release binaries; hyprmoncfg ships a Go release) yay-bin shelly-bin hyprmoncfg ) # Clean up the work dir on any exit (success, error, or Ctrl-C). 2>/dev/null # is in case $WORK never got created (e.g. we exited before mkdir). The # offline payload cache in $OFFLINE is deliberately NOT cleaned. trap 'sudo rm -rf "$WORK" 2>/dev/null || true' EXIT # =========================================================================== # write_installer — emit the live-ISO installer to the given path. # This function holds the script that runs on the *target* machine inside # the live ISO (auto-launched on tty1). It is intentionally embedded here so # this file is the only artifact needed to rebuild the installer ISO. # =========================================================================== write_installer() { cat > "$1" <<'__INSTALLER_PAYLOAD__' #!/usr/bin/env bash set -euo pipefail MARKER="/tmp/.installer-ran" if [ -f "$MARKER" ]; then echo echo " Installer already ran this session." echo " To retry: rm $MARKER && bash $0" echo exec /bin/bash fi touch "$MARKER" # Mirror stdout/stderr to /tmp/installer.log. If the script aborts, the user # can switch to tty2, log in as root, and `cat /tmp/installer.log` to see the # last "==> ..." marker reached and any error printed by the failing command. exec > >(tee /tmp/installer.log) 2>&1 if [ ! -d /sys/firmware/efi ]; then echo "ERROR: not booted in UEFI mode. UEFI-only installer." >&2 exec /bin/bash fi clear # --- styled UI helpers (pure bash, no extra packages). They draw to /dev/tty # directly so the arrow-key redraws never spam the tee'd installer.log. The UI # is centre-aligned to the console width. -------------------------------------- NSI_G=$'\033[38;5;150m' # NoSignal green NSI_GB=$'\033[1;38;5;150m' # bold green NSI_DIM=$'\033[0;90m' # dim/grey NSI_B=$'\033[1m' NSI_R=$'\033[0m' # Console width (from the real console — stdout is tee'd to a pipe). Default 80. nsi_cols() { local c; c=$(stty size /dev/null | awk '{print $2}') case "$c" in ''|*[!0-9]*) c=80 ;; esac [ "$c" -ge 20 ] 2>/dev/null || c=80 printf '%s' "$c" } # Left-pad string for centring a block of visible width $1. nsi_pad() { local cols p; cols=$(nsi_cols); p=$(( (cols - $1) / 2 )); [ "$p" -lt 0 ] && p=0 printf '%*s' "$p" '' } # Centred line of PLAIN text. $2=colour code (optional), $3=reset (optional). cecho() { printf '%s%s%s%s\n' "$(nsi_pad "${#1}")" "${2:-}" "$1" "${3:-}"; } # Centred read into VAR: cread VAR "prompt". csecret = silent (passwords). cread() { printf '%s' "$(nsi_pad "${#2}")" >/dev/tty; read -rp "$2" "$1" /dev/tty; read -rsp "$2" "$1" sets MENU_CHOICE (0-based index) + MENU_VALUE. # Navigate with ↑/↓ (or j/k), Enter selects. Centre-aligned as a block. tui_menu() { local title="$1"; shift local -a opts=("$@") local n=${#opts[@]} sel=0 first=1 i key rest o w local lines=$(( n + 3 )) # title + blank + n options + hint local hint='↑/↓ move · Enter select' # Block width = widest of title, "❯ "+option, hint — then centre that block. local maxw=${#title} for o in "${opts[@]}"; do w=$(( ${#o} + 2 )); [ "$w" -gt "$maxw" ] && maxw=$w; done [ "${#hint}" -gt "$maxw" ] && maxw=${#hint} local pad; pad=$(nsi_pad "$maxw") printf '\033[?25l' >/dev/tty while true; do if [ "$first" -eq 1 ]; then first=0; else printf '\033[%dA' "$lines" >/dev/tty; fi { printf '\r\033[2K%s%s%s%s\n\r\033[2K\n' "$pad" "$NSI_GB" "$title" "$NSI_R" for i in "${!opts[@]}"; do if [ "$i" -eq "$sel" ]; then printf '\r\033[2K%s%s❯%s %s%s%s\n' "$pad" "$NSI_GB" "$NSI_R" "$NSI_B" "${opts[$i]}" "$NSI_R" else printf '\r\033[2K%s %s%s%s\n' "$pad" "$NSI_DIM" "${opts[$i]}" "$NSI_R" fi done printf '\r\033[2K%s%s%s%s\n' "$pad" "$NSI_DIM" "$hint" "$NSI_R" } >/dev/tty IFS= read -rsn1 key /dev/tty MENU_CHOICE=$sel MENU_VALUE="${opts[$sel]}" } # Install-progress UI. During the heavy install phase ALL command output is # redirected to /tmp/installer.log (off-screen) and the user sees only a single # CENTRED spinner + the current phase name on /dev/tty — calm and consistent # with the centred menus, no scrolling formatting/build-hook spam. Full detail # stays in the log (Ctrl+Alt+F2 -> cat /tmp/installer.log). NSI_PHASE_FILE=/tmp/nsi-phase NSI_SPIN_PID="" # Centred line drawn straight to the real console (stdout is the log mid-install). tcecho() { printf '%s%s%s%s\n' "$(nsi_pad "${#1}")" "${2:-}" "$1" "${3:-}" >/dev/tty; } # Set the phase label the spinner shows. nsi_phase() { printf '%s' "$1" > "$NSI_PHASE_FILE" 2>/dev/null || true; } nsi_spin_start() { printf '\033[?25l' >/dev/tty # hide cursor ( set +e local frames='/-\|' i=0 msg vis pad while true; do msg=$(cat "$NSI_PHASE_FILE" 2>/dev/null); [ -n "$msg" ] || msg="Working" vis=$(( ${#msg} + 5 )) # frame + 2 spaces + msg + ellipsis pad=$(nsi_pad "$vis") printf '\r\033[2K%s%s%s%s %s%s…%s' \ "$pad" "$NSI_GB" "${frames:i%4:1}" "$NSI_R" "$NSI_B" "$msg" "$NSI_R" >/dev/tty i=$((i+1)); sleep 0.15 done ) & NSI_SPIN_PID=$! } nsi_spin_stop() { [ -n "$NSI_SPIN_PID" ] && { kill "$NSI_SPIN_PID" 2>/dev/null || true; wait "$NSI_SPIN_PID" 2>/dev/null || true; NSI_SPIN_PID=""; } printf '\r\033[2K\033[?25h' >/dev/tty # clear the spinner line, show cursor } # ERR trap during install: stop the spinner, restore on-screen output, and show # which phase failed + the tail of the log (full detail on tty2). nsi_fail() { trap - ERR nsi_spin_stop exec >/dev/tty 2>&1 echo cecho "Install FAILED at: $(cat "$NSI_PHASE_FILE" 2>/dev/null)" "$NSI_GB" "$NSI_R" cecho "See the log on tty2: Ctrl+Alt+F2 → cat /tmp/installer.log" "$NSI_DIM" "$NSI_R" echo tail -n 25 /tmp/installer.log 2>/dev/null } # Banner — centred as a block. ASCII + box-drawing + Latin-1 middot only: the # Linux console font has no CJK glyphs, so the kana rendered as blocks and (being # double-width) pushed the right border out. The 入力信号なし wordmark lives on # the Plymouth splash (a PNG) instead. Borders are 46 cells; interior is 44. BPAD=$(nsi_pad 46) printf '%s%s%s\n' "$BPAD" "$NSI_GB" "┌────────────────────────────────────────────┐" printf '%s%s%s\n' "$BPAD" "$NSI_GB" "│ NoSignal · NO SIGNAL │" printf '%s%s%s\n' "$BPAD" "$NSI_GB" "└────────────────────────────────────────────┘" printf '%s' "$NSI_R" cecho "Arch · Hyprland · Caelestia · SDDM — fully offline installer" "$NSI_DIM" "$NSI_R" echo # ----------------------------------------------------- offline payload ------ # Everything installs from the package repo bundled on the install media. # archiso's copytoram defaults to AUTO and fires on every non-optical boot # (USB stick + enough RAM): the initramfs copies airootfs.sfs to RAM, then # UNMOUNTS and rmdir's /run/archiso/bootmnt — taking /nosignal with it. CD # boots are exempt, which is why VM -cdrom certification never caught it. # Remount the install media by the UUID archiso itself booted from. NOSIGNAL_PAYLOAD=/run/archiso/bootmnt/nosignal if [ ! -f "$NOSIGNAL_PAYLOAD/repo/nosignal.db" ]; then cecho "Boot media unmounted by copytoram — remounting the offline repo..." "$NSI_DIM" "$NSI_R" iso_uuid=$(sed -n 's/.*archisosearchuuid=\([^ ]*\).*/\1/p' /proc/cmdline) iso_label=$(sed -n 's/.*archisolabel=\([^ ]*\).*/\1/p' /proc/cmdline) iso_dev="" for _ in $(seq 1 15); do if [ -n "$iso_uuid" ] && [ -e "/dev/disk/by-uuid/$iso_uuid" ]; then iso_dev="/dev/disk/by-uuid/$iso_uuid"; break fi if [ -n "$iso_label" ] && [ -e "/dev/disk/by-label/$iso_label" ]; then iso_dev="/dev/disk/by-label/$iso_label"; break fi sleep 1 done if [ -n "$iso_dev" ]; then mkdir -p /run/archiso/bootmnt mount -o ro "$iso_dev" /run/archiso/bootmnt || true fi fi if [ ! -f "$NOSIGNAL_PAYLOAD/repo/nosignal.db" ] || [ ! -f "$NOSIGNAL_PAYLOAD/base-packages.list" ]; then echo "ERROR: offline package repo not found on the install media" >&2 echo " (expected $NOSIGNAL_PAYLOAD/repo). Bad burn or wrong ISO?" >&2 exec /bin/bash fi # Strip comments/blank lines so the list file can be annotated. mapfile -t BASE_PKGS < <(grep -vE '^\s*(#|$)' "$NOSIGNAL_PAYLOAD/base-packages.list") cecho "Offline repo found (${#BASE_PKGS[@]} base packages)." "$NSI_DIM" "$NSI_R" echo # pacman config used for every install-time operation: ONLY the bundled repo. # The installed system gets the normal online mirrors instead (written later). # SigLevel Never: the bundled packages were verified when the ISO was BUILT # (official sigs checked on download; AUR pkgs built locally) — the ISO is the # trust boundary. Anything weaker fails: e.g. 'Optional TrustAll' still tries # to VERIFY any signature embedded in the repo db and dies on the missing key. cat > /tmp/nosignal-pacman.conf <<'OFFLINECONF' [options] Architecture = x86_64 SigLevel = Never [nosignal] Server = file:///run/archiso/bootmnt/nosignal/repo OFFLINECONF # ---------------------------------------------------------------- network ---- # The install is fully offline — no network is needed here or on first boot. # Wired boxes DHCP automatically via NetworkManager; Wi-Fi users connect from # the desktop's Wi-Fi panel (click-to-connect) or nm-connection-editor on first # boot. So the installer asks for nothing here. # ---------------------------------------------------------------- prompts ---- while true; do cread HOSTNAME "Hostname [nosignal]: " HOSTNAME="${HOSTNAME:-nosignal}" [[ "$HOSTNAME" =~ ^[a-zA-Z0-9][a-zA-Z0-9-]{0,62}$ ]] && break cecho "Invalid (RFC 1123: letters/digits/hyphen, no leading hyphen, max 63 chars)." done while true; do cread USERNAME "Username: " [[ "$USERNAME" =~ ^[a-z_][a-z0-9_-]{0,30}$ ]] && break cecho "Invalid (lowercase, starts a-z or _, max 31 chars)." done while true; do csecret PW1 "Password for $USERNAME (also used for root): " csecret PW2 "Confirm password: " if [ -z "$PW1" ]; then cecho "Empty — try again."; continue; fi if [ "$PW1" != "$PW2" ]; then cecho "Mismatch — try again."; continue; fi break done USER_PW="$PW1" echo # Localisation — pick a region from a curated list; it sets the timezone, locale, # console keymap and Hyprland xkb layout together (no more three cryptic prompts). # "Other" drops to the search-based prompts for anything not listed. The curated # values are all standard tzdata/locale.gen/kbd names present on the ISO. tui_menu "Where are you? (sets time zone, language & keyboard)" \ "United Kingdom" \ "Ireland" \ "United States" \ "Canada" \ "Australia" \ "Germany" \ "France" \ "Spain" \ "Italy" \ "Netherlands" \ "Other (advanced — type & search)" case "$MENU_VALUE" in "United Kingdom") TIMEZONE=Europe/London; LOCALE=en_GB.UTF-8; KEYMAP=uk; XKB_LAYOUT=gb ;; "Ireland") TIMEZONE=Europe/Dublin; LOCALE=en_IE.UTF-8; KEYMAP=uk; XKB_LAYOUT=gb ;; "United States") TIMEZONE=America/New_York; LOCALE=en_US.UTF-8; KEYMAP=us; XKB_LAYOUT=us ;; "Canada") TIMEZONE=America/Toronto; LOCALE=en_CA.UTF-8; KEYMAP=us; XKB_LAYOUT=us ;; "Australia") TIMEZONE=Australia/Sydney; LOCALE=en_AU.UTF-8; KEYMAP=us; XKB_LAYOUT=us ;; "Germany") TIMEZONE=Europe/Berlin; LOCALE=de_DE.UTF-8; KEYMAP=de-latin1; XKB_LAYOUT=de ;; "France") TIMEZONE=Europe/Paris; LOCALE=fr_FR.UTF-8; KEYMAP=fr-latin1; XKB_LAYOUT=fr ;; "Spain") TIMEZONE=Europe/Madrid; LOCALE=es_ES.UTF-8; KEYMAP=es; XKB_LAYOUT=es ;; "Italy") TIMEZONE=Europe/Rome; LOCALE=it_IT.UTF-8; KEYMAP=it; XKB_LAYOUT=it ;; "Netherlands") TIMEZONE=Europe/Amsterdam; LOCALE=nl_NL.UTF-8; KEYMAP=us; XKB_LAYOUT=us ;; *) # Other (advanced): the previous search-based prompts. Enter '?text' to search. while true; do cread TIMEZONE "Timezone [Europe/London] ('?text' searches): " TIMEZONE="${TIMEZONE:-Europe/London}" if [[ "$TIMEZONE" == \?* ]]; then timedatectl list-timezones 2>/dev/null | grep -i -- "${TIMEZONE#\?}" | head -25 | sed 's/^/ /' continue fi [ -f "/usr/share/zoneinfo/$TIMEZONE" ] && break cecho "Not found (e.g. Europe/London, America/New_York, Australia/Sydney)." done while true; do cread LOCALE "Locale [en_GB.UTF-8] ('?text' searches): " LOCALE="${LOCALE:-en_GB.UTF-8}" if [[ "$LOCALE" == \?* ]]; then grep -oE '^#?[^ ]+ ' /etc/locale.gen | tr -d '# ' | grep -i -- "${LOCALE#\?}" | head -25 | sed 's/^/ /' continue fi grep -oE '^#?[^ ]+ ' /etc/locale.gen | tr -d '# ' | grep -qxF "$LOCALE" && break cecho "Not found (e.g. en_GB.UTF-8, en_US.UTF-8, de_DE.UTF-8)." done while true; do cread KEYMAP "Console keymap [uk] ('?text' searches): " KEYMAP="${KEYMAP:-uk}" if [[ "$KEYMAP" == \?* ]]; then localectl list-keymaps 2>/dev/null | grep -i -- "${KEYMAP#\?}" | head -25 | sed 's/^/ /' continue fi localectl list-keymaps 2>/dev/null | grep -qxF "$KEYMAP" && break cecho "Not found (e.g. uk, us, de-latin1, fr-latin1)." done # Map the console keymap to an XKB layout for Hyprland's kb_layout. The names # usually agree (us, de, it, es, pl...); this table covers the common # exceptions, and the fallback strips variant suffixes (cz-qwertz -> cz). case "$KEYMAP" in uk) XKB_LAYOUT="gb" ;; fr-latin1|fr-latin9) XKB_LAYOUT="fr" ;; pt-latin1|pt-latin9) XKB_LAYOUT="pt" ;; br-abnt2) XKB_LAYOUT="br" ;; la-latin1) XKB_LAYOUT="latam" ;; sv-latin1) XKB_LAYOUT="se" ;; sg|sg-latin1) XKB_LAYOUT="ch" ;; dvorak*) XKB_LAYOUT="us" ;; *) XKB_LAYOUT="${KEYMAP%%[-.]*}" ;; esac ;; esac cecho "Region set: ${TIMEZONE} · ${LOCALE} · keymap ${KEYMAP}" "$NSI_DIM" "$NSI_R" # Disk — arrow-key pick from the real disks, then a plain Yes/No confirm # (defaults to No). No more typing the device path. echo mapfile -t DISKS < <(lsblk -d -n -p -o NAME,SIZE,MODEL | grep -Ev '/dev/(loop|sr|zram|fd)') if [ "${#DISKS[@]}" -eq 0 ]; then cecho "No disks found. Dropping to shell." exec /bin/bash fi while true; do DISK_LABELS=() for d in "${DISKS[@]}"; do DISK_LABELS+=("$(awk '{name=$1; size=$2; $1=""; $2=""; sub(/^[[:space:]]+/,""); printf "%-16s %-9s %s", name, size, $0}' <<<"$d")") done tui_menu "Install to which disk? (everything on it is erased)" "${DISK_LABELS[@]}" DISK=$(awk '{print $1}' <<<"${DISKS[$MENU_CHOICE]}") tui_menu "Erase $DISK and install NoSignal?" \ "No — choose a different disk" \ "Yes — ERASE $DISK and install" [ "$MENU_CHOICE" -eq 1 ] && break done if [[ "$DISK" =~ nvme|mmcblk ]]; then EFI_PART="${DISK}p1"; ROOT_PART="${DISK}p2" else EFI_PART="${DISK}1"; ROOT_PART="${DISK}2" fi # ---------------------------------------------------------------- install ---- # Go quiet + centred for the heavy phase: the prompts are done, so hide every # command's output to the log and show a single centred spinner with the current # phase name (consistent with the centred menus, no formatting/build-hook spam). clear # Silence kernel printk on the console — disk format/mount triggers harmless # btrfs/ext4 probe messages ("VFS: Can't find ext4 filesystem") that write # straight to the VT and would splatter over the clean spinner. (dmesg keeps # them; this only stops console printing. Not restored — a reboot follows.) dmesg -D 2>/dev/null || echo 1 > /proc/sys/kernel/printk 2>/dev/null || true tcecho "Installing NoSignal" "$NSI_GB" "$NSI_R" tcecho "this takes a few minutes — sit tight" "$NSI_DIM" "$NSI_R" printf '\n' >/dev/tty nsi_phase "Preparing the disk" exec >>/tmp/installer.log 2>&1 trap nsi_fail ERR nsi_spin_start echo "==> Wiping $DISK..." swapoff -a || true umount -R /mnt 2>/dev/null || true wipefs -af "$DISK" sgdisk --zap-all "$DISK" echo "==> Partitioning (1G EFI + rest btrfs)..." # 1 GiB ESP (was 512M): UKIs are self-contained kernel+initramfs+microcode, and # the NVIDIA UKI is ~139 MB (driver + GSP firmware). With ENABLE_LIMINE_FALLBACK # (a 2nd UKI) plus multiple kernel versions, a 512 MB ESP can fill and UKI writes # then fail (finding F4 secondary risk). 1 GiB is safe headroom on every GPU. sgdisk -n1:0:+1G -t1:ef00 -c1:EFI "$DISK" sgdisk -n2:0:0 -t2:8300 -c2:ROOT "$DISK" partprobe "$DISK" udevadm settle echo "==> Formatting (btrfs root for Limine snapshots)..." mkfs.fat -F32 -n EFI "$EFI_PART" mkfs.btrfs -f -L ROOT "$ROOT_PART" # Btrfs subvolume layout for snapshot/rollback (root-only snapshots): # @ -> / (snapshotted + rolled back) # @home -> /home (separate, so a rollback never reverts user data) # @snapshots -> /.snapshots (separate, so snapshots survive a rollback of @) # @log -> /var/log (separate, so logs survive a rollback — useful) echo "==> Creating btrfs subvolumes..." mount "$ROOT_PART" /mnt btrfs subvolume create /mnt/@ btrfs subvolume create /mnt/@home btrfs subvolume create /mnt/@snapshots btrfs subvolume create /mnt/@log umount /mnt echo "==> Mounting subvolumes..." BTRFS_OPTS="noatime,compress=zstd,space_cache=v2" mount -o "$BTRFS_OPTS,subvol=@" "$ROOT_PART" /mnt mkdir -p /mnt/boot /mnt/home /mnt/.snapshots /mnt/var/log mount -o "$BTRFS_OPTS,subvol=@home" "$ROOT_PART" /mnt/home mount -o "$BTRFS_OPTS,subvol=@snapshots" "$ROOT_PART" /mnt/.snapshots mount -o "$BTRFS_OPTS,subvol=@log" "$ROOT_PART" /mnt/var/log mount "$EFI_PART" /mnt/boot # ------------------------------------------------------------ GPU drivers ---- # Hardware-detect the GPU(s) and pick driver packages, borrowing CachyOS chwd's # method: scan PCI display-class controllers (class 0300/0302/0380) and map the # vendor id (10de NVIDIA / 1002 AMD / 8086 Intel) to a driver set — plus the VM # vendors so Hyprland comes up in a guest. Done natively (no chwd binary, no # CachyOS repos / lib32 / cachyos-kernel deps) so the ISO stays vanilla Arch. # Multiple GPUs union their package sets (e.g. an Intel/AMD iGPU + NVIDIA dGPU). # ALL variants below are present in the offline repo (see GPU_ALL_PKGS in the # builder — keep the two in sync). echo "==> Detecting GPU(s)..." GPU_NVIDIA=0; GPU_AMD=0; GPU_INTEL=0; GPU_VM=0 IGPU_PCI_ID="" # vendor:device of the iGPU (for hybrid Vulkan device steering) while IFS= read -r gpuline; do case "$gpuline" in *'[10de:'*) GPU_NVIDIA=1 ;; *'[1002:'*) GPU_AMD=1; [ -z "$IGPU_PCI_ID" ] && IGPU_PCI_ID=$(printf '%s\n' "$gpuline" | grep -oE '1002:[0-9a-fA-F]{4}' | head -1) ;; *'[8086:'*) GPU_INTEL=1; [ -z "$IGPU_PCI_ID" ] && IGPU_PCI_ID=$(printf '%s\n' "$gpuline" | grep -oE '8086:[0-9a-fA-F]{4}' | head -1) ;; *'[1af4:'*|*'[1b36:'*|*'[1234:'*|*'[15ad:'*|*'[80ee:'*) GPU_VM=1 ;; esac done < <(lspci -nn 2>/dev/null | grep -Ei 'VGA compatible controller|3D controller|Display controller') # Hybrid-laptop awareness. An iGPU (Intel/AMD) present ALONGSIDE an NVIDIA dGPU on # a LAPTOP is almost always muxless Optimus: the internal panel is wired to the # iGPU and the dGPU is only a secondary render device. Forcing the NVIDIA GLX/ # VA-API globally — correct when the display hangs off the NVIDIA card (desktop, # or a laptop MUX set to discrete) — then breaks/slows the iGPU-driven Wayland # session and keeps the dGPU awake on battery. So on a hybrid laptop we keep the # iGPU primary, ship prime-run for on-demand offload, and enable dGPU runtime # power management, while STILL installing the NVIDIA driver + KMS so offload # works. Desktops with NVIDIA present are unaffected (no laptop chassis/battery). # Override the heuristic with NOSIGNAL_GPU_PRIMARY=nvidia|igpu|auto (default auto). GPU_HYBRID_LAPTOP=0 IGPU_VAAPI=radeonsi if [ "$GPU_NVIDIA" = 1 ] && { [ "$GPU_INTEL" = 1 ] || [ "$GPU_AMD" = 1 ]; }; then is_laptop=0 ct=$(cat /sys/class/dmi/id/chassis_type 2>/dev/null || echo 0) case "$ct" in 8|9|10|11|14|30|31|32) is_laptop=1 ;; esac for b in /sys/class/power_supply/BAT*; do [ -e "$b" ] && is_laptop=1; done case "${NOSIGNAL_GPU_PRIMARY:-auto}" in igpu) GPU_HYBRID_LAPTOP=1 ;; nvidia) GPU_HYBRID_LAPTOP=0 ;; *) GPU_HYBRID_LAPTOP=$is_laptop ;; esac # iGPU VA-API backend (the panel-connected decoder): Intel -> iHD, AMD -> radeonsi. [ "$GPU_INTEL" = 1 ] && IGPU_VAAPI=iHD fi GPU_PKGS=(mesa vulkan-icd-loader) GPU_SUMMARY=() if [ "$GPU_NVIDIA" = 1 ]; then # Open-kernel-module NVIDIA (Turing/RTX+; the current default). DKMS so it # tracks the stock 'linux' kernel; linux-headers builds it during pacstrap. GPU_PKGS+=(nvidia-open-dkms nvidia-utils egl-wayland libva-nvidia-driver nvidia-settings linux-headers) GPU_SUMMARY+=("NVIDIA (nvidia-open-dkms + Wayland KMS)") fi if [ "$GPU_AMD" = 1 ]; then GPU_PKGS+=(vulkan-radeon) GPU_SUMMARY+=("AMD (mesa / vulkan-radeon)") fi if [ "$GPU_INTEL" = 1 ]; then GPU_PKGS+=(vulkan-intel intel-media-driver) GPU_SUMMARY+=("Intel (mesa / vulkan-intel)") fi if [ "$GPU_VM" = 1 ]; then GPU_PKGS+=(qemu-guest-agent spice-vdagent) GPU_SUMMARY+=("VM guest (virtio / qemu)") fi if [ "${#GPU_SUMMARY[@]}" -eq 0 ]; then # Nothing recognised — software rendering keeps Hyprland able to start. GPU_PKGS+=(vulkan-swrast) GPU_SUMMARY+=("unknown — software fallback (vulkan-swrast)") fi if [ "$GPU_HYBRID_LAPTOP" = 1 ]; then # prime-run for on-demand dGPU offload; iGPU stays primary (see env block below). GPU_PKGS+=(nvidia-prime) GPU_SUMMARY+=("hybrid laptop -> iGPU primary + NVIDIA PRIME offload (prime-run)") fi echo " Driver target(s): ${GPU_SUMMARY[*]}" nsi_phase "Installing the NoSignal desktop (this is the slow part)" pacstrap -C /tmp/nosignal-pacman.conf -K /mnt "${BASE_PKGS[@]}" "${GPU_PKGS[@]}" echo "==> Generating fstab..." genfstab -U /mnt >> /mnt/etc/fstab # F7: genfstab inherits the ESP's live mount options (fmask=0022,dmask=0022), # which make /boot world-readable — exposing the bootloader random-seed and # limine.conf (kernel cmdline, root PARTUUID, snapshot history) to any local # user. systemd's bootctl flags this at every boot as a security hole. Tighten # the vfat (ESP) line to root rw / group r / others none: files rw-r----- and # dirs rwxr-x---. (Only the ESP is vfat here, so the vfat-line match is exact.) sed -i -E '/[[:space:]]vfat[[:space:]]/{ s/fmask=[0-7]+/fmask=0137/; s/dmask=[0-7]+/dmask=0027/ }' /mnt/etc/fstab if grep -qE '[[:space:]]vfat[[:space:]].*fmask=0137' /mnt/etc/fstab; then echo " ESP hardened (fmask=0137,dmask=0027 — not world-readable)." else echo " WARNING: could not harden ESP mount perms in fstab — review /boot fmask/dmask." fi nsi_phase "Configuring the system" echo "==> Configuring base system in chroot..." # Localisation from the prompts (written from outside the chroot — simpler # than threading variables through a quoted heredoc). ln -sf "/usr/share/zoneinfo/$TIMEZONE" /mnt/etc/localtime sed -i "s|^#${LOCALE} |${LOCALE} |" /mnt/etc/locale.gen # en_US.UTF-8 as an always-present fallback locale. sed -i 's|^#en_US.UTF-8 UTF-8|en_US.UTF-8 UTF-8|' /mnt/etc/locale.gen echo "LANG=$LOCALE" > /mnt/etc/locale.conf echo "KEYMAP=$KEYMAP" > /mnt/etc/vconsole.conf # Resolve .local hostnames via Avahi (printers, other LAN boxes): insert # mdns_minimal before the resolve/dns sources, the standard nss-mdns setup. sed -i '/^hosts:/ s/resolve/mdns_minimal [NOTFOUND=return] resolve/' /mnt/etc/nsswitch.conf arch-chroot /mnt /bin/bash <<'CHROOT' set -e hwclock --systohc locale-gen sed -i 's/^# %wheel ALL=(ALL:ALL) ALL/%wheel ALL=(ALL:ALL) ALL/' /etc/sudoers systemctl enable NetworkManager.service systemctl enable bluetooth.service systemctl enable cups.socket systemctl enable avahi-daemon.service systemctl enable power-profiles-daemon.service # Process sysusers so the 'sddm' system user exists (pacstrap doesn't run # the package's sysusers.d automatically). Harmless if already present. systemd-sysusers || true CHROOT # --------------------------------------------------------------- Plymouth ---- # Boot splash between Limine and the SDDM greeter: insert the `plymouth` # initramfs hook right after the init manager hook and set the NoSignal theme # (the "NO SIGNAL / 入力信号なし" logo centred on black). With the quiet/splash # kernel cmdline, the boot shows that splash instead of scrolling kernel text. echo "==> Configuring Plymouth (NoSignal boot splash)..." # Ship the NoSignal Plymouth theme (vendored). Root dir is 'nosignal' -> # /usr/share/plymouth/themes/nosignal. Must land BEFORE the initramfs rebuild # below so the theme's images/script are embedded in the initramfs. install -d -m 755 /mnt/usr/share/plymouth/themes tar -xzf "$NOSIGNAL_PAYLOAD/vendor/nosignal-plymouth.tar.gz" \ -C /mnt/usr/share/plymouth/themes # The plymouth hook goes right after the init manager hook. Arch's default # mkinitcpio.conf now uses the `systemd` hook (not `udev`), so handle both: # plymouth after `systemd` if present, else after `udev`. Idempotent. if ! grep -qE '^HOOKS=.*\bplymouth\b' /mnt/etc/mkinitcpio.conf; then if grep -qE '^HOOKS=.*\bsystemd\b' /mnt/etc/mkinitcpio.conf; then sed -i '/^HOOKS=/ s/\bsystemd\b/systemd plymouth/' /mnt/etc/mkinitcpio.conf else sed -i '/^HOOKS=/ s/\budev\b/udev plymouth/' /mnt/etc/mkinitcpio.conf fi fi # Set NoSignal as the default theme (no -R: the mkinitcpio -P below rebuilds the # initramfs and embeds it). Falls back to the built-in spinner if it can't apply. arch-chroot /mnt plymouth-set-default-theme nosignal \ || arch-chroot /mnt plymouth-set-default-theme spinner || true # ------------------------------------------------------------- NVIDIA KMS ---- # Borrowed from chwd's pre_install hook: early-load the NVIDIA modules and drop # the 'kms' hook (which would otherwise bind nouveau to the card), enable DRM # modeset, and set the Wayland EGL/VA env. This is what actually lets Hyprland # start on NVIDIA. No-op on AMD/Intel/VM-only systems. if [ "$GPU_NVIDIA" = 1 ]; then echo "==> Configuring NVIDIA DRM/KMS for Wayland (chwd-style)..." install -d -m 755 /mnt/etc/mkinitcpio.conf.d cat > /mnt/etc/mkinitcpio.conf.d/10-nosignal-nvidia.conf <<'NV' # Generated by NoSignal. Early-load the NVIDIA modules for KMS. MODULES+=(nvidia nvidia_modeset nvidia_uvm nvidia_drm) NV cat > /mnt/etc/mkinitcpio.conf.d/20-nosignal-nvidia-kms.conf <<'NVK' # Generated by NoSignal. Drop the 'kms' hook so nouveau isn't pulled in early. HOOKS=(${HOOKS[@]/kms/}) NVK cat > /mnt/etc/modprobe.d/nvidia.conf <<'NVMOD' options nvidia_drm modeset=1 fbdev=1 NVMOD # System-wide EGL/VA env. caelestia symlinks ~/.config/hypr, so per-user hypr # env files would be clobbered — /etc/environment is read for the session. if [ "$GPU_HYBRID_LAPTOP" = 1 ]; then # Muxless Optimus: the iGPU drives the panel. Do NOT force NVIDIA GLX/VA-API # globally — that would push the whole session onto the dGPU. Leave GLX to the # iGPU, point VA-API at the iGPU decoder, and let apps opt into the dGPU with # `prime-run`. Enable dGPU runtime power management (D3) so it sleeps when idle # (Turing+; nvidia-open supports it — matches the nvidia-open-dkms gate). cat >> /mnt/etc/environment <> /mnt/etc/environment < /mnt/etc/modprobe.d/nvidia-pm.conf <<'NVPM' # Fine-grained dGPU runtime power management for Optimus battery life (Turing+). options nvidia NVreg_DynamicPowerManagement=0x02 NVPM install -d -m 755 /mnt/etc/udev/rules.d cat > /mnt/etc/udev/rules.d/80-nosignal-nvidia-pm.rules <<'NVUDEV' # Let the NVIDIA dGPU autosuspend when idle (runtime D3). VGA + 3D controllers. # # RTD3 Cause 1 (2026-06-20 hardware test, Acer AMD iGPU + RTX 3050): the nvidia # driver binds the dGPU INSIDE the initramfs (mkinitcpio `kms` hook pulls it in # even with MODULES=()), before real-root udevd exists. Coldplug then replays the # device as ACTION=="add", which an ACTION=="bind" rule never matched, leaving # power/control at the kernel default "on" (dGPU never reached D3, ~4 W idle). # DRIVER=nvidia is already set at coldplug, so match the bound device with NO # ACTION gate. Keep restoring "auto" on unbind for a later rebind/probe. SUBSYSTEM=="pci", DRIVERS=="nvidia", ATTR{vendor}=="0x10de", ATTR{class}=="0x030000", TEST=="power/control", ATTR{power/control}="auto" SUBSYSTEM=="pci", DRIVERS=="nvidia", ATTR{vendor}=="0x10de", ATTR{class}=="0x030200", TEST=="power/control", ATTR{power/control}="auto" ACTION=="unbind", SUBSYSTEM=="pci", DRIVERS=="nvidia", ATTR{vendor}=="0x10de", ATTR{class}=="0x030000", TEST=="power/control", ATTR{power/control}="auto" ACTION=="unbind", SUBSYSTEM=="pci", DRIVERS=="nvidia", ATTR{vendor}=="0x10de", ATTR{class}=="0x030200", TEST=="power/control", ATTR{power/control}="auto" NVUDEV else # NVIDIA drives the display (desktop, or a laptop MUX set to discrete): force # the NVIDIA GLX/VA-API session-wide. cat >> /mnt/etc/environment <<'NVENV' # --- NoSignal NVIDIA Wayland env --- LIBVA_DRIVER_NAME=nvidia __GLX_VENDOR_LIBRARY_NAME=nvidia NVD_BACKEND=direct NVENV fi fi # ---------------------------------------------------------- Acer power ------- # Acer Predator/Nitro laptops keep the dGPU's higher power states LOCKED until # acer_wmi loads with predator_v4=1 — then the NitroSense/Turbo key cycles them # (e.g. 35->40->50->60 W). NoSignal didn't set this out of the box, so the dGPU # was capped low (hardware test 2026-06-20). Bake it in, gated on Acer hardware # AND a kernel acer_wmi that accepts the option, so non-Acer machines are # untouched. nvidia-powerd (Dynamic Boost) is enabled when an NVIDIA driver is # present (the unit only exists then). Platform profile stays the Omarchy default # (balanced) — the user can raise it. Source: 28allday/Acer-Power-Control-Omarchy. SYS_VENDOR=$(cat /sys/class/dmi/id/sys_vendor 2>/dev/null || echo "") if printf '%s' "$SYS_VENDOR" | grep -qi acer \ && modinfo -p acer_wmi 2>/dev/null | grep -q "^predator_v4:"; then echo "==> Acer laptop detected — unlocking Predator/Nitro GPU power (predator_v4=1)..." printf 'options acer_wmi predator_v4=1\n' > /mnt/etc/modprobe.d/acer-wmi.conf if [ "$GPU_NVIDIA" = 1 ]; then arch-chroot /mnt systemctl enable nvidia-powerd >/dev/null 2>&1 \ && echo " nvidia-powerd enabled (Dynamic Boost)" \ || echo " nvidia-powerd not enabled (optional)" fi fi nsi_phase "Installing the bootloader" echo "==> Installing Limine bootloader (UEFI)..." ROOT_PARTUUID=$(blkid -s PARTUUID -o value "$ROOT_PART") # Root is the btrfs @ subvolume — Limine/kernel needs rootflags to mount it. # The quiet/splash flags give a clean boot: Plymouth shows the NoSignal splash # (NO SIGNAL logo on black) and the kernel/systemd "loading" text is suppressed, # so the boot goes Limine -> NoSignal splash -> SDDM greeter with no scroll. KERNEL_OPTS="root=PARTUUID=$ROOT_PARTUUID rw rootflags=subvol=@" KERNEL_OPTS="$KERNEL_OPTS quiet splash loglevel=3 systemd.show_status=false rd.udev.log_level=3 vt.global_cursor_default=0" # Disable zswap: the swap device is zram (already compressed RAM), so zswap in # front of it would double-compress pages (zswap -> zram) and waste CPU. KERNEL_OPTS="$KERNEL_OPTS zswap.enabled=0" # NVIDIA needs DRM modeset on the cmdline too (belt-and-suspenders with the # modprobe.d option) for Wayland/Hyprland. [ "$GPU_NVIDIA" = 1 ] && KERNEL_OPTS="$KERNEL_OPTS nvidia_drm.modeset=1" # /mnt/boot is the FAT32 ESP. Drop Limine's UEFI binary at BOTH the spec-defined # vendor path (/EFI/limine) and the removable-media fallback (/EFI/BOOT/BOOTX64 # .EFI). The fallback is what makes the disk boot without an NVRAM entry — the # efibootmgr call below often can't write NVRAM (chroot / VMs / locked firmware), # so the fallback path is the reliable one; the NVRAM entry is a nicety. install -d /mnt/boot/EFI/limine /mnt/boot/EFI/BOOT cp /mnt/usr/share/limine/BOOTX64.EFI /mnt/boot/EFI/limine/BOOTX64.EFI cp /mnt/usr/share/limine/BOOTX64.EFI /mnt/boot/EFI/BOOT/BOOTX64.EFI # limine.conf lives on the ESP root; boot():/ resolves to that same ESP, where # the kernel/ucode/initramfs all sit (since /boot IS the ESP). Microcode modules # must precede the main initramfs. # timeout: 10 — keep the Limine menu up 10s before auto-booting (user choice; # the limine snapshot tooling only manages entries, it never rewrites this). # # F4 (dead-boot after a kernel update): with ENABLE_UKI=yes the first kernel # update DELETES /boot/vmlinuz-linux + initramfs-linux.img (they're folded into # the UKI). The OLD seed made the *first/default* manual entry `protocol: linux` # pointing at those now-missing files, with no `default_entry:` — so once the # UKI hook ran, the default auto-boot target dead-booted to a TTY. The previous # post-install conversion mis-fired (ran once, no-op'd, marked applied), so the # durable fix lives HERE in the seed: # * entry 1 = `protocol: efi` -> the UKI (boots once the UKI exists, which is # at install in the normal path, or at worst after the first kernel update). # * entry 2 = a `protocol: linux` fallback for the rare pre-UKI first boot # (snapshot/limine-update failed at install, so no UKI yet). Becomes a no-op # stub once the UKI exists; harmless, never the default. # * `default_entry: 1` pins the UKI entry as default regardless of the auto # entries limine appends below it on every kernel update (limine preserves # these manual entries + general directives verbatim across regenerations, # so this seed is durable — no per-update hook needed). cat > /mnt/boot/limine.conf </dev/null || true # ------------------------------------------------- Btrfs + Limine snapshots --- # Omarchy-style bootable snapshots, configured AT INSTALL TIME (this all lived # in the first-boot script before Phase 3): limine-snapper-sync + the UKI-based # limine-mkinitcpio-hook + snapper (root only). After this, every pacman # transaction auto-snapshots (snap-pac), and each snapshot shows as a bootable # entry in the Limine menu — pick one to roll back a bad update. Resilient: if # any of this fails, the basic Limine boot set up above still works. echo "==> Configuring Btrfs + Limine snapshots..." # /etc/default/limine is read by the limine tools to build UKIs + boot entries. # It must exist BEFORE the tools are installed/run. Carries the same cmdline. cat > /mnt/etc/default/limine < /mnt/etc/mkinitcpio.conf.d/10-nosignal-btrfs.conf fi # The snapshot tools come PREBUILT from the omarchy repo (bundled in the # offline repo) — installed in a second pacstrap now that their config exists. # KEEP IN SYNC with LIMINE_TOOLS in the builder. nsi_phase "Installing snapshot + boot tooling" pacstrap -C /tmp/nosignal-pacman.conf /mnt limine-snapper-sync limine-mkinitcpio-hook # snapper "root" config. /.snapshots is already a mounted @snapshots subvol, # which collides with snapper create-config (it wants to make that subvol) — # so do the documented dance: unmount, let snapper create its own, delete it, # then remount our @snapshots. --no-dbus because there's no snapperd in chroot. SNAPSHOTS_OK=0 if umount /mnt/.snapshots \ && rm -rf /mnt/.snapshots \ && arch-chroot /mnt snapper --no-dbus -c root create-config / \ && arch-chroot /mnt btrfs subvolume delete /.snapshots \ && mkdir /mnt/.snapshots \ && mount -o "$BTRFS_OPTS,subvol=@snapshots" "$ROOT_PART" /mnt/.snapshots \ && chmod 750 /mnt/.snapshots; then # Root-only, keep 5, no timeline (pre/post pacman snapshots via snap-pac). arch-chroot /mnt snapper --no-dbus -c root set-config NUMBER_LIMIT=5 NUMBER_LIMIT_IMPORTANT=5 TIMELINE_CREATE=no || true arch-chroot /mnt btrfs quota disable / 2>/dev/null || true # qgroup accounting is a perf drag arch-chroot /mnt systemctl enable limine-snapper-sync.service || true SNAPSHOTS_OK=1 echo " Snapshots configured." else echo " snapper setup failed — snapshots disabled (basic Limine boot unaffected)." fi # Rebuild the initramfs once, now that Plymouth, any NVIDIA overrides and the # btrfs-overlayfs hook are all in place — then let the limine tooling generate # the UKIs and the final boot entries. # Full path: limine-mkinitcpio-hook ships a /usr/local/bin/mkinitcpio wrapper # that PROMPTS [Y/n] after -P ("run limine-mkinitcpio now?") — an interactive # blocker mid-install. limine-update below regenerates the entries anyway. nsi_phase "Building the boot image" arch-chroot /mnt /usr/bin/mkinitcpio -P if [ "$SNAPSHOTS_OK" = 1 ]; then if arch-chroot /mnt limine-update; then echo " Limine UKI entries generated." else echo " limine-update failed — basic Limine boot still works." fi fi # ---------------------------------------------------------------- firewall --- # Default deny inbound, allow outbound (a desktop needs no open inbound ports). # ufw in a chroot can't apply rules to netfilter and returns non-zero on some # versions — tolerate that; ENABLED=yes in ufw.conf + the enabled service make # the policy apply at boot. echo "==> Configuring firewall (ufw)..." arch-chroot /mnt ufw default deny incoming || true arch-chroot /mnt ufw default allow outgoing || true # omarchy-send / LocalSend protocol (TCP transfers + UDP discovery). arch-chroot /mnt ufw allow 53317 || true # mDNS responses (Avahi: .local resolution, printer/device discovery). arch-chroot /mnt ufw allow 5353/udp || true sed -i 's/^ENABLED=no/ENABLED=yes/' /mnt/etc/ufw/ufw.conf 2>/dev/null || true arch-chroot /mnt systemctl enable ufw.service || true echo "==> Writing hostname / hosts..." echo "$HOSTNAME" > /mnt/etc/hostname cat > /mnt/etc/hosts < Creating user $USERNAME..." # video/audio/input groups for a desktop session; wheel for sudo. arch-chroot /mnt useradd -m -G wheel,video,audio,input -s /bin/bash "$USERNAME" printf 'root:%s\n%s:%s\n' "$USER_PW" "$USERNAME" "$USER_PW" | arch-chroot /mnt chpasswd # ----------------------------------------------------------- ssh handoff ---- # SSH is OPTIONAL on a desktop. If the build host baked a public key we install # it and enable sshd (handy for remote rescue / setup), opening 22 in ufw. # Otherwise sshd stays disabled — this is a desktop, not a server. if [ -f /root/master.pub ]; then echo "==> Installing master SSH key for $USERNAME + enabling sshd..." install -m 700 -d "/mnt/home/$USERNAME/.ssh" install -m 600 /root/master.pub "/mnt/home/$USERNAME/.ssh/authorized_keys" arch-chroot /mnt chown -R "$USERNAME:$USERNAME" "/home/$USERNAME/.ssh" install -m 755 -d /mnt/etc/ssh/sshd_config.d cat > /mnt/etc/ssh/sshd_config.d/10-key-only.conf <<'SSHD' PasswordAuthentication no KbdInteractiveAuthentication no PermitRootLogin no SSHD arch-chroot /mnt systemctl enable sshd.service || true arch-chroot /mnt ufw allow 22/tcp || true else echo " (no /root/master.pub on the ISO — sshd left disabled)" fi # ------------------------------------------------- online repos (post-boot) -- # The INSTALLED system uses the normal online mirrors, not the install media. # The archiso live mirrorlist is reflector-generated only when online — on an # offline install it can be empty/commented, so write a sane default the user # can refine later. NB: nothing is synced yet; the user's first package # operation should be `sudo pacman -Syu`. echo "==> Writing pacman mirrorlist for the installed system..." cat > /mnt/etc/pacman.d/mirrorlist <<'MIRROR' ## NoSignal default mirror (geo-routed). Refine with reflector if you like: ## sudo reflector --latest 10 --sort rate --save /etc/pacman.d/mirrorlist Server = https://geo.mirror.pkgbuild.com/$repo/os/$arch MIRROR # The omarchy repo serves PREBUILT updates for the Limine snapshot tools # (otherwise they rebuild via a ~530MB GraalVM toolchain from the AUR). # NB: SigLevel is Optional TrustAll (unsigned) — same as Omarchy uses; it means # trusting omarchy's binaries. Only the two limine-* packages come from it. echo "==> Adding omarchy repo (prebuilt Limine snapshot tool updates)..." cat >> /mnt/etc/pacman.conf <<'OMARCHY_REPO' [omarchy] SigLevel = Optional TrustAll Server = https://pkgs.omarchy.org/edge/$arch OMARCHY_REPO # Enable [multilib] (layer change 15 — gaming enablement): the lib32-* deps # Steam/gaming installers (DeckShift) need live there. Uncomments ONLY the # [multilib] block (leaves [multilib-testing] alone). Nothing is synced here — # the install is offline; the user's first `pacman -Syu` pulls the db. echo "==> Enabling [multilib] repo in the installed system..." sed -i '/^#\[multilib\]/,/^#Include/ s/^#//' /mnt/etc/pacman.conf # ---------------------------------------------------------- zram swap -------- # A desktop with NO swap OOM-kills heavy workloads; zram gives compressed # in-RAM swap (no disk partition needed). zram-generator is a systemd # generator: the package + this config auto-activate systemd-zram-setup@zram0 # at boot, no explicit enable needed. echo "==> Configuring zram swap..." cat > /mnt/etc/systemd/zram-generator.conf <<'ZRAM' [zram0] zram-size = min(ram, 8192) compression-algorithm = zstd ZRAM chmod 644 /mnt/etc/systemd/zram-generator.conf # ------------------------------------- CachyOS-derived performance tweaks ---- # Cherry-picked from CachyOS-Settings (github.com/CachyOS/CachyOS-Settings), # same borrow-the-method approach as the chwd GPU detection: a curated subset # of their sysctl + udev rules, vendored as plain text (no CachyOS repos or # kernel). swappiness=100 is correct WITH zram (compressed swap is cheap). echo "==> Applying CachyOS-derived performance tweaks..." cat > /mnt/etc/sysctl.d/99-nosignal-performance.conf <<'SYSCTL' # NoSignal — desktop responsiveness tweaks (curated from CachyOS-Settings). kernel.nmi_watchdog = 0 kernel.split_lock_mitigate = 0 vm.max_map_count = 2147483642 vm.swappiness = 100 vm.vfs_cache_pressure = 50 # With zram (RAM-fast, per-page decompression) swap read-ahead just wastes CPU, # so disable it (Arch Wiki / Fedora recommend page-cluster=0 with zram). vm.page-cluster = 0 net.ipv4.tcp_fastopen = 3 SYSCTL cat > /mnt/etc/udev/rules.d/60-ioschedulers.rules <<'IOSCHED' # NoSignal — I/O scheduler per device type (from CachyOS-Settings): # NVMe -> none, SATA/eMMC SSD -> mq-deadline, rotational -> bfq. ACTION=="add|change", KERNEL=="nvme[0-9]*", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="none" ACTION=="add|change", KERNEL=="sd[a-z]*|mmcblk[0-9]*", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="mq-deadline" ACTION=="add|change", KERNEL=="sd[a-z]*", ATTR{queue/rotational}=="1", ATTR{queue/scheduler}="bfq" IOSCHED # ---------------------------------------------- ~/.local/bin on PATH -------- # Put ~/.local/bin onto every login shell's PATH so user-installed binaries are # runnable without manual PATH edits. /etc/profile.d/*.sh is sourced by # /etc/profile for any login shell, before user dotfiles. echo "==> Adding ~/.local/bin to PATH for all login shells..." install -d -m 755 /mnt/etc/profile.d cat > /mnt/etc/profile.d/local-bin.sh <<'PROFILE_LOCAL_BIN' # Add ~/.local/bin to PATH for login shells (idempotent). case ":$PATH:" in *":$HOME/.local/bin:"*) ;; *) [ -d "$HOME/.local/bin" ] && PATH="$HOME/.local/bin:$PATH" ;; esac PROFILE_LOCAL_BIN chmod 644 /mnt/etc/profile.d/local-bin.sh # --------------------------------------------------------------- SDDM -------- # Display manager: SDDM (layer change 16, REQUIRED — DeckShift's # desktop<->gaming session switching rewrites SDDM config + restarts it; # greetd/tuigreet couldn't do this). Password greeter, NO autologin baked — # DeckShift manages its own autologin/session entries post-install (change 17, # opt-in). Canonical source: the layer's display-manager-sddm/ # sddm-10-nosignal.conf; mirrored here to keep the installer self-contained. echo "==> Writing + enabling SDDM..." install -d -m 755 /mnt/etc/sddm.conf.d cat > /mnt/etc/sddm.conf.d/10-nosignal.conf <<'SDDMCONF' # NoSignal SDDM config (installed to /etc/sddm.conf.d/10-nosignal.conf). [General] # X11 greeter — the reliable, well-trodden path (needs xorg-server). The Hyprland # SESSION still runs on Wayland; only SDDM's own greeter uses X here. # For an X-free image instead, use the Wayland greeter: # DisplayServer=wayland # [Wayland] # CompositorCommand=weston --shell=kiosk (adds `weston` to the image) DisplayServer=x11 Numlock=on [Users] # The desktop session to prefer. hyprland-uwsm.desktop launches Hyprland through # uwsm exactly like the old greetd config — keep this (NOT plain hyprland.desktop), # or caelestia's uwsm/systemd-managed session env won't apply. # (SDDM remembers the last-used session per user; DeckShift overrides this when # switching to/from its Gamescope gaming session.) SDDMCONF chmod 644 /mnt/etc/sddm.conf.d/10-nosignal.conf # Seed SDDM's remembered last user/session so the very first greeter already # preselects the uwsm-managed Hyprland session (hyprland-uwsm.desktop — the # same canonical uwsm launch the old tuigreet --cmd used; plain # hyprland.desktop would lose caelestia's uwsm/systemd-managed session env). # SDDM rewrites this file itself after every login. mkdir -p (not install -d): # the sddm package owns /var/lib/sddm with its own ownership/mode — don't touch. mkdir -p /mnt/var/lib/sddm cat > /mnt/var/lib/sddm/state.conf < Installing caelestia desktop for $USERNAME (offline)..." USER_HOME="/home/$USERNAME" M_HOME="/mnt$USER_HOME" install -d -m 755 "$M_HOME/.local/share" "$M_HOME/.config" tar -xzf "$NOSIGNAL_PAYLOAD/vendor/caelestia-dotfiles.tar.gz" -C "$M_HOME/.local/share" chmod u+x "$M_HOME/.local/share/caelestia/hypr/scripts/wsaction.fish" 2>/dev/null || true # The symlink set upstream install.fish creates (targets are the FINAL paths # as seen on the booted system, so they dangle here in the live env — fine). for link in hypr foot fish uwsm btop; do [ -e "$M_HOME/.local/share/caelestia/$link" ] || continue ln -sfn "$USER_HOME/.local/share/caelestia/$link" "$M_HOME/.config/$link" done [ -e "$M_HOME/.local/share/caelestia/starship.toml" ] \ && ln -sfn "$USER_HOME/.local/share/caelestia/starship.toml" "$M_HOME/.config/starship.toml" # --- NoSignal restraint config (Phase 2) --------------------------------------- # Overrides on top of caelestia's defaults. Pure config — Caelestia's own # token/appearance system reads these; no shell code is touched, and deleting # any file restores stock caelestia. Canonical source: the nosignal-shell config # repo (nosignal/config/); mirrored here to keep the installer self-contained. echo "==> Applying NoSignal desktop restraint config..." install -d -m 755 "$M_HOME/.config/caelestia" # Restrained design tokens: SQUARE corners (rounding ramp zeroed — redesign goal; # the 77 Tokens.rounding.* bindings honour these, appearance.rounding.scale does # not feed them; rounding.full omitted so genuine circles stay), tighter # spacing/padding, denser fonts, snappier animations (overrides Caelestia's # config-driven Tokens system). cat > "$M_HOME/.config/caelestia/shell-tokens.json" <<'NOSIGNAL_TOKENS' { "appearance": { "rounding": { "extraSmall": 0, "small": 0, "medium": 0, "large": 0, "largeIncreased": 0, "extraLarge": 0, "extraLargeIncreased": 0, "extraExtraLarge": 0 }, "spacing": { "extraSmall": 3, "small": 6, "medium": 9, "large": 13, "largeIncreased": 16, "extraLarge": 22, "extraLargeIncreased": 26, "extraExtraLarge": 38 }, "padding": { "extraSmall": 3, "small": 6, "medium": 9, "large": 13, "largeIncreased": 16, "extraLarge": 22, "extraLargeIncreased": 26, "extraExtraLarge": 38 }, "fontSize": { "small": 10, "smaller": 11, "normal": 12, "larger": 14, "large": 16, "extraLarge": 24 }, "animDurations": { "small": 120, "normal": 200, "large": 320, "extraLarge": 450, "expressiveFastSpatial": 200, "expressiveDefaultSpatial": 300, "expressiveSlowSpatial": 400, "expressiveFastEffects": 100, "expressiveDefaultEffects": 150, "expressiveSlowEffects": 220 } } } NOSIGNAL_TOKENS # Appearance restraint + config-level cuts: opaque panels, Material Symbols # Outlined icons, drop the Performance/Weather dashboard tabs, visualiser off. # F9: NoSignal launches Hyprland via uwsm, but caelestia's built-in default # session.commands.logout targets a plain (non-uwsm) session, so the session # menu's Logout button was a no-op. `uwsm stop` is the documented way to end a # uwsm session (drops back to SDDM); shutdown/reboot/hibernate use systemctl and # are left at caelestia's defaults. cat > "$M_HOME/.config/caelestia/shell.json" <<'NOSIGNAL_SHELL' { "appearance": { "transparency": { "enabled": false }, "rounding": { "scale": 0 }, "font": { "headline": { "family": "JetBrainsMono Nerd Font" }, "title": { "family": "JetBrainsMono Nerd Font" }, "body": { "family": "JetBrainsMono Nerd Font" }, "label": { "family": "JetBrainsMono Nerd Font" }, "mono": { "family": "JetBrainsMono Nerd Font" }, "icon": { "family": "Material Symbols Outlined" } } }, "general": { "apps": { "terminal": ["kitty"], "explorer": ["nautilus"] } }, "session": { "commands": { "logout": ["uwsm", "stop"] } }, "dashboard": { "showPerformance": false, "showWeather": true }, "background": { "visualiser": { "enabled": false } }, "bar": { "entries": [ { "id": "logo", "enabled": true }, { "id": "workspaces", "enabled": true }, { "id": "spacer", "enabled": true }, { "id": "clock", "enabled": true }, { "id": "spacer", "enabled": true }, { "id": "statusIcons", "enabled": true }, { "id": "tray", "enabled": true }, { "id": "power", "enabled": true } ] } } NOSIGNAL_SHELL # Flatten the Hyprland glassmorphism (blur/shadow off, solid windows, tighter # corners + gaps). Sourced by caelestia's hyprland.conf right after variables.conf. cat > "$M_HOME/.config/caelestia/hypr-vars.conf" <<'NOSIGNAL_HYPRVARS' # NoSignal restraint — flatten Caelestia's glassmorphism (deleting restores stock). # Default applications (caelestia's stock picks aren't installed: zen-browser/ # codium/thunar — point the Super-key app binds at what NoSignal actually ships). $terminal = kitty $browser = chromium $editor = gnome-text-editor $fileExplorer = nautilus $blurEnabled = false $blurPopups = false $blurInputMethods = false $shadowEnabled = false $windowOpacity = 1.0 $windowRounding = 0 $workspaceGaps = 10 $windowGapsIn = 4 $windowGapsOut = 8 $singleWindowGapsOut = 12 NOSIGNAL_HYPRVARS # --- Workspace Overview (standalone sidecar) ---------------------------------- # quickshell-overview runs as its OWN qs instance next to caelestia, toggled by # Super+Tab over IPC. Caelestia's shell is never modified (no fork/package). echo "==> Installing workspace Overview (sidecar)..." install -d -m 755 "$M_HOME/.config/quickshell" tar -xzf "$NOSIGNAL_PAYLOAD/vendor/quickshell-overview.tar.gz" -C "$M_HOME/.config/quickshell" # theme-match caelestia + low-RAM event previews + no blur cat > "$M_HOME/.config/quickshell/overview/config.json" <<'NOSIGNAL_OV_CFG' { "appearance": { "colorSource": "caelestia" }, "overview": { "previewMode": "event", "previewsEnabled": true, "closeOnFocusLoss": true, "hideEmptyRows": true, "effects": { "enableBlur": false, "enableBackdrop": false, "glassMode": false } } } NOSIGNAL_OV_CFG # --- omarchy-send (LAN file transfer, LocalSend-compatible) ------------------- # Vendored release binary; system-wide so every user gets it. The desktop entry # opens the TUI in a floating kitty (TUI.float class — float rule above). echo "==> Installing omarchy-send..." install -m 755 "$NOSIGNAL_PAYLOAD/vendor/omarchy-send" /mnt/usr/local/bin/omarchy-send install -d -m 755 /mnt/usr/share/applications /mnt/usr/share/icons/hicolor/scalable/apps cat > /mnt/usr/share/icons/hicolor/scalable/apps/omarchy-send.svg <<'OMS_ICON' OMS_ICON cat > /mnt/usr/share/applications/omarchy-send.desktop <<'OMS_DESKTOP' [Desktop Entry] Name=Omarchy-Send Comment=Send & receive files over the LAN (LocalSend-compatible) Exec=kitty --class TUI.float -e omarchy-send Icon=omarchy-send Terminal=false Type=Application Categories=Network;FileTransfer; Keywords=localsend;share;transfer;airdrop; OMS_DESKTOP # --- OS theme wallpapers ------------------------------------------------------- # The user's own Moebius-style art set. caelestia's dynamic Material scheme is # generated FROM the default wallpaper (foam-sea.png, change 18) in the scheme # step below. echo "==> Installing NoSignal wallpapers..." install -d -m 755 "$M_HOME/Pictures/Wallpapers" tar -xf "$NOSIGNAL_PAYLOAD/vendor/nosignal-wallpapers.tar" -C "$M_HOME/Pictures/Wallpapers" # Chromium on Wayland: pick the ozone backend automatically (native Wayland # under Hyprland instead of XWayland). cat > /mnt/etc/chromium-flags.conf <<'CHROMIUM_FLAGS' --ozone-platform-hint=auto CHROMIUM_FLAGS # autostart the sidecar + toggle bind via caelestia's last-sourced user hook. # Overview lives on Super+GRAVE (not Tab): the Omarchy keymap appended below # puts next-workspace on Super+Tab — see dev-docs/BUILDER-HANDOFF.md. cat > "$M_HOME/.config/caelestia/hypr-user.conf" <<'NOSIGNAL_HYPRUSER' # NoSignal — overview sidecar autostart + keybind (deleting restores stock). exec-once = qs -c overview -d bind = Super, Grave, exec, qs ipc -c overview call overview toggle # F10: open Caelestia Settings (nexus). Super+Alt+Space is free (NoSignal moved # the old float-toggle to Super+T). caelestia:nexus is the registered global # shortcut that opens the Settings window. bind = Super+Alt, Space, global, caelestia:nexus # hyprmoncfg writes monitor layout/resolution + workspace assignments here # (the files are pre-created empty so Hyprland never warns about the source; # hyprmoncfg refuses to write monitors.conf unless it is sourced). source = ~/.config/hypr/monitors.conf source = ~/.config/hypr/workspaces.conf # omarchy-send launches in a floating terminal window windowrule = float true, match:class TUI\.float windowrule = size 1100 700, match:class TUI\.float # Passwordless-sudo password prompt (Settings -> Services). Dedicated class + # centered/pinned floating window; SudoToggleRow also dispatches `focuswindow` so # the prompt grabs keyboard focus (without it the prompt missed keystrokes and # three blank tries tripped pam_faillock — hardware bug 2026-06-20). # NB: `stayfocused` is NOT valid in Hyprland 0.55's match: windowrule grammar # (errors at parse time), so focus is forced from the shell via hyprctl instead. windowrule = float on, match:class nosignal-sudo windowrule = size 640 220, match:class nosignal-sudo windowrule = center on, match:class nosignal-sudo windowrule = pin on, match:class nosignal-sudo # polkit auth agent — none ran out of the box, so GUI privilege prompts (pkexec, # nm-connection-editor system connections, etc.) silently failed. Non-fatal. exec-once = systemctl --user start hyprpolkitagent.service NOSIGNAL_HYPRUSER # Keyboard layout from the installer prompt (appended unquoted — $XKB_LAYOUT). { echo '' echo '# Keyboard layout chosen at install' echo "input:kb_layout = $XKB_LAYOUT" } >> "$M_HOME/.config/caelestia/hypr-user.conf" # F1-B / F11-2: the hypr-user.conf override above was NOT winning — caelestia's # stock hypr/hyprland/input.conf hardcodes `kb_layout = us` and is sourced such # that it overrides the variable set above, so the Wayland session AND the lock # surface came up US-layout on a GB system (mistyped password symbols were F1's # original trigger). Set the chosen layout in that authoritative file too. The # dotfiles are extracted once at install (not a package), so this is durable. # ~/.config/hypr is a symlink into the clone; edit the real file under it. INPUT_CONF="$M_HOME/.local/share/caelestia/hypr/hyprland/input.conf" # Fall back to locating whichever file in the hypr tree carries kb_layout, in # case upstream moves it (keeps the fix working across caelestia layout changes). if [ ! -f "$INPUT_CONF" ]; then INPUT_CONF=$(grep -rlE '^[[:space:]]*kb_layout[[:space:]]*=' \ "$M_HOME/.local/share/caelestia/hypr/" 2>/dev/null | head -n1) fi if [ -n "$INPUT_CONF" ] && [ -f "$INPUT_CONF" ]; then if grep -qE '^[[:space:]]*kb_layout[[:space:]]*=' "$INPUT_CONF"; then sed -i "s|^\([[:space:]]*kb_layout[[:space:]]*=[[:space:]]*\).*|\1$XKB_LAYOUT|" "$INPUT_CONF" else printf '\n# Keyboard layout chosen at install (NoSignal)\ninput {\n kb_layout = %s\n}\n' "$XKB_LAYOUT" >> "$INPUT_CONF" fi echo " Hyprland kb_layout set to $XKB_LAYOUT in input.conf" else echo " NOTE: caelestia input.conf not found — relying on hypr-user.conf kb_layout only" fi # Empty targets for hyprmoncfg (live in the dotfiles clone — ~/.config/hypr # is a symlink into it; hyprmoncfg follows the symlink when saving). touch "$M_HOME/.local/share/caelestia/hypr/monitors.conf" \ "$M_HOME/.local/share/caelestia/hypr/workspaces.conf" # kitty: bash is the default shell (NoSignal layer change 2 — Omarchy setup); # ~/.bashrc applies caelestia's colour-scheme escape sequences at terminal # start (cat sequences.txt), so kitty gets themed the same way fish did. install -d -m 755 "$M_HOME/.config/kitty" cat > "$M_HOME/.config/kitty/kitty.conf" <<'NOSIGNAL_KITTY' # NoSignal defaults — kitty is the default terminal ($terminal in hypr-vars.conf). shell bash font_family JetBrainsMono Nerd Font font_size 12.0 enable_audio_bell no confirm_os_window_close 0 NOSIGNAL_KITTY # fastfetch: the NoSignal layout (Hardware / Software / Age-Uptime boxes + the # NoSignal logo). Shipped as a real config — NOT symlinked into the caelestia # clone (fastfetch was dropped from the symlink loop above) so it overrides the # dotfiles default. The logo lives beside it; config.jsonc points at it. install -d -m 755 "$M_HOME/.config/fastfetch" install -m 644 "$NOSIGNAL_PAYLOAD/vendor/fastfetch-config.jsonc" \ "$M_HOME/.config/fastfetch/config.jsonc" install -m 644 "$NOSIGNAL_PAYLOAD/vendor/fastfetch-logo.txt" \ "$M_HOME/.config/fastfetch/logo.txt" # ---------------------------------------------- NoSignal layer (changes 1-26) -- # The "os updates" rounds, baked in at install time: keybind cheatsheet, bash # default shell, nosignal-update mechanism, yay+Shelly store, Omarchy default # keybindings, omadots developer polish, hyprmoncfg, settings Updates page, # system polish (1-9); universal copy/paste, screenshots, the launcher/ # dashboard/monitor fix-ups, gaming enablement, SDDM + themed greeter (10-16, # 18-19); Wi-Fi wrong-password recovery, monitor profile hot-load, yay-menu # pre-answers, tidied cheatsheet, CLIAmp music player, Settings Additions # page, uuctl hidden (20-26, the 2026-06-12 hardware-test round). Change 17 # (deckshift-login) SHIPS in the tree but is NOT applied — gaming is opt-in # post-install. The packages came from pacstrap; everything else comes from # the layer tree itself, so the ISO and a live-updated box are identical. # Conf fragments are appended FROM the extracted tree so they cannot drift. nsi_phase "Applying the NoSignal layer" echo "==> Installing NoSignal layer (os-updates round)..." tar -xzf "$NOSIGNAL_PAYLOAD/vendor/nosignal-layer.tar.gz" -C "$M_HOME/.local/share" LAYER="$M_HOME/.local/share/nosignal" chmod +x "$LAYER/nosignal-update/bin/nosignal-update" "$LAYER/nosignal-update/migrations/"*.sh # On-box management skill: a fresh Claude Code on this machine auto-discovers # the `nosignal` skill (its description is surfaced in the available-skills list) # and can help the USER configure / fix / maintain their install — strictly # end-user system management, NOT the dev/QA test protocol. The skill's # references/ are copied from the layer's single-source docs at install time, so # they track the build and can never drift (the staleness trap the frozen # ONBOX-TEST-NOTES.md fell into). The originals stay in ~/.local/share/nosignal/. SKILL_DIR="$M_HOME/.claude/skills/nosignal" install -d -m 755 "$SKILL_DIR/references" install -m 644 "$LAYER/onbox-skill/SKILL.md" "$SKILL_DIR/SKILL.md" install -m 644 "$LAYER/ONBOX-AI-NOTES.md" "$SKILL_DIR/references/ONBOX-AI-NOTES.md" install -m 644 "$LAYER/NoSignal-keybindings.md" "$SKILL_DIR/references/NoSignal-keybindings.md" # Commands onto ~/.local/bin (already on PATH via the profile drop above). # nosignal-update stays a symlink INTO the layer tree — it locates its # migrations by resolving its own path. install -d -m 755 "$M_HOME/.local/bin" install -m 755 "$LAYER/nosignal-keybinds" "$LAYER/nosignal-keybinds-gen" \ "$LAYER/system-polish/nosignal-webapp-install" \ "$LAYER/system-polish/nosignal-webapp-remove" \ "$LAYER/system-polish/nosignal-welcome" \ "$LAYER/updates-panel/nosignal-update-check" \ "$LAYER/super-clipboard/super-copy" \ "$LAYER/super-clipboard/super-paste" \ "$LAYER/screenshots/nosignal-screenshot" \ "$LAYER/additions-installer/nosignal-additions" \ "$LAYER/app-theme-awareness/nosignal-app-theme-sync" \ "$LAYER/kernel-reboot-notify/nosignal-reboot-check" \ "$M_HOME/.local/bin/" # Omarchy CLI shims (change 15) — let Omarchy-targeted gaming installers # (DeckShift) run on NoSignal: omarchy-pkg-add -> yay, steam installer, NVIDIA # GSP probes, restart-walker no-op. install -m 755 "$LAYER/gaming-enablement/omarchy-pkg-add" \ "$LAYER/gaming-enablement/omarchy-install-gaming-steam" \ "$LAYER/gaming-enablement/omarchy-hw-nvidia-gsp" \ "$LAYER/gaming-enablement/omarchy-hw-nvidia-without-gsp" \ "$LAYER/gaming-enablement/omarchy-restart-walker" \ "$M_HOME/.local/bin/" ln -sf "$USER_HOME/.local/share/nosignal/nosignal-update/bin/nosignal-update" \ "$M_HOME/.local/bin/nosignal-update" # --- change 27 (xdg-terminal-exec-handler, finding F2): app2unit hardcodes its # terminal handler to `xdg-terminal-exec`, which NoSignal never shipped, so any # Terminal=true desktop entry (and app2unit -T) failed with a critical # notification. Ship a small shim on PATH that launches the configured terminal # ($TERMINAL, else kitty), plus a system xdg-terminals.list pointing at kitty. # (The shim self-removes if the real freedesktop package ever lands in a system # bin — see the component installer.) install -m 755 "$LAYER/xdg-terminal-exec-handler/xdg-terminal-exec" \ "$M_HOME/.local/bin/xdg-terminal-exec" install -d -m 755 /mnt/etc/xdg printf 'kitty.desktop\n' > /mnt/etc/xdg/xdg-terminals.list chmod 644 /mnt/etc/xdg/xdg-terminals.list # Mark every shipped migration as applied — the layer is baked in, so the # user's first `nosignal-update` must report "0 new migrations" (they are # idempotent, but re-running needs network/sudo and is noisy). Do NOT create # the `welcomed` stamp: the first-login welcome notice must fire. install -d -m 755 "$M_HOME/.local/state/nosignal" for m in "$LAYER/nosignal-update/migrations/"*.sh; do basename "$m" done > "$M_HOME/.local/state/nosignal/applied" # --- change 29 (limine-uki-dead-entry, finding F4, BLOCKER): FIXED IN THE SEED. # The dead-boot is now prevented at the source — the seeded limine.conf above # ships a `protocol: efi` -> UKI entry as entry 1 with `default_entry: 1`, plus a # `protocol: linux` fallback as entry 2. Limine preserves these manual entries + # `default_entry` verbatim across every kernel-update regeneration, so the # default boot target can never become the dead vmlinuz entry. No post-install # conversion is needed (the old one-shot conversion mis-fired on 20260613). The # limine-uki-dead-entry/ component is kept in the layer ONLY as a manual repair # tool for boxes installed from a pre-fix ISO; it is NOT run at install anymore. # --- change 2: default shell bash, matching Omarchy. The fish PACKAGE stays # (caelestia-meta hard-depends on it) and the hypr config keeps calling the # *.fish script paths — replaced with bash ports under the same filenames. echo "==> Default shell: bash (Omarchy setup)..." install -d -m 755 "$M_HOME/.local/share/omarchy/default" cp -a "$LAYER/fish-to-bash/bash" "$M_HOME/.local/share/omarchy/default/bash" install -m 644 "$LAYER/fish-to-bash/bashrc" "$M_HOME/.bashrc" install -m 755 "$LAYER/fish-to-bash/hypr-wsaction.bash" \ "$M_HOME/.local/share/caelestia/hypr/scripts/wsaction.fish" install -m 755 "$LAYER/fish-to-bash/hypr-configs.bash" \ "$M_HOME/.local/share/caelestia/hypr/scripts/configs.fish" # foot config lives in the dotfiles clone (~/.config/foot is a symlink) sed -i 's/^shell=.*/shell=bash/' "$M_HOME/.local/share/caelestia/foot/foot.ini" # Square corners everywhere (redesign goal): caelestia's rules.conf hardcodes # `rounding 10` on Steam + some XWayland windows, ignoring $windowRounding=0. # Zero them so those windows aren't the only rounded ones on the desktop. sed -i -E 's/^(windowrule = )rounding 10,/\1rounding 0,/' \ "$M_HOME/.local/share/caelestia/hypr/hyprland/rules.conf" 2>/dev/null || true # --- change 6: omadots developer polish. starship/btop replace the caelestia # symlinks made above (re-linking restores stock); git behaviors arrive via # include.path so user identity stays out of the image; LazyVim from vendor. echo "==> omadots developer polish..." rm -f "$M_HOME/.config/starship.toml" install -m 644 "$LAYER/omadots-extras/starship.toml" "$M_HOME/.config/starship.toml" rm -f "$M_HOME/.config/btop" install -d -m 755 "$M_HOME/.config/btop" "$M_HOME/.config/tmux" "$M_HOME/.config/git" install -m 644 "$LAYER/omadots-extras/btop.conf" "$M_HOME/.config/btop/btop.conf" install -m 644 "$LAYER/omadots-extras/tmux.conf" "$M_HOME/.config/tmux/tmux.conf" install -m 644 "$LAYER/omadots-extras/omadots.gitconfig" "$M_HOME/.config/git/omadots.gitconfig" cat > "$M_HOME/.gitconfig" < Omarchy keymap + layer binds..." { echo '' cat "$LAYER/omarchy-keys/omarchy-keys-vars.conf" } >> "$M_HOME/.config/caelestia/hypr-vars.conf" { echo '' cat "$LAYER/hyprland-keybinds-help.conf" echo '' cat "$LAYER/software-install/hyprland-software-install.conf" echo '' cat "$LAYER/omarchy-keys/omarchy-keys-user.conf" echo '' cat "$LAYER/monitor-control/hyprland-monitor-control.conf" echo '' sed -n '/# >>> super-clipboard/,/# <<< super-clipboard/p' \ "$LAYER/super-clipboard/install-super-clipboard.sh" echo '' sed -n '/# >>> nosignal screenshots/,/# <<< nosignal screenshots/p' \ "$LAYER/screenshots/install-screenshots.sh" echo '' # change 24: CLIAmp marked block (float rules + Super+M launch-or-focus) — # shipped as a standalone conf fragment, appended VERBATIM markers included. cat "$LAYER/cliamp-music/hyprland-cliamp.conf" echo '' echo '# NoSignal — one-time first-login welcome (stamps ~/.local/state/nosignal/welcomed)' echo 'exec-once = ~/.local/bin/nosignal-welcome' } >> "$M_HOME/.config/caelestia/hypr-user.conf" # --- change 11: screenshots land in ~/Pictures/Screenshots (clipboard too); # swappy (the caelestia region/freeze annotate binds) saves to the same place. install -d -m 755 "$M_HOME/Pictures/Screenshots" "$M_HOME/.config/swappy" install -m 644 "$LAYER/screenshots/swappy-config" "$M_HOME/.config/swappy/config" # --- changes 7+8: user services. No systemd in the chroot — enable = the # symlink systemctl would create, targets read from each unit's [Install]. echo "==> Enabling user services (hyprmoncfgd, update check timer)..." install -d -m 755 "$M_HOME/.config/systemd/user/default.target.wants" \ "$M_HOME/.config/systemd/user/timers.target.wants" install -m 644 "$LAYER/updates-panel/nosignal-update-check.service" \ "$LAYER/updates-panel/nosignal-update-check.timer" \ "$M_HOME/.config/systemd/user/" ln -sf ../nosignal-update-check.timer \ "$M_HOME/.config/systemd/user/timers.target.wants/nosignal-update-check.timer" ln -sf /usr/lib/systemd/user/hyprmoncfgd.service \ "$M_HOME/.config/systemd/user/default.target.wants/hyprmoncfgd.service" # change 12 (fix for change 7): point hyprmoncfgd at hypr-user.conf (the file # that actually sources monitors.conf — it refuses to write otherwise) and # gate its start until Hyprland's IPC answers (avoids a login-race error # notification). Same flags are baked into the Super+Ctrl+H bind above. install -d -m 755 "$M_HOME/.config/systemd/user/hyprmoncfgd.service.d" install -m 644 "$LAYER/monitor-control-fix/hyprmoncfgd-override.conf" \ "$M_HOME/.config/systemd/user/hyprmoncfgd.service.d/override.conf" # change 21 (monitor-hotload): path unit watches ~/.config/hyprmoncfg/profiles # and try-restarts hyprmoncfgd, so a plain "Save Profile" in the Super+Ctrl+H # TUI applies live. The watched dir must exist (empty — the no-pre-seeded- # profiles rule from change 7 still stands). install -m 644 "$LAYER/monitor-hotload/hyprmoncfgd-rescan.path" \ "$LAYER/monitor-hotload/hyprmoncfgd-rescan.service" \ "$M_HOME/.config/systemd/user/" ln -sf ../hyprmoncfgd-rescan.path \ "$M_HOME/.config/systemd/user/default.target.wants/hyprmoncfgd-rescan.path" install -d -m 755 "$M_HOME/.config/hyprmoncfg/profiles" # --- change 30 (app-theme-awareness, finding F6): make external apps # (Chrome/Electron/Firefox/GTK/Qt) follow Caelestia's light/dark mode. The sync # script (installed to ~/.local/bin above) mirrors `caelestia scheme get` to the # freedesktop appearance portal color-scheme + the GTK prefer-dark flag; a .path # unit re-runs it whenever Caelestia's scheme.json changes, and .service runs it # at login. portals.conf pins the Settings portal to gtk so the value is served. echo "==> Enabling app-theme awareness (follow Caelestia dark/light)..." install -m 644 "$LAYER/app-theme-awareness/nosignal-app-theme.service" \ "$LAYER/app-theme-awareness/nosignal-app-theme.path" \ "$M_HOME/.config/systemd/user/" ln -sf ../nosignal-app-theme.path \ "$M_HOME/.config/systemd/user/default.target.wants/nosignal-app-theme.path" ln -sf ../nosignal-app-theme.service \ "$M_HOME/.config/systemd/user/default.target.wants/nosignal-app-theme.service" install -d -m 755 "$M_HOME/.config/xdg-desktop-portal" install -m 644 "$LAYER/app-theme-awareness/portals.conf" \ "$M_HOME/.config/xdg-desktop-portal/portals.conf" # --- changes 8/25/20: Updates page, Additions page, and Wi-Fi wrong-password # recovery are now BAKED INTO the pinned nosignal-shell fork (Phase 2 — we own # the shell, so the old patch-script + pacman-hook machinery that re-applied # these after every caelestia-shell upgrade is gone). The Updates/Additions # CLIs + units are still installed above; only the shell-side QML edits moved # into the fork source. The Additions status cache is still seeded below. install -d -m 755 /mnt/etc/pacman.d/hooks # --- change 31 (kernel-reboot-notify, finding F5): a pacman PostTransaction # hook that prints a "reboot required" reminder (and "boot the UKI entry") in # the pacman/yay output whenever a kernel image changes, so out-of-band # `pacman -Syu`/`yay` updates warn too. The desktop notification + state stamp # are handled by nosignal-reboot-check, which nosignal-update calls at its end. echo "==> Installing kernel-reboot reminder pacman hook..." install -m 644 "$LAYER/kernel-reboot-notify/95-nosignal-kernel-reboot.hook" \ /mnt/etc/pacman.d/hooks/95-nosignal-kernel-reboot.hook # --- change 28 (caelestia-lock-faillock, finding F1, BLOCKER): the faillock-free # lock-screen PAM service (assets/pam.d/caelestia) and the Pam.qml repoint # (config: "passwd" -> "caelestia") are now BAKED INTO the pinned nosignal-shell # fork (Phase 2). No patch script or pacman hook needed — the fork ships the # correct lock auth, so a desktop screen-lock can never faillock the user out # of their own session. # --- change 36 (sudo-timed-nopasswd): a Settings -> Services toggle (+ CLI) # that grants the user NOPASSWD: ALL for 15 minutes then auto-reverts (a root # systemd timer), with a boot-clean safety net and a sudoers `visudo -cf` # validation before install. Installs the CLI + boot-clean service + the panel # toggle; does NOT enable sudoless (that's a deliberate user action). The # SudoToggleRow + its ServicesPage insert are now BAKED INTO the pinned # nosignal-shell fork (Phase 2), so NOSIGNAL_SKIP_SHELL_PATCH tells the installer # to skip the (now redundant) QML patch and no pacman hook is written. The CLI + # boot-clean safety-net service still install here. echo "==> Installing time-boxed passwordless-sudo toggle..." arch-chroot /mnt env NOSIGNAL_SKIP_SHELL_PATCH=1 sh "$USER_HOME/.local/share/nosignal/sudo-timed-nopasswd/install-sudo-timed-nopasswd.sh" \ || echo " (sudo-timed-nopasswd install failed — CLI/toggle may be absent)" # --- change 37 (cachyos-repo-switch): a Settings -> Services toggle (+ CLI) that # switches the system to/from CachyOS optimized builds + the linux-cachyos kernel, # passwordless and auto-detecting the best x86-64 tier (v4 > v3). Installs the CLI # + the visudo-validated sudoers drop-in + the panel toggle; does NOT enable the # CachyOS repos (a deliberate user action — heavy, networked, reboots the kernel). # CachyRepoToggleRow + its ServicesPage insert are BAKED INTO the pinned # nosignal-shell fork (Phase 2), so NOSIGNAL_SKIP_SHELL_PATCH skips the redundant # QML patch. The CLI + sudoers install here. (gawk/curl/kitty/limine already in # the base closure; the helper live-fetches the CachyOS keyring/mirrorlist/awk at # enable time — enabling CachyOS is online by nature.) echo "==> Installing CachyOS repo + kernel switch toggle..." arch-chroot /mnt env NOSIGNAL_SKIP_SHELL_PATCH=1 sh "$USER_HOME/.local/share/nosignal/cachyos-repo-switch/install-cachyos-repo-switch.sh" \ || echo " (cachyos-repo-switch install failed — CLI/toggle may be absent)" # --- change 4: Flathub remote for Shelly's flatpak pages. The vendored # .flatpakrepo embeds the GPG key, so this is an offline-safe config write. # Staged in /root, NOT /tmp: arch-chroot mounts a fresh tmpfs over the # target's /tmp, which shadows anything copied there from outside. echo "==> Configuring Flathub remote..." cp "$NOSIGNAL_PAYLOAD/vendor/flathub.flatpakrepo" /mnt/root/flathub.flatpakrepo arch-chroot /mnt flatpak remote-add --if-not-exists flathub /root/flathub.flatpakrepo \ || echo " (flathub remote-add failed — once online run: flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo)" rm -f /mnt/root/flathub.flatpakrepo # --- changes 9+26: hide launcher clutter via per-user overrides. The PACKAGES # stay (avahi-daemon is needed for .local/printer discovery, foot is a # caelestia-meta dep, uuctl ships with uwsm) — only their menu entries # disappear. echo "==> Hiding launcher clutter..." install -d -m 755 "$M_HOME/.local/share/applications" for id in avahi-discover bssh bvnc qv4l2 qvidcap foot footclient foot-server \ org.gnupg.pinentry-qt org.gnupg.pinentry-qt5 uuctl; do [ -f "/mnt/usr/share/applications/$id.desktop" ] || continue cat > "$M_HOME/.local/share/applications/$id.desktop" <<'HIDDEN_ENTRY' [Desktop Entry] Type=Application Name=Hidden by NoSignal NoDisplay=true Hidden=true HIDDEN_ENTRY done # (change 9's printing piece — cups.socket — is already enabled in the base.) # --- hardware-test 2026-06-15 (finding F-T2): stop the redundant blueman tray # applet from autostarting. The base ships /etc/xdg/autostart/blueman.desktop, # but blueman-applet duplicates the redesign's own bar Bluetooth pill + BlueZ # panel (and was the only thing keeping the now-gated system-tray pill visible). # A per-user XDG autostart override (Hidden=true) suppresses just the applet; # the blueman package stays so blueman-manager can still be launched by hand and # Bluetooth control is unaffected. echo "==> Disabling redundant blueman tray applet..." install -d -m 755 "$M_HOME/.config/autostart" cat > "$M_HOME/.config/autostart/blueman.desktop" <<'BLUEMAN_OFF' [Desktop Entry] Type=Application Name=blueman-applet Comment=NoSignal: redundant with the bar Bluetooth pill + BlueZ panel — disabled Hidden=true X-GNOME-Autostart-enabled=false BLUEMAN_OFF chmod 644 "$M_HOME/.config/autostart/blueman.desktop" # --- change 24: CLIAmp default music player. The package came from pacstrap # ([omarchy] repo); the Super+M bind is the marked block appended above. Here: # the desktop entry (launches in kitty, class cliamp) + the audio MIME # defaults. Nothing else writes ~/.config/mimeapps.list at install time (the # dotfiles ship none), so a plain write is safe. echo "==> CLIAmp default music player..." install -m 644 "$LAYER/cliamp-music/cliamp.desktop" \ "$M_HOME/.local/share/applications/cliamp.desktop" { echo '[Default Applications]' for m in audio/mpeg audio/mp4 audio/x-m4a audio/aac audio/flac audio/x-flac \ audio/ogg audio/x-vorbis+ogg audio/x-opus+ogg audio/wav audio/x-wav \ audio/webm; do echo "$m=cliamp.desktop" done } > "$M_HOME/.config/mimeapps.list" chmod 644 "$M_HOME/.config/mimeapps.list" # --- change 19: SDDM greeter themed to match the desktop. Theme dir + sync # script + [Theme] drop-in baked from the layer tree. The shipped theme.conf # already carries the foam-sea Material palette (matching the change-18 # default wallpaper), so the first-boot greeter matches the desktop before # any sync has run; the background is seeded from the same wallpaper here. # sddm-theme-sync needs a logged-in user to mirror, so it is NOT run in the # chroot — after a wallpaper change the user runs `sudo sddm-theme-sync`. echo "==> Installing SDDM greeter theme (caelestia)..." install -d -m 755 /mnt/usr/share/sddm/themes/caelestia/backgrounds install -m 644 "$LAYER/sddm-theme/caelestia/Main.qml" \ /mnt/usr/share/sddm/themes/caelestia/Main.qml install -m 644 "$LAYER/sddm-theme/caelestia/metadata.desktop" \ /mnt/usr/share/sddm/themes/caelestia/metadata.desktop install -m 644 "$LAYER/sddm-theme/caelestia/theme.conf" \ /mnt/usr/share/sddm/themes/caelestia/theme.conf install -m 644 "$M_HOME/Pictures/Wallpapers/nosignal/foam-sea.png" \ /mnt/usr/share/sddm/themes/caelestia/backgrounds/wallpaper.png install -m 755 "$LAYER/sddm-theme/sddm-theme-sync" /mnt/usr/local/bin/sddm-theme-sync cat > /mnt/etc/sddm.conf.d/20-sddm-theme.conf <<'SDDMTHEME' # Greeter theme matching the desktop scheme (sddm-theme component). # Remove this file to fall back to SDDM's default greeter. [Theme] Current=caelestia SDDMTHEME chmod 644 /mnt/etc/sddm.conf.d/20-sddm-theme.conf arch-chroot /mnt chown -R "$USERNAME:$USERNAME" "$USER_HOME" # --- colour scheme + wallpaper ------------------------------------------------- # The OS theme: set the default Moebius wallpaper (foam-sea.png, change 18 — # matches the baked SDDM greeter theme), then switch the scheme to # `dynamic` so caelestia generates the Material palette FROM the wallpaper # (whole shell + terminals pick it up). Both CLI calls are headless-safe (PIL + # file writes, no compositor needed; verified the same way scheme-set was). # Order matters: `wallpaper -f` first (writes the state + thumbnail), THEN # `scheme set -n dynamic` (regenerates colours from that thumbnail). # Fallbacks keep the old behaviour: shadotheme scheme, no wallpaper state. echo "==> Seeding caelestia theme (foam-sea wallpaper + dynamic scheme)..." as_user() { arch-chroot /mnt runuser -u "$USERNAME" -- \ env HOME="$USER_HOME" XDG_STATE_HOME="$USER_HOME/.local/state" \ XDG_CONFIG_HOME="$USER_HOME/.config" XDG_CACHE_HOME="$USER_HOME/.cache" \ "$@" } WALL="$USER_HOME/Pictures/Wallpapers/nosignal/foam-sea.png" if as_user caelestia wallpaper -f "$WALL" >/dev/null 2>&1 \ && as_user caelestia scheme set -n dynamic >/dev/null 2>&1 \ && [ -f "$M_HOME/.local/state/caelestia/scheme.json" ]; then echo " Dynamic scheme generated from $WALL" else echo " Dynamic scheme failed in chroot — falling back to shadotheme." as_user caelestia scheme set -n shadotheme >/dev/null 2>&1 || true fi if [ ! -f "$M_HOME/.local/state/caelestia/scheme.json" ]; then install -d -m 755 "$M_HOME/.local/state/caelestia" install -m 644 "$NOSIGNAL_PAYLOAD/vendor/scheme-shadotheme.json" \ "$M_HOME/.local/state/caelestia/scheme.json" fi # --- change 25 (cont.): seed the Additions status cache so the page has data # on first open (all 15 items not-installed on a fresh image — the page IS the # opt-in). Needs as_user, hence here rather than next to the QML patch. as_user "$USER_HOME/.local/bin/nosignal-additions" status >/dev/null 2>&1 \ || echo " (Additions status seed failed — the page populates on first re-check)" arch-chroot /mnt chown -R "$USERNAME:$USERNAME" "$USER_HOME/.local/state" 2>/dev/null || true # --------------------------------------------------------- install cleanup --- # The file:// repo must not leak into the installed system: drop its synced db # (the [nosignal] repo is not in the target's pacman.conf) and clear the package # cache copies pacstrap made (~3.5GB — the install media still has them all). nsi_phase "Finishing up" echo "==> Cleaning install-time package cache..." rm -f /mnt/var/lib/pacman/sync/nosignal.* rm -f /mnt/var/cache/pacman/pkg/* # Heavy phase done — stop the spinner, restore on-screen output, show a centred # completion screen. nsi_spin_stop trap - ERR exec >/dev/tty 2>&1 clear tcecho "NoSignal install complete" "$NSI_GB" "$NSI_R" printf '\n' >/dev/tty tcecho "Remove the install media and reboot." "$NSI_B" "$NSI_R" printf '\n' >/dev/tty tcecho "First boot goes straight to the graphical login (SDDM, themed) —" "$NSI_DIM" "$NSI_R" tcecho "log in and the themed Hyprland desktop starts. No internet needed;" "$NSI_DIM" "$NSI_R" tcecho "run 'sudo pacman -Syu' once when you're online to sync databases." "$NSI_DIM" "$NSI_R" printf '\n' >/dev/tty PROMPT="Press ENTER to reboot now (Ctrl+C for a shell)... " printf '%s' "$(nsi_pad "${#PROMPT}")" >/dev/tty read -rp "$PROMPT" Building offline payload (cached in $OFFLINE)..." mkdir -p "$OFFLINE/iso/repo" "$OFFLINE/iso/vendor" "$OFFLINE/aur" # ---- vendored source tarballs (pinned) ---------------------------------- if [ ! -f "$OFFLINE/iso/vendor/caelestia-dotfiles.tar.gz" ]; then echo "==> Vendoring caelestia dotfiles ($CAELESTIA_DOTS_COMMIT)..." rm -rf "$OFFLINE/aur/caelestia" git clone "$CAELESTIA_DOTS_REPO" "$OFFLINE/aur/caelestia" git -C "$OFFLINE/aur/caelestia" checkout -q "$CAELESTIA_DOTS_COMMIT" # Tarball root dir is 'caelestia' -> extracts to ~/.local/share/caelestia. # .git is kept so the user can pull/diff upstream later. tar -czf "$OFFLINE/iso/vendor/caelestia-dotfiles.tar.gz" -C "$OFFLINE/aur" caelestia fi if [ ! -f "$OFFLINE/iso/vendor/quickshell-overview.tar.gz" ]; then echo "==> Vendoring quickshell-overview ($OVERVIEW_COMMIT)..." rm -rf "$OFFLINE/aur/overview" git clone "$OVERVIEW_REPO" "$OFFLINE/aur/overview" git -C "$OFFLINE/aur/overview" checkout -q "$OVERVIEW_COMMIT" # Tarball root dir is 'overview' -> extracts to ~/.config/quickshell/overview. tar -czf "$OFFLINE/iso/vendor/quickshell-overview.tar.gz" -C "$OFFLINE/aur" overview fi # OS theme wallpapers — repo-local user art, re-tarred EVERY build (like the # layer tree) so image swaps always reach the next ISO. PNGs don't compress, # so plain tar. Stale Last Horizon tarballs from older builds are removed. if [ ! -d "$WALLPAPERS_DIR" ] || [ ! -f "$WALLPAPERS_DIR/$DEFAULT_WALLPAPER" ]; then echo "ERROR: wallpapers not found at $WALLPAPERS_DIR (need $DEFAULT_WALLPAPER)" >&2 exit 1 fi echo "==> Vendoring OS theme wallpapers..." rm -f "$OFFLINE/iso/vendor/lasthorizon-wallpapers.tar.gz" # Tarball root dir is 'nosignal' -> extracts to ~/Pictures/Wallpapers/nosignal. tar -cf "$OFFLINE/iso/vendor/nosignal-wallpapers.tar" \ -C "$(dirname "$WALLPAPERS_DIR")" \ --transform 's|^wallpapers|nosignal|' wallpapers # Plymouth boot-splash theme (NO SIGNAL logo on black) — repo-local, re-tarred # EVERY build (like the wallpapers/layer) so logo/script edits reach the ISO. # Tarball root dir is 'nosignal' -> extracts to # /usr/share/plymouth/themes/nosignal. if [ ! -f "$SCRIPT_DIR/assets/plymouth/nosignal/nosignal.script" ]; then echo "ERROR: Plymouth theme not found at $SCRIPT_DIR/assets/plymouth/nosignal" >&2 exit 1 fi echo "==> Vendoring Plymouth boot-splash theme..." tar -czf "$OFFLINE/iso/vendor/nosignal-plymouth.tar.gz" \ -C "$SCRIPT_DIR/assets/plymouth" nosignal if [ ! -f "$OFFLINE/iso/vendor/lazyvim-starter.tar.gz" ]; then echo "==> Vendoring LazyVim starter ($LAZYVIM_COMMIT)..." rm -rf "$OFFLINE/aur/lazyvim-starter" git clone "$LAZYVIM_REPO" "$OFFLINE/aur/lazyvim-starter" git -C "$OFFLINE/aur/lazyvim-starter" checkout -q "$LAZYVIM_COMMIT" rm -rf "$OFFLINE/aur/lazyvim-starter/.git" # Tarball root dir is 'nvim' -> extracts to ~/.config/nvim. tar -czf "$OFFLINE/iso/vendor/lazyvim-starter.tar.gz" -C "$OFFLINE/aur" \ --transform 's|^lazyvim-starter|nvim|' lazyvim-starter fi if [ ! -f "$OFFLINE/iso/vendor/flathub.flatpakrepo" ]; then echo "==> Vendoring Flathub remote definition..." curl -fSL --proto '=https' --tlsv1.2 \ -o "$OFFLINE/iso/vendor/flathub.flatpakrepo" "$FLATHUB_REPO_URL" fi # NoSignal layer tree — local source, small, and actively iterated on: re-tar # it EVERY build (no cache stamp) so layer edits always reach the next ISO. if [ ! -d "$NOSIGNAL_LAYER_DIR" ]; then echo "ERROR: NoSignal layer tree not found at $NOSIGNAL_LAYER_DIR" >&2 exit 1 fi echo "==> Vendoring NoSignal layer tree..." # Tarball root dir is 'nosignal' -> extracts to ~/.local/share/nosignal. tar -czf "$OFFLINE/iso/vendor/nosignal-layer.tar.gz" \ -C "$(dirname "$NOSIGNAL_LAYER_DIR")" \ --transform 's|^os updates|nosignal|' "os updates" if [ ! -f "$OFFLINE/iso/vendor/omarchy-send" ]; then echo "==> Vendoring omarchy-send ($OMARCHY_SEND_VERSION)..." curl -fSL --proto '=https' --tlsv1.2 -o "$OFFLINE/iso/vendor/omarchy-send" \ "https://github.com/$OMARCHY_SEND_REPO/releases/download/$OMARCHY_SEND_VERSION/omarchy-send-linux-amd64" chmod 755 "$OFFLINE/iso/vendor/omarchy-send" fi install -m 644 "$SCRIPT_DIR/assets/scheme-shadotheme.json" "$OFFLINE/iso/vendor/" install -m 644 "$SCRIPT_DIR/assets/fastfetch-config.jsonc" \ "$SCRIPT_DIR/assets/fastfetch-logo.txt" "$OFFLINE/iso/vendor/" # ---- bootstrap repo db (so the build chroot can reference it from day 0) - if [ ! -e "$OFFLINE/iso/repo/nosignal.db" ]; then tar -czf "$OFFLINE/iso/repo/nosignal.db.tar.gz" --files-from /dev/null tar -czf "$OFFLINE/iso/repo/nosignal.files.tar.gz" --files-from /dev/null ln -sf nosignal.db.tar.gz "$OFFLINE/iso/repo/nosignal.db" ln -sf nosignal.files.tar.gz "$OFFLINE/iso/repo/nosignal.files" fi # ---- build chroot -------------------------------------------------------- # Clean-room AUR builds via devtools. The chroot's pacman.conf includes the # local bootstrap repo: arch-nspawn auto-bind-mounts file:// Server dirs, so # each built package becomes resolvable by the next build (caelestia-shell # needs caelestia-cli + quickshell-git etc.). cat > "$OFFLINE/chroot-pacman.conf" < Creating clean build chroot (devtools mkarchroot)..." mkdir -p "$OFFLINE/chroot" sudo mkarchroot -C "$OFFLINE/chroot-pacman.conf" "$OFFLINE/chroot/root" base-devel fi # ---- AUR packages (clean-chroot builds, dependency order) ---------------- local name p for name in "${AUR_BUILD_ORDER[@]}"; do if [ -f "$OFFLINE/aur/.built-$name" ]; then echo " $name: cached." continue fi echo "==> Building AUR package: $name..." rm -rf "${OFFLINE:?}/aur/$name" if [ "$name" = "nosignal-shell" ]; then # NOT an AUR package — it's our fork of the caelestia shell. Pull the # PKGBUILD from the fork at the pinned commit and pin the git source to # the same commit (reproducible, exactly like quickshell-git below). The # PKGBUILD already carries DISTRIBUTOR + the `git` makedepend that the # shell's M3Shapes CMake FetchContent needs in a clean chroot, so no # further patching is required here. mkdir -p "$OFFLINE/aur/$name" git clone --depth 1 "$NOSIGNAL_SHELL_REPO" -b nosignal "$OFFLINE/aur/$name.fork" ( cd "$OFFLINE/aur/$name.fork" && git fetch --depth 1 origin "$NOSIGNAL_SHELL_COMMIT" && git checkout -q "$NOSIGNAL_SHELL_COMMIT" ) cp "$OFFLINE/aur/$name.fork/packaging/PKGBUILD" "$OFFLINE/aur/$name/PKGBUILD" rm -rf "$OFFLINE/aur/$name.fork" sed -i 's|#branch=nosignal|#commit='"$NOSIGNAL_SHELL_COMMIT"'|' "$OFFLINE/aur/$name/PKGBUILD" else git clone --depth 1 "https://aur.archlinux.org/$name.git" "$OFFLINE/aur/$name" fi if [ "$name" = "quickshell-git" ]; then # Pin to the validated commit (makepkg git source fragment). sed -i 's|git+\$url\.git|&#commit='"$QUICKSHELL_GIT_COMMIT"'|' "$OFFLINE/aur/$name/PKGBUILD" fi # Self-heal the clean chroot's [nosignal] repo path: a project rename # (hyprOS -> nosignal) leaves a stale Server path baked into the chroot's # persistent pacman.conf, which only bites the first chroot build after the # move (cached AUR pkgs skip the build). Keep it pointed at the live OFFLINE. for pc in "$OFFLINE"/chroot/*/etc/pacman.conf; do [ -f "$pc" ] && sudo sed -i "s#file:///.*/offline/iso/repo#file://$OFFLINE/iso/repo#g" "$pc" done # Refresh the ROOT chroot's sync dbs first: makechrootpkg -c clones the # copy from root, and the dependency install inside runs withOUT -Sy — so # without this, packages repo-added to [nosignal] after mkarchroot are # invisible and chained builds fail with "target not found". sudo arch-nspawn "$OFFLINE/chroot/root" pacman -Syu --noconfirm ( cd "$OFFLINE/aur/$name" && sudo makechrootpkg -c -r "$OFFLINE/chroot" ) for p in "$OFFLINE/aur/$name"/*.pkg.tar.zst; do [[ "$(basename "$p")" == *-debug-* ]] && continue cp -f "$p" "$OFFLINE/iso/repo/" repo-add -R "$OFFLINE/iso/repo/nosignal.db.tar.gz" "$OFFLINE/iso/repo/$(basename "$p")" done touch "$OFFLINE/aur/.built-$name" done # caelestia-meta builds from the PKGBUILD inside the dotfiles repo itself # (the AUR snapshot lags it — the dotfiles clone is the authoritative # source). Pure metapackage: no build() step, so plain makepkg -d on the # host is safe (no dep pollution; pkgver() just needs git). # PRIVACY: host makepkg records builddir/startdir in the package's # .BUILDINFO — built from $HOME that would ship the builder's username on a # distributable ISO. Build from a neutral /tmp copy instead. (Chroot-built # packages don't have this problem: their paths are /build.) if [ ! -f "$OFFLINE/aur/.built-caelestia-meta" ]; then echo "==> Building caelestia-meta (from the pinned dotfiles clone)..." if [ ! -d "$OFFLINE/aur/caelestia/.git" ]; then rm -rf "$OFFLINE/aur/caelestia" git clone "$CAELESTIA_DOTS_REPO" "$OFFLINE/aur/caelestia" git -C "$OFFLINE/aur/caelestia" checkout -q "$CAELESTIA_DOTS_COMMIT" fi meta_tmp=$(mktemp -d /tmp/nosignal-meta.XXXXXX) cp -a "$OFFLINE/aur/caelestia" "$meta_tmp/caelestia" # keep .git (pkgver) ( cd "$meta_tmp/caelestia" && rm -f ./*.pkg.tar.zst \ && BUILDDIR="$meta_tmp/build" makepkg -df --noconfirm ) for p in "$meta_tmp/caelestia"/*.pkg.tar.zst; do cp -f "$p" "$OFFLINE/iso/repo/" repo-add -R "$OFFLINE/iso/repo/nosignal.db.tar.gz" "$OFFLINE/iso/repo/$(basename "$p")" done rm -rf "$meta_tmp" touch "$OFFLINE/aur/.built-caelestia-meta" fi # ---- full repo dependency closure ---------------------------------------- # Resolve EVERYTHING the installer can possibly pacstrap — base set, every # GPU variant, the limine tools (prebuilt, from the omarchy repo) and the # caelestia stack (from the local repo, pulling its repo deps) — against an # EMPTY local db, and download into the repo dir. Already-present files are # skipped, so this is cheap on rebuilds. echo "==> Downloading full package dependency closure..." cat > "$OFFLINE/dl-pacman.conf" </dev/null 2>&1; then paccache -rqk1 -c "$OFFLINE/iso/repo" || true else echo " (paccache not found — old package versions may bloat the ISO;" echo " install pacman-contrib to enable pruning)" fi # F5 (2026-06-12 hardware test): hyprland 0.55.3 segfaults at screen lock # (renderer SEGV during lock/DPMS monitor churn; the watchdog then drops to # the confusing safe-mode "basic desktop"). 0.55.4 backports the #15048 fix # and was retested clean on the test box — refuse to ship anything older. local hyprver hyprver=$(basename "$(ls "$OFFLINE/iso/repo"/hyprland-[0-9]*.pkg.tar.zst | sort -V | tail -1)") hyprver=${hyprver#hyprland-}; hyprver=${hyprver%-x86_64.pkg.tar.zst} if [ "$(vercmp "$hyprver" 0.55.4)" -lt 0 ]; then echo "ERROR: offline repo has hyprland $hyprver — F5 requires >= 0.55.4" >&2 echo " (lock-screen renderer SEGV). Refresh mirrors and rebuild." >&2 exit 1 fi echo " hyprland $hyprver (>= 0.55.4, F5 lock-screen fix) OK" echo "==> Building repo database..." # Drop detached signatures (omarchy ships them): repo-add would embed them # in the db and pacman then REQUIRES the signing key even under TrustAll. # Offline install verifies nothing (SigLevel Never) — the sigs are dead weight. rm -f "$OFFLINE/iso/repo"/*.sig rm -f "$OFFLINE/iso/repo"/nosignal.db* "$OFFLINE/iso/repo"/nosignal.files* "$OFFLINE/iso/repo"/*.old repo-add -q "$OFFLINE/iso/repo/nosignal.db.tar.gz" "$OFFLINE/iso/repo"/*.pkg.tar.zst # ISO9660-safe: ship the .db/.files as REAL files, not symlinks. local f for f in db files; do rm -f "$OFFLINE/iso/repo/nosignal.$f" cp "$OFFLINE/iso/repo/nosignal.$f.tar.gz" "$OFFLINE/iso/repo/nosignal.$f" done # ---- base package list (single source of truth for the installer) -------- printf '%s\n' "${BASE_PKGS[@]}" > "$OFFLINE/iso/base-packages.list" echo "==> Offline payload ready: $(du -sh "$OFFLINE/iso" | cut -f1)" } # =========================================================================== # Host-side workflow: locate stock ISO, build offline payload, unsquash, # inject, re-squash, repack, output. # =========================================================================== # ---- locate stock ISO ---------------------------------------------------- shopt -s nullglob CANDIDATES=("$SCRIPT_DIR"/archlinux-*.iso) shopt -u nullglob STOCK_ISO="" for iso in "${CANDIDATES[@]}"; do [[ "$(basename "$iso")" == *NoSignal* ]] && continue STOCK_ISO="$iso" break done if [ -z "$STOCK_ISO" ]; then echo "ERROR: No stock Arch ISO found in $SCRIPT_DIR" >&2 echo >&2 echo "Download the latest from https://archlinux.org/download/ and put it" >&2 echo "in this folder (filename must start with 'archlinux-')." >&2 exit 1 fi echo " Stock ISO: $(basename "$STOCK_ISO")" echo " Output: $(basename "$OUT_ISO")" echo # ---- tooling check ------------------------------------------------------- for cmd in xorriso unsquashfs mksquashfs git sha512sum mkarchroot makechrootpkg repo-add makepkg; do command -v "$cmd" >/dev/null || { echo "ERROR: missing tool: $cmd" >&2 echo " install with: sudo pacman -S libisoburn squashfs-tools git coreutils devtools pacman-contrib" >&2 exit 1 } done # ---- OPTIONAL master SSH public key (OPT-IN ONLY) ------------------------- # The ISO is distributable: by default NO key is baked and sshd stays disabled # on installed systems — nothing identifying the build host ships on the # image. For a personal/dev build (remote rescue over SSH), opt in explicitly: # SSH_PUBKEY=/path/to/key.pub ./nosignal.sh # (The old behaviour of auto-baking ~/.ssh/id_*.pub was removed deliberately — # do not reintroduce it; a give-away ISO must never carry the builder's key.) SSH_PUBKEY="${SSH_PUBKEY:-}" HAVE_KEY=0 if [ -z "$SSH_PUBKEY" ] || [ "$SSH_PUBKEY" = "none" ]; then echo " SSH key: (none — sshd disabled on installs; distributable default." echo " Opt in with SSH_PUBKEY=/path/to/key.pub for a dev build.)" elif [ -f "$SSH_PUBKEY" ]; then HAVE_KEY=1 echo " SSH key: $SSH_PUBKEY (DEV BUILD — key baked in, sshd enabled on installs;" echo " do NOT distribute this ISO)" else echo "ERROR: SSH_PUBKEY=$SSH_PUBKEY not found" >&2 exit 1 fi # ---- offline payload ------------------------------------------------------ build_offline_payload # ---- workspace ----------------------------------------------------------- sudo rm -rf "$WORK" mkdir -p "$WORK" SFS_DIR="$WORK/airootfs" # ---- pull squashfs out of the stock ISO --------------------------------- echo "==> Extracting airootfs.sfs from stock ISO..." xorriso -osirrox on -indev "$STOCK_ISO" \ -extract /arch/x86_64/airootfs.sfs "$WORK/airootfs-stock.sfs" 2>&1 | tail -3 echo "==> Unsquashing airootfs (slow: ~1 min)..." sudo unsquashfs -d "$SFS_DIR" "$WORK/airootfs-stock.sfs" >/dev/null # ---- inject payload ------------------------------------------------------ if [ "$HAVE_KEY" = 1 ]; then echo "==> Staging master SSH key for first-boot SSH access..." sudo install -m 644 -o root -g root "$SSH_PUBKEY" "$SFS_DIR/root/master.pub" fi echo "==> Injecting installer.sh..." INSTALLER_TMP="$WORK/installer.sh" write_installer "$INSTALLER_TMP" sudo install -m 755 -o root -g root "$INSTALLER_TMP" "$SFS_DIR/root/installer.sh" echo "==> Masking online-only units (they hang/hijack the console offline)..." # With no network, archlinux-keyring-wkd-sync pulls systemd-time-wait-sync, # which waits for NTP sync FOREVER ("no limit") and repaints the console with # its start-job spinner right over the running installer. Useless offline. for unit in systemd-time-wait-sync.service archlinux-keyring-wkd-sync.service archlinux-keyring-wkd-sync.timer; do sudo ln -sf /dev/null "$SFS_DIR/etc/systemd/system/$unit" done echo "==> Wiring getty@tty1 to auto-launch the installer..." sudo mkdir -p "$SFS_DIR/etc/systemd/system/getty@tty1.service.d" sudo tee "$SFS_DIR/etc/systemd/system/getty@tty1.service.d/override.conf" >/dev/null <<'EOF' [Service] ExecStart= ExecStart=-/usr/bin/bash /root/installer.sh StandardInput=tty StandardOutput=tty Restart=no RestartPreventExitStatus=1 2 3 4 5 6 7 8 EOF # ---- extend USB enumeration timeout in boot loader cmdline -------------- # Slow / fussy USB controllers (some Intel mini-PCs, NUCs) don't enumerate USB # block devices within archiso's default search window. Adding rootdelay=60 to # the kernel cmdline makes initramfs wait 60s before mounting root — fast # hardware finds the device in 2s and moves on; slow hardware gets breathing # room. consoleblank=0 keeps the live install console from DPMS-blanking. echo "==> Patching bootloader cmdlines (rootdelay=60 for slow USB enumeration)..." mkdir -p "$WORK/boot-edit" BOOT_CFG_FILES=( /loader/entries/01-archiso-linux.conf /loader/entries/02-archiso-speech-linux.conf /boot/syslinux/archiso_sys-linux.cfg ) for path in "${BOOT_CFG_FILES[@]}"; do dest="$WORK/boot-edit/$(basename "$path")" # Don't pipe xorriso straight into tail — that masks its exit status under # 'set -e', so a missing boot file would slip through here and instead blow # up later at the unconditional -map during repack, with a murkier error. if ! xorriso -osirrox on -indev "$STOCK_ISO" -extract "$path" "$dest" 2>"$WORK/xorriso-extract.log"; then echo "ERROR: failed to extract $path from stock ISO:" >&2 tail -3 "$WORK/xorriso-extract.log" >&2 exit 1 fi done for f in "$WORK/boot-edit"/*; do [ -f "$f" ] || continue sed -i -E '/archisosearchuuid=/ { /rootdelay=/! s/$/ rootdelay=60/ }' "$f" sed -i -E '/archisosearchuuid=/ { /consoleblank=/! s/$/ consoleblank=0/ }' "$f" done # ---- re-squash ----------------------------------------------------------- echo "==> Re-squashing airootfs (slow: ~2 min)..." sudo rm -f "$WORK/airootfs.sfs" sudo mksquashfs "$SFS_DIR" "$WORK/airootfs.sfs" \ -comp xz -Xbcj x86 -b 1M -noappend -no-progress -quiet # ---- regenerate sha512 -------------------------------------------------- echo "==> Recalculating airootfs.sha512..." ( cd "$WORK" && sudo sha512sum airootfs.sfs | sudo tee airootfs.sha512 >/dev/null ) # ---- repack ISO --------------------------------------------------------- # -boot_image any replay regenerates the hybrid MBR/GPT + El Torito layout # SIZED TO THE NEW IMAGE. Never copy the stock system_area verbatim: its # partition table describes the stock 1.2GB ISO, so everything past that # (the /nosignal payload) lands OUTSIDE any partition and is unreadable when # the media is mounted via a partition device (USB boot on real hardware). echo "==> Repacking ISO (offline repo + replayed boot layout + volume UUID)..." sudo rm -f "$OUT_ISO" # Force output volume UUID to match the stock's. archiso's initramfs hook # searches for the boot media by this UUID (also baked into the kernel cmdline # as archisosearchuuid=) — if xorriso regenerates the UUID with a fresh build # timestamp, the search misses the USB entirely. Pinning the UUID keeps stock # cmdline + marker file in sync. STOCK_UUID=$(blkid -s UUID -o value "$STOCK_ISO") STOCK_UUID_RAW=$(echo "$STOCK_UUID" | tr -d '-') xorriso \ -indev "$STOCK_ISO" \ -outdev "$OUT_ISO" \ -volume_date "uuid" "$STOCK_UUID_RAW" \ -boot_image any replay \ -rm /arch/x86_64/airootfs.sfs -- \ -map "$WORK/airootfs.sfs" /arch/x86_64/airootfs.sfs \ -rm /arch/x86_64/airootfs.sha512 -- \ -map "$WORK/airootfs.sha512" /arch/x86_64/airootfs.sha512 \ -rm /loader/entries/01-archiso-linux.conf -- \ -map "$WORK/boot-edit/01-archiso-linux.conf" /loader/entries/01-archiso-linux.conf \ -rm /loader/entries/02-archiso-speech-linux.conf -- \ -map "$WORK/boot-edit/02-archiso-speech-linux.conf" /loader/entries/02-archiso-speech-linux.conf \ -rm /boot/syslinux/archiso_sys-linux.cfg -- \ -map "$WORK/boot-edit/archiso_sys-linux.cfg" /boot/syslinux/archiso_sys-linux.cfg \ -map "$OFFLINE/iso" /nosignal \ -end 2>&1 | tail -5 # ---- cleanup ------------------------------------------------------------ sudo chown "$INVOKING_USER:$INVOKING_GROUP" "$OUT_ISO" sudo rm -rf "$WORK" echo echo "==========================================================" echo " Done: $OUT_ISO" echo " Size: $(du -h "$OUT_ISO" | cut -f1)" echo echo " Fully offline installer — no network needed on the target." echo echo " Burn to USB:" echo " sudo dd if='$OUT_ISO' of=/dev/sdX bs=4M status=progress conv=fsync" echo echo " Or drop $(basename "$OUT_ISO") into your Ventoy USB." echo "=========================================================="