diff --git a/config/install.conf b/config/install.conf index f1d71d8..41bf476 100644 --- a/config/install.conf +++ b/config/install.conf @@ -7,7 +7,7 @@ HOSTNAME="xps9700" USERNAME="moze" USER_FULLNAME="moze" USER_UID="1000" -USER_GROUPS="wheel,docker,video,audio,input,plugdev,network,kvm,users" +USER_GROUPS="wheel,docker,video,audio,input,plugdev,network,kvm,users,bluetooth" DEFAULT_SHELL="/bin/bash" # ---------- Locale ---------- diff --git a/config/profiles/mainline-niri/customizations/niri.sh b/config/profiles/mainline-niri/customizations/niri.sh index be426ab..372f2be 100644 --- a/config/profiles/mainline-niri/customizations/niri.sh +++ b/config/profiles/mainline-niri/customizations/niri.sh @@ -47,10 +47,8 @@ prefer-no-csd spawn-at-startup "swaybg" "-i" "/usr/share/backgrounds/void-installer/pxfuel.jpg" spawn-at-startup "mako" -spawn-at-startup "nm-applet" "--indicator" -spawn-at-startup "blueman-applet" spawn-at-startup "/usr/libexec/polkit-gnome-authentication-agent-1" -spawn-at-startup "noctalia-shell" +spawn-at-startup "sh" "-c" "i=0; while [ \$i -lt 30 ] && ! dbus-send --system --print-reply --dest=org.freedesktop.DBus /org/freedesktop/DBus org.freedesktop.DBus.GetNameOwner string:org.bluez >/dev/null 2>&1; do sleep 1; i=\$((i+1)); done; exec noctalia-shell" cursor { xcursor-theme "Bibata-Modern-Ice" diff --git a/config/profiles/mainline-niri/packages.list b/config/profiles/mainline-niri/packages.list index ec4f3c3..f60c0e4 100644 --- a/config/profiles/mainline-niri/packages.list +++ b/config/profiles/mainline-niri/packages.list @@ -110,10 +110,12 @@ gvfs-smb file-roller gnome-keyring seahorse -network-manager-applet blueman bluez +# --- bluetooth audio --- +bluez-alsa + # --- display manager --- # niri can be launched directly via TTY (`niri-session`) or via a wayland-aware # greeter. We use greetd + tuigreet — lighter than lightdm under wayland. @@ -170,9 +172,6 @@ system-config-printer sane simple-scan -# --- bluetooth audio --- -bluez-alsa - # --- backups / snapshots --- timeshift grub-btrfs diff --git a/config/profiles/mainline-niri/packages.live-desktop.list b/config/profiles/mainline-niri/packages.live-desktop.list index 13972aa..80b4308 100644 --- a/config/profiles/mainline-niri/packages.live-desktop.list +++ b/config/profiles/mainline-niri/packages.live-desktop.list @@ -89,8 +89,8 @@ brightnessctl ImageMagick python3 upower +power-profiles-daemon wl-clipboard -network-manager-applet # --- XDG portals --- xdg-desktop-portal @@ -98,7 +98,7 @@ xdg-desktop-portal-gnome xdg-utils xdg-user-dirs -# --- nix (for prebaked packages — spotify, discord, vscode, fastfetch, etc.) --- +# --- nix (for prebaked packages — spotify, discord, google-chrome, vscode, fastfetch, etc.) --- nix # --- noctalia-shell (from noctalia third-party XBPS repo) --- diff --git a/installer/first-login.sh b/installer/first-login.sh index 0914496..014f9e5 100644 --- a/installer/first-login.sh +++ b/installer/first-login.sh @@ -2,7 +2,7 @@ # First-login one-shot setup for the user. # Installs: Claude Code, NVM + node LTS, VS Code extensions, # and (if NIX_PACKAGES_FILE is present) nix user packages -# (google-chrome, spotify, discord, localsend, mission-center). +# (google-chrome, spotify, discord, localsend, mission-center, vscode). # Idempotent: creates ~/.first-login-done marker on success. # NOTE: do NOT use `set -u` here — nvm.sh references unbound vars. @@ -23,16 +23,17 @@ if ! curl -fsSL --max-time 3 --connect-timeout 3 -o /dev/null https://api.github exit 0 fi -# --- Claude Code (official native installer) --- mkdir -p "$HOME/.local/bin" export PATH="$HOME/.local/bin:$PATH" + +# --- Claude Code (official native installer) --- if ! command -v claude >/dev/null 2>&1 && [[ ! -x "$HOME/.local/bin/claude" ]]; then echo "==> installing Claude Code via official installer" curl -fsSL https://claude.ai/install.sh | bash || { echo "!! claude install failed"; } fi -# --- Nix user packages (google-chrome, spotify, discord, etc.) --- +# --- Nix user packages (google-chrome, spotify, discord, vscode, etc.) --- # Present when running from the live ISO (written by build-live-iso.sh). # In the installed system the packages come from first-boot-nix.sh instead. # NOTE: nix packages are intentionally skipped in the live session — they @@ -72,6 +73,7 @@ if [[ -r "$NIX_PACKAGES_FILE" ]] && command -v nix >/dev/null 2>&1; then fi export NIXPKGS_ALLOW_UNFREE=1 + export NIX_REMOTE=local mapfile -t pkgs < <(grep -vE '^\s*(#|$)' "$NIX_PACKAGES_FILE") if [[ ${#pkgs[@]} -gt 0 ]]; then diff --git a/installer/install.sh b/installer/install.sh index 00d6260..25d6737 100755 --- a/installer/install.sh +++ b/installer/install.sh @@ -98,7 +98,7 @@ main() { configure_nvidia_prime configure_zram configure_nix - install_vscode_real + [[ "${DESKTOP:-cinnamon}" != "niri" ]] && install_vscode_real install_customizations enable_services install_grub diff --git a/installer/lib/postinstall.sh b/installer/lib/postinstall.sh index e1805df..c62c9d6 100755 --- a/installer/lib/postinstall.sh +++ b/installer/lib/postinstall.sh @@ -206,13 +206,12 @@ mark=/var/lib/first-boot-nix.done [[ -f "\$mark" ]] && exit 0 # Wait for nix-daemon to be available. -# The Void xbps nix package puts the socket at /var/nix/daemon-socket/socket. for _ in \$(seq 1 60); do - [[ -S /var/nix/daemon-socket/socket ]] && break + [[ -S /nix/var/nix/daemon-socket/socket ]] && break sleep 2 done -if [[ ! -S /var/nix/daemon-socket/socket ]]; then +if [[ ! -S /nix/var/nix/daemon-socket/socket ]]; then echo "nix-daemon not available; aborting first-boot nix install" >&2 exit 0 fi @@ -230,6 +229,12 @@ touch "\$mark" EOF chmod 0755 "$TARGET/usr/local/libexec/first-boot-nix.sh" + # Persistent nixpkgs config so the installed user can install unfree packages + # without needing to export NIXPKGS_ALLOW_UNFREE=1 every time. + install -d -m 0755 "$TARGET/home/$USERNAME/.config/nixpkgs" + echo '{ allowUnfree = true; }' > "$TARGET/home/$USERNAME/.config/nixpkgs/config.nix" + run_chroot "chown -R $USERNAME:$USERNAME /home/$USERNAME/.config/nixpkgs" + # runit one-shot service. install -d -m 0755 "$TARGET/etc/sv/first-boot-nix" cat > "$TARGET/etc/sv/first-boot-nix/run" <<'EOF' @@ -312,10 +317,8 @@ enable_services() { local enabled=( dbus NetworkManager - lightdm polkitd docker - bluetoothd acpid tlp elogind @@ -326,6 +329,14 @@ enable_services() { cupsd cups-browsed ) + + # Display manager: greetd for wayland/niri, lightdm for cinnamon. + if [[ "${DESKTOP:-cinnamon}" == "niri" ]]; then + enabled+=(greetd bluetoothd) + else + enabled+=(lightdm bluetoothd) + fi + [[ "${SSHD_ENABLE:-no}" == "yes" ]] && enabled+=(sshd) for svc in "${enabled[@]}"; do diff --git a/iso/_inner-build-live.sh b/iso/_inner-build-live.sh index 05558c9..8e4fc20 100755 --- a/iso/_inner-build-live.sh +++ b/iso/_inner-build-live.sh @@ -112,6 +112,8 @@ if [[ -n "${NIX_PACKAGES_PREBAKE:-}" ]]; then echo " staging /nix into overlay ($(du -sh /nix/store 2>/dev/null | cut -f1))" mkdir -p "$INCLUDE_DIR/nix" rsync -a /nix/ "$INCLUDE_DIR/nix/" 2>&1 | tail -1 + # Single-user nix: the live user (uid 1000) must own the store to create lock files and new paths. + chown -R 1000:1000 "$INCLUDE_DIR/nix" # /etc/skel/.nix-profile → the pre-baked store profile path. # dracut's adduser.sh runs 'useradd -m' which copies skel → /home/live, diff --git a/iso/_inner-build-niri-live.sh b/iso/_inner-build-niri-live.sh index f4a4257..780fb91 100755 --- a/iso/_inner-build-niri-live.sh +++ b/iso/_inner-build-niri-live.sh @@ -73,6 +73,8 @@ if [[ -n "${NIX_PACKAGES_PREBAKE:-}" ]]; then echo " staging /nix into overlay ($(du -sh /nix/store 2>/dev/null | cut -f1))" mkdir -p "$INCLUDE_DIR/nix" rsync -a /nix/ "$INCLUDE_DIR/nix/" 2>&1 | tail -1 + # Single-user nix: the live user (uid 1000) must own the store to create lock files and new paths. + chown -R 1000:1000 "$INCLUDE_DIR/nix" _STORE_PROFILE=$(cat "$_NIX_CACHE/.profile-path" 2>/dev/null \ || readlink -f /root/.nix-profile 2>/dev/null || echo "") @@ -113,9 +115,10 @@ for ua in ("curl/8.0", "xbps/0.59.2", "Mozilla/5.0 (X11; Linux x86_64)"): data = urllib.request.urlopen(req, timeout=15).read() tf = tarfile.open(fileobj=io.BytesIO(data)) idx = plistlib.loads(tf.extractfile("index.plist").read()) - for pkgver, meta in idx.items(): - if isinstance(meta, dict) and meta.get("pkgname") in want: - found[meta["pkgname"]] = pkgver + # index.plist keys are package names; pkgver field holds the versioned name + for pkgname, meta in idx.items(): + if isinstance(meta, dict) and pkgname in want: + found[pkgname] = meta.get("pkgver", pkgname) if len(found) >= len(want): break except Exception: diff --git a/iso/build-live-iso.sh b/iso/build-live-iso.sh index 7d19e05..b2c8757 100755 --- a/iso/build-live-iso.sh +++ b/iso/build-live-iso.sh @@ -90,7 +90,7 @@ LIVE_USER="${USERNAME:-live}" LIVE_USER="${USERNAME:-live}" # Extra groups (dracut only adds audio,video,wheel) -for g in plugdev input network docker; do +for g in plugdev input network docker bluetooth; do groupadd -f "$g" 2>/dev/null || true usermod -aG "$g" "$LIVE_USER" 2>/dev/null || true done @@ -107,6 +107,7 @@ if [ -x /usr/bin/nix ]; then install -d -m 0755 /etc/nix cat > /etc/nix/nix.conf </dev/null || true done @@ -570,6 +571,13 @@ if [[ -d "${HOME:-}/.nix-profile/bin" ]]; then *) export PATH="$HOME/.nix-profile/bin:$PATH" ;; esac fi +export NIXPKGS_ALLOW_UNFREE=1 +# Pre-baked nix is single-user (no daemon) — bypass daemon connection attempt +export NIX_REMOTE=local +# Flake commands ignore NIXPKGS_ALLOW_UNFREE unless --impure is passed. +# Wrap nix so interactive installs work without extra flags. +nix() { command nix "$@" --impure; } +export -f nix NIXEOF @@ -641,6 +649,10 @@ cat > "$INCLUDE_DIR/etc/profile.d/nix-packages-path.sh" <<'EOF' export NIX_PACKAGES_FILE=/usr/local/libexec/nix-packages.list EOF +# nixpkgs config: allow unfree packages for all users +install -d -m 0755 "$INCLUDE_DIR/etc/skel/.config/nixpkgs" +echo '{ allowUnfree = true; }' > "$INCLUDE_DIR/etc/skel/.config/nixpkgs/config.nix" + # ── 3g) Skel: .bash_profile sources .bashrc only (no first-login autorun) ── install -d -m 0755 "$INCLUDE_DIR/etc/skel" cat > "$INCLUDE_DIR/etc/skel/.bash_profile" <<'EOF' diff --git a/iso/build-niri-live-iso.sh b/iso/build-niri-live-iso.sh index eef314e..9c1190f 100755 --- a/iso/build-niri-live-iso.sh +++ b/iso/build-niri-live-iso.sh @@ -119,7 +119,7 @@ LIVE_USER="${USERNAME:-live}" LIVE_USER="${LIVE_USER:-live}" # Extra groups (dracut only adds audio,video,wheel) -for g in plugdev input network video audio _seatd; do +for g in plugdev input network video audio _seatd bluetooth; do groupadd -f "$g" 2>/dev/null || true usermod -aG "$g" "$LIVE_USER" 2>/dev/null || true done @@ -145,6 +145,7 @@ XDG_RUN="/run/user/$(id -u "$LIVE_USER" 2>/dev/null || echo 1000)" install -d -m 0700 "$XDG_RUN" 2>/dev/null || true chown "$LIVE_USER" "$XDG_RUN" 2>/dev/null || true + # Start elogind here, once, before runsvdir brings up greetd. # Runit does NOT supervise it — this avoids cgroup-race restart spam. # We wait until dbus is available (dbus service starts first in runsvdir), @@ -209,7 +210,7 @@ sleep 3 EOF chmod 0755 "$INCLUDE_DIR/etc/sv/elogind/finish" -for svc in dbus elogind NetworkManager sshd; do +for svc in dbus elogind NetworkManager bluetoothd sshd; do ln -sf "/etc/sv/$svc" "$INCLUDE_DIR/etc/runit/runsvdir/default/$svc" done @@ -237,6 +238,13 @@ if [[ -d "${HOME:-}/.nix-profile/bin" ]]; then *) export PATH="$HOME/.nix-profile/bin:$PATH" ;; esac fi +export NIXPKGS_ALLOW_UNFREE=1 +# Pre-baked nix is single-user (no daemon) — bypass daemon connection attempt +export NIX_REMOTE=local +# Flake commands ignore NIXPKGS_ALLOW_UNFREE unless --impure is passed. +# Wrap nix so interactive installs work without extra flags. +nix() { command nix "$@" --impure; } +export -f nix EOF chmod 0644 "$INCLUDE_DIR/etc/profile.d/nix-prebaked.sh" @@ -244,6 +252,7 @@ chmod 0644 "$INCLUDE_DIR/etc/profile.d/nix-prebaked.sh" install -d -m 0755 "$INCLUDE_DIR/etc/nix" cat > "$INCLUDE_DIR/etc/nix/nix.conf" < "$INCLUDE_DIR/etc/skel/.config/nixpkgs/config.nix" + # ── 3e) niri config.kdl in /etc/skel ─────────────────────────────────── # dracut's adduser.sh copies skel → /home/live, so the live user gets a # ready niri config without any first-boot setup step. @@ -314,15 +327,17 @@ spawn-at-startup "pipewire" spawn-at-startup "pipewire-pulse" spawn-at-startup "wireplumber" -// Background, notifications, network, bluetooth, auth +// Background, notifications and auth spawn-at-startup "swaybg" "-i" "/usr/share/backgrounds/void-installer/pxfuel.jpg" "-m" "fill" spawn-at-startup "mako" -spawn-at-startup "nm-applet" "--indicator" -spawn-at-startup "blueman-applet" spawn-at-startup "/usr/libexec/polkit-gnome-authentication-agent-1" -// noctalia-shell (Quickshell-based Wayland shell) -spawn-at-startup "quickshell" "-c" "noctalia-shell" +// noctalia-shell — wait for org.bluez before launching so the BT module +// initialises correctly (bluetoothd may not be on D-Bus yet at this point). +spawn-at-startup "sh" "-c" "i=0; while [ \$i -lt 30 ] && ! dbus-send --system --print-reply --dest=org.freedesktop.DBus /org/freedesktop/DBus org.freedesktop.DBus.GetNameOwner string:org.bluez >/dev/null 2>&1; do sleep 1; i=\$((i+1)); done; exec quickshell -c noctalia-shell" + +// First-login setup: installs Claude Code (and NVM) once, then closes +spawn-at-startup "sh" "-c" "[ -f ~/.first-login-done ] || alacritty -T 'Void Setup' -e /usr/local/libexec/first-login.sh" binds { Mod+T { spawn "alacritty"; } @@ -408,6 +423,28 @@ if [[ -d "$BIBATA_SRC" ]]; then echo " cursor: Bibata-Modern-Ice" fi +# first-login.sh — deployed by _deploy_first_login() to the installed system. +[[ -r "$PROJECT_DIR/installer/first-login.sh" ]] && \ + install -m 0755 "$PROJECT_DIR/installer/first-login.sh" "$OVERLAY/first-login.sh" + +# Claude Code config + auth tokens from host (deployed to the installed system). +CLAUDE_SRC="${CLAUDE_SRC:-$HOME/.claude}" +[[ -d "$CLAUDE_SRC" ]] && { cp -a "$CLAUDE_SRC" "$OVERLAY/claude"; echo " claude: ~/.claude bundled"; } +[[ -r "${HOME}/.claude.json" ]] && install -m 0600 "${HOME}/.claude.json" "$OVERLAY/claude.json" + +# VS Code user config + extension list from host. +VSCODE_SRC="${VSCODE_USER_SRC:-$HOME/.config/Code/User}" +install -d -m 0755 "$OVERLAY/vscode-user" +if [[ -d "$VSCODE_SRC" ]]; then + for f in settings.json keybindings.json; do + [[ -r "$VSCODE_SRC/$f" ]] && install -m 0644 "$VSCODE_SRC/$f" "$OVERLAY/vscode-user/$f" + done + [[ -d "$VSCODE_SRC/snippets" ]] && cp -a "$VSCODE_SRC/snippets" "$OVERLAY/vscode-user/snippets" + command -v code >/dev/null 2>&1 && \ + code --list-extensions > "$OVERLAY/vscode-extensions.txt" 2>/dev/null || true + echo " vscode-user: staged from $VSCODE_SRC" +fi + # Copy wallpapers and assets into usr/share (rootfs overlay) install -d -m 0755 "$INCLUDE_DIR/usr/share/backgrounds/void-installer" cp -a "$OVERLAY/wallpapers"/. "$INCLUDE_DIR/usr/share/backgrounds/void-installer/" 2>/dev/null || true @@ -488,6 +525,20 @@ exec /usr/local/lib/void-installer/install.sh "$@" WRAPEOF chmod 0755 "$INCLUDE_DIR/usr/local/bin/void-install" +# first-login.sh at /usr/local/libexec: available in the live session and +# deployed by _deploy_first_login() to the installed system via the overlay. +install -d -m 0755 "$INCLUDE_DIR/usr/local/libexec" +install -m 0755 "$PROJECT_DIR/installer/first-login.sh" \ + "$INCLUDE_DIR/usr/local/libexec/first-login.sh" + +# nix-packages.list: tells first-login.sh which nix packages to install. +# In the live session these are already prebaked; in the installed system the +# first-boot-nix runit service handles them, so this is informational only. +{ + printf '# Nix user packages\n' + printf '%s\n' "${NIX_USER_PACKAGES[@]}" +} > "$INCLUDE_DIR/usr/local/libexec/nix-packages.list" + _SECRETS_SRC="${SECRETS_ENV:-$PROJECT_DIR/secrets.env}" if [[ -r "$_SECRETS_SRC" ]]; then install -d -m 0755 "$INCLUDE_DIR/etc"