feat(niri): niri live config

This commit is contained in:
mozempk
2026-04-25 13:23:49 +02:00
parent a63446a832
commit 6d65f28844
10 changed files with 1142 additions and 53 deletions

4
.gitignore vendored
View File

@@ -12,9 +12,7 @@ id_ovh*
authorized_keys authorized_keys
# ── Generated build staging (build-iso.sh populates this at build time) ─ # ── Generated build staging (build-iso.sh populates this at build time) ─
build/includes/ build/*
build/live-includes/
build/first-login.sh
# ── Build artifacts ──────────────────────────────────────────────────── # ── Build artifacts ────────────────────────────────────────────────────
out/ out/

View File

@@ -78,6 +78,7 @@ NIX_USER_PACKAGES=(
"nixpkgs#google-chrome" "nixpkgs#google-chrome"
"nixpkgs#mission-center" "nixpkgs#mission-center"
"nixpkgs#vscode" "nixpkgs#vscode"
"nixpkgs#fastfetch"
) )
# ---------- Cinnamon customization ---------- # ---------- Cinnamon customization ----------

View File

@@ -0,0 +1,109 @@
# Packages included in the LIVE desktop ISO squashfs for the mainline-niri profile.
# Lines beginning with '#' or empty are skipped.
# noctalia-shell comes from the third-party repo added by build-niri-live-iso.sh.
# --- base / boot ---
base-system
linux-mainline
linux-firmware
linux-firmware-network
intel-ucode
dracut
# --- core userspace ---
sudo
bash
bash-completion
git
curl
wget
vim
nano
htop
tmux
unzip
zip
xz
rsync
pciutils
usbutils
lsof
file
which
man-pages
mdocml
ca-certificates
xtools
gptfdisk
parted
btrfs-progs
dosfstools
efibootmgr
# --- networking ---
NetworkManager
NetworkManager-openvpn
openssh
iwd
chrony
# --- audio (pipewire stack) ---
pipewire
wireplumber
alsa-pipewire
pavucontrol
alsa-utils
playerctl
# --- Wayland session ---
mesa-dri
niri
xwayland-satellite
elogind
seatd
dbus
# --- display manager ---
greetd
tuigreet
# --- terminal + launcher ---
alacritty
fuzzel
foot
# --- notification + background ---
mako
swaybg
# --- bluetooth ---
bluez
blueman
# --- polkit (auth dialogs) ---
polkit
polkit-gnome
# --- noctalia-shell runtime deps ---
brightnessctl
ImageMagick
python3
upower
wl-clipboard
network-manager-applet
# --- XDG portals ---
xdg-desktop-portal
xdg-desktop-portal-gnome
xdg-utils
xdg-user-dirs
# --- nix (for prebaked packages — spotify, discord, vscode, fastfetch, etc.) ---
nix
# --- noctalia-shell (from noctalia third-party XBPS repo) ---
noctalia-shell
# --- fonts ---
noto-fonts-ttf
noto-fonts-emoji

View File

@@ -85,44 +85,74 @@ The `/etc/lightdm/.session` file (content: `cinnamon`) is read by the hook to se
## Nix Integration ## Nix Integration
### Daemon mode (not single-user) ### Prebake architecture (packages baked into squashfs)
The Void `nix` xbps package ships `nix-daemon` with a runit service at `/etc/sv/nix-daemon`. The daemon puts its socket at: Nix packages are **pre-installed at ISO build time** inside the Docker container and the entire `/nix` store is rsynced into the squashfs overlay. This means packages are available immediately on boot — no downloads, no tmpfs space pressure.
```
/var/nix/daemon-socket/socket **Why not install at first login?** The live system mounts squashfs + tmpfs overlay. Installing ~4 GB of nix packages at runtime fills the tmpfs overlay and causes out-of-space failures. Baking them into squashfs sidesteps this completely.
### Build-time nix install (inside Docker, single-user)
Docker runs as root. Nix is installed in single-user mode (no daemon, no nixbld group):
```sh
mkdir -m 0755 -p /nix
export NIX_CONFIG="build-users-group = " # suppress nixbld group requirement
curl -L https://nixos.org/nix/install | sh -s -- --no-daemon
source /root/.nix-profile/etc/profile.d/nix.sh
export PATH="/root/.nix-profile/bin:$PATH"
NIXPKGS_ALLOW_UNFREE=1 nix profile add \
--extra-experimental-features "nix-command flakes" --impure \
nixpkgs#spotify nixpkgs#discord ...
``` ```
We use daemon mode (not single-user) because `/nix/store` stays root-owned. The live user is granted trust via `nix.conf`: The full `/nix` directory is then staged into the squashfs overlay:
```sh
rsync -a /nix/ "$INCLUDE_DIR/nix/"
```
### Nix prebake cache
To avoid re-downloading packages on every build, the nix store is cached at:
```
cache/nix-prebake/<md5-of-package-list>/
```
If the cache exists and the package list md5 matches, the build restores from cache instead of re-running `nix profile add`. Cache is ~5 GB. Subsequent builds with an unchanged package list complete the nix step in ~1 minute instead of ~20 minutes.
### Current packages (NIX_USER_PACKAGES in build-live-iso.sh)
- `nixpkgs#google-chrome` — replaces chromium (removed from xbps packages)
- `nixpkgs#spotify`
- `nixpkgs#discord`
- `nixpkgs#localsend`
- `nixpkgs#mission-center`
- `nixpkgs#vscode`
### XDG / PATH setup for live user
For Cinnamon to find nix `.desktop` files and for terminals to find nix binaries:
- `/etc/environment`: `XDG_DATA_DIRS=/home/live/.nix-profile/share:/usr/local/share:/usr/share`
- `/etc/profile.d/nix-prebaked.sh`: adds nix profile to `PATH` for terminal sessions
- `/etc/skel/.nix-profile` → symlink to the pre-baked store profile, copied to `/home/live/` when the live user is created by the dracut hook
### Live system nix-daemon (daemon mode)
On the **booted live system**, the Void `nix` xbps package provides `nix-daemon` as a runit service. `/nix/store` stays root-owned; the live user is granted trust via `nix.conf`:
``` ```
experimental-features = nix-command flakes experimental-features = nix-command flakes
sandbox = false sandbox = false
auto-optimise-store = true auto-optimise-store = true
trusted-users = root live trusted-users = root live
``` ```
The daemon socket is at `/var/nix/daemon-socket/socket` (Void's path, not the upstream default `/nix/var/nix/daemon-socket/socket`).
`sandbox = false` is required because the live system has no `nixbld` users and no user namespaces in the dracut initramfs environment. `sandbox = false` is required — no `nixbld` users exist in the dracut initramfs environment.
### Package list
`/usr/local/libexec/nix-packages.list` is written at ISO build time from `NIX_USER_PACKAGES` in `config/install.conf`. At first login, `first-login.sh` reads this file and runs `nix profile install --impure` with `NIXPKGS_ALLOW_UNFREE=1`.
Current packages:
- `nixpkgs#google-chrome`
- `nixpkgs#spotify`
- `nixpkgs#discord`
- `nixpkgs#localsend`
- `nixpkgs#mission-center`
### postinstall.sh socket path (installed system) ### postinstall.sh socket path (installed system)
In the **installed system** (not live), `installer/lib/postinstall.sh` polls for the nix-daemon socket. The correct path is: In the **installed system** (not live), `installer/lib/postinstall.sh` polls for the nix-daemon socket at:
``` ```
/var/nix/daemon-socket/socket /var/nix/daemon-socket/socket
``` ```
Not `/nix/var/nix/daemon-socket/socket` (upstream Nix default) — Void's package uses `/var/nix/`. Not `/nix/var/nix/daemon-socket/socket` — Void's package uses `/var/nix/`.
--- ---
## dconf / Theme ## dconf / Theme
The Gruvbox-Dark GTK theme and Cinnamon dconf settings are pre-applied via a system-db. The dconf binary database must be compiled at **ISO build time**, not at runtime. Cinnamon settings (theme, keyboard layout, dark mode, etc.) are pre-applied via a dconf system-db. The binary database is compiled at **ISO build time** inside the Docker container.
### Build-time compilation ### Build-time compilation
`iso/_inner-build-live.sh` runs inside the Debian Docker container. The Dockerfile installs `dconf-cli` for this step. The correct Debian `dconf-cli` API is: `iso/_inner-build-live.sh` runs inside the Debian Docker container. The Dockerfile installs `dconf-cli` for this step. The correct Debian `dconf-cli` API is:
@@ -143,20 +173,47 @@ system-db:local
``` ```
Without this file, the compiled system-db is ignored and Cinnamon shows a black wallpaper with default GTK theme. Without this file, the compiled system-db is ignored and Cinnamon shows a black wallpaper with default GTK theme.
### System DB keyfile (`/etc/dconf/db/local.d/00-cinnamon`)
Built by `iso/build-live-iso.sh` from config values. Relevant excerpts:
```ini
[org/gnome/desktop/input-sources]
sources=[('xkb', 'ch+fr_nodeadkeys')]
[org/gnome/desktop/interface]
color-scheme='prefer-dark'
```
The `KEYMAP` variable comes from `config/install.conf` as `ch-fr_nodeadkeys` (vconsole dash format). The system DB uses XKB plus format. The substitution `${KEYMAP//-/+}` handles this conversion at build time.
### dconf lock file (critical for keyboard)
A lock file at `/etc/dconf/db/local.d/locks/keyboard` lists:
```
/org/gnome/desktop/input-sources/sources
```
This makes the keyboard setting **non-writable from the user session**`gsettings set org.gnome.desktop.input-sources sources ...` silently does nothing when this lock is in place. The correct value must be set in the system DB itself (see above). Do not attempt to override the keyboard via `gsettings` from `apply-live-settings.sh` or any autostart script.
### Keyboard format: vconsole (dash) vs XKB (plus)
- mklive.sh `-k` flag accepts vconsole format: `ch-fr_nodeadkeys` (dash-separated)
- XKB / gsettings / dconf uses plus format: `ch+fr_nodeadkeys`
- Bash substitution: `${KEYMAP//-/+}` converts vconsole → XKB
- `KEYMAP` is defined in `config/install.conf` in vconsole (dash) format
--- ---
## First-Login Setup (`installer/first-login.sh`) ## First-Login Setup (`apply-live-settings.sh`)
Runs once via XDG autostart (`~/.config/autostart/void-live-first-login.desktop`) when Cinnamon first loads. Installs: A lightweight XDG autostart script runs once when Cinnamon first loads and applies theme/UX settings via `gsettings`. It does **not** install packages (packages are pre-baked into squashfs).
1. **Claude Code** — official installer from `https://claude.ai/install.sh` **Location in ISO:** `/usr/local/libexec/apply-live-settings.sh`
2. **Nix user packages** — from `/usr/local/libexec/nix-packages.list` **Autostart:** `/etc/xdg/autostart/void-live-settings.desktop` (only in Cinnamon: `OnlyShowIn=X-Cinnamon`)
3. **NVM + Node LTS** **Idempotency guard:** creates `~/.void-live-settings-done` on success
4. **VS Code extensions** — from `/etc/installer-vscode-extensions.txt`
Idempotent: creates `~/.first-login-done` on success. Logs to `~/.first-login.log`. Settings applied:
- GTK/icon/cursor theme (Gruvbox-Dark)
- Cinnamon shell theme
- Wallpaper
- Default terminal (alacritty)
The script does NOT use `set -u` because `nvm.sh` references unbound variables. The script waits for `DBUS_SESSION_BUS_ADDRESS` to be set before calling `gsettings`. It does **not** set keyboard layout — that is locked in the dconf system DB (see dconf section above).
--- ---
@@ -164,19 +221,45 @@ The script does NOT use `set -u` because `nvm.sh` references unbound variables.
``` ```
iso/build-live-iso.sh (host — stages overlay, builds Docker image if needed) iso/build-live-iso.sh (host — stages overlay, builds Docker image if needed)
└─ Docker: void-installer-builder:latest └─ Docker: void-installer-builder:latest (debian:stable-slim)
└─ iso/_inner-build-live.sh └─ iso/_inner-build-live.sh
├─ dconf compile (pre-bakes system-db) ├─ nix prebake: install packages into /nix, rsync to $INCLUDE_DIR/nix/
└─ void-mklive/mklive.sh -a x86_64 -r <repo> -I <include_dir> ... │ └─ cache/nix-prebake/<md5>/ used if package list unchanged
└─ squashfs + GRUB + ISO 9660 ├─ dconf compile (compiles system-db binary from keyfile)
├─ void-mklive/mklive.sh -a x86_64 -r <repo> -I <include_dir> ...
│ └─ squashfs (xz) + GRUB + ISO 9660
└─ chown -R $HOST_UID:$HOST_GID $INCLUDE_DIR (fix Docker root ownership)
``` ```
Output: `out/void-live-stable.iso` (~2.9 GB) Output: `out/void-live-stable.iso` (~4.8 GB, xz-compressed squashfs ~22 GB uncompressed)
### Docker UID/GID ownership fix
Docker runs as root. Without remediation, files created inside the container (especially the ~5 GB nix store) are owned by `root` on the host, causing `rm -rf build/live-includes` to fail with `Permission denied` on the next build.
**Fix in `_inner-build-live.sh`** (end of script):
```sh
# Fix ownership so host user can clean up on next build
if [[ -n "${HOST_UID:-}" && "$HOST_UID" != "0" ]]; then
chmod -R u+w "$INCLUDE_DIR" 2>/dev/null || true
chown -R "${HOST_UID}:${HOST_GID}" "$INCLUDE_DIR" 2>/dev/null || true
fi
```
`HOST_UID` and `HOST_GID` are passed via `docker run -e HOST_UID=$(id -u) -e HOST_GID=$(id -g)`.
**Belt-and-suspenders guard in `build-live-iso.sh`** (before `rm -rf $INCLUDE_DIR`):
```sh
chmod -R u+w "$INCLUDE_DIR/nix" 2>/dev/null || sudo rm -rf "$INCLUDE_DIR/nix"
```
**Emergency manual cleanup:** `sudo rm -rf build/live-includes/nix`
### Dockerfile dependencies
`iso/Dockerfile` (based on `debian:stable-slim`) installs: `bash git curl ca-certificates xz-utils tar patch python3 mtools xorriso squashfs-tools dosfstools e2fsprogs kmod dconf-cli rsync`. The `rsync` package is required for nix store staging.
### Build artifacts that must NOT be committed ### Build artifacts that must NOT be committed
- `build/live-includes/` — generated staging tree (hundreds of binary assets) - `build/live-includes/` — generated staging tree (hundreds of binary assets, nix store)
- `out/` — ISO output - `out/` — ISO output
- `cache/` — cloned void-mklive, xbps package cache - `cache/` — cloned void-mklive, xbps/nix package cache
--- ---
@@ -200,12 +283,50 @@ Use `nix profile add` instead. `nix profile install` is an alias that emits a wa
**Cause:** QEMU's user-mode DNS proxy may not forward queries correctly depending on the host network configuration. **Cause:** QEMU's user-mode DNS proxy may not forward queries correctly depending on the host network configuration.
**Workaround for QEMU testing:** `echo nameserver 8.8.8.8 > /etc/resolv.conf`. This is not needed on real hardware. **Workaround for QEMU testing:** `echo nameserver 8.8.8.8 > /etc/resolv.conf`. This is not needed on real hardware.
### Docker root-owned files break next build
**Symptom:** `rm -rf build/live-includes` or `rm -rf build/live-includes/nix` fails with `Permission denied` at the start of a rebuild.
**Cause:** Docker runs as root. The ~5 GB nix store rsynced into `build/live-includes/nix/` is owned by `root:root` on the host.
**Fix:** `_inner-build-live.sh` now `chown -R $HOST_UID:$HOST_GID $INCLUDE_DIR` at the end of each Docker run. `HOST_UID`/`HOST_GID` are passed as env vars. See Build Pipeline section.
**Emergency cleanup:** `sudo rm -rf build/live-includes/nix`
### dconf lock file silently blocks `gsettings set`
**Symptom:** `gsettings set org.gnome.desktop.input-sources sources "[('xkb', 'ch+fr_nodeadkeys')]"` runs without error but the keyboard layout is not applied.
**Cause:** `/etc/dconf/db/local.d/locks/keyboard` locks the `input-sources` key. Any `gsettings set` targeting a locked key is silently ignored in the user session.
**Fix:** Set the correct value in the system dconf DB keyfile at ISO build time. Do not attempt to set it from an autostart script.
### Keyboard format mismatch (vconsole dash vs XKB plus)
**Symptom:** Keyboard layout reverts to US QWERTY even though `KEYMAP=ch-fr_nodeadkeys` is set.
**Cause:** mklive.sh accepts the vconsole format (`ch-fr_nodeadkeys`, dash-separated). XKB / dconf uses plus format (`ch+fr_nodeadkeys`). Passing the vconsole string directly to the dconf system DB or to `gsettings` sets an unknown layout that falls back to US.
**Fix:** In `build-live-iso.sh`, use `${KEYMAP//-/+}` when writing the dconf keyfile:
```ini
[org/gnome/desktop/input-sources]
sources=[('xkb', 'ch+fr_nodeadkeys')] # generated as: ${KEYMAP//-/+}
```
--- ---
## QEMU Testing
### Quick launch
```bash
bash tests/launch-live-qemu.sh
# or via Makefile:
make live-qemu
```
### What `launch-live-qemu.sh` does
- RAM: 12288 MB, 4 CPUs, KVM acceleration
- Device: `virtio-vga` with `display gtk,gl=off` (no hardware GL)
- Searches `out/void-live-stable*.iso` for the ISO
- Serial console socket: `out/live-serial.sock`
- Monitor socket: `out/qemu-monitor.sock`
- Credentials: `live`/`voidlinux` (desktop), `root`/`voidlinux` (TTY)
### Manual launch (if needed)
```bash ```bash
cp /usr/share/OVMF/OVMF_VARS.fd out/OVMF_VARS.live.fd cp /usr/share/OVMF/OVMF_VARS.fd out/OVMF_VARS.live.fd
qemu-system-x86_64 -name void-live-test -machine q35,accel=kvm:tcg -cpu max \ qemu-system-x86_64 -name void-live-test -machine q35,accel=kvm:tcg -cpu max \
-m 4096 -smp 4 \ -m 12288 -smp 4 \
-drive "if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_CODE.fd" \ -drive "if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_CODE.fd" \
-drive "if=pflash,format=raw,file=out/OVMF_VARS.live.fd" \ -drive "if=pflash,format=raw,file=out/OVMF_VARS.live.fd" \
-cdrom out/void-live-stable.iso -boot order=d,menu=off \ -cdrom out/void-live-stable.iso -boot order=d,menu=off \
@@ -215,12 +336,20 @@ qemu-system-x86_64 -name void-live-test -machine q35,accel=kvm:tcg -cpu max \
-device virtio-vga -display gtk,gl=off & -device virtio-vga -display gtk,gl=off &
``` ```
Serial console access (root shell for diagnostics): ### Serial console access (Python)
```python ```python
import socket, time import socket
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect('out/live-serial.sock') s.connect('out/live-serial.sock')
# send commands, read output # send commands, read output
``` ```
GPU in QEMU: `virtio-vga` is detected as virtual → `modesetting + LIBGL_ALWAYS_SOFTWARE=1`. ### GPU in QEMU
`virtio-vga` is detected as a virtual GPU by `live-setup.sh` → writes `modesetting + AccelMethod none` xorg conf, sets `LIBGL_ALWAYS_SOFTWARE=1` in `/etc/profile.d/live-env.sh`.
### Verifying keyboard layout (in live session)
```bash
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus \
gsettings get org.gnome.desktop.input-sources sources
# expected: [('xkb', 'ch+fr_nodeadkeys')]
```

View File

@@ -10,7 +10,7 @@ ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
bash git curl ca-certificates xz-utils tar patch python3 \ bash git curl ca-certificates xz-utils tar patch python3 \
mtools xorriso squashfs-tools dosfstools e2fsprogs \ mtools xorriso squashfs-tools dosfstools e2fsprogs \
kmod dconf-cli rsync \ kmod dconf-cli rsync openssl \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# xbps-static is downloaded into /cache by the host script and added to PATH # xbps-static is downloaded into /cache by the host script and added to PATH

View File

@@ -27,14 +27,35 @@ command -v xbps-install.static >/dev/null \
mkdir -p "$(dirname "$OUT_ISO")" mkdir -p "$(dirname "$OUT_ISO")"
# Compile dconf system-db inside the include dir so it ships compiled. # Compile dconf system-db using Void's own dconf binary (inside the Void
# Debian's dconf-cli provides 'dconf compile <output_db> <keyfile_dir>'. # rootfs chroot via mklive's -x postsetup hook). This guarantees the GVDB
if command -v dconf >/dev/null 2>&1 && [[ -d "$INCLUDE_DIR/etc/dconf/db/local.d" ]]; then # binary is produced by the exact same dconf/glib version that runs on the
dconf compile "$INCLUDE_DIR/etc/dconf/db/local" \ # live system — no cross-distro format mismatch possible.
"$INCLUDE_DIR/etc/dconf/db/local.d" 2>/dev/null \ # (We do NOT pre-compile with Debian's dconf here; that caused silent failures
&& echo "dconf: compiled system-db/local" \ # when the GVDB format differed between the host and Void's glib.)
|| echo "dconf: compile failed (non-fatal)" _DCONF_POSTSETUP="$(mktemp -p "$MKLIVE_DIR" postsetup-dconf.XXXXX.sh)"
cat > "$_DCONF_POSTSETUP" <<'PSEOF'
#!/bin/bash
# Postsetup script: compile dconf system-db AND user skel db with Void's own binary.
ROOTFS="$1"
if [[ -x "$ROOTFS/usr/bin/dconf" ]] && [[ -d "$ROOTFS/etc/dconf/db/local.d" ]]; then
# 1. Compile system-db (read by all users via /etc/dconf/profile/user)
chroot "$ROOTFS" dconf compile /etc/dconf/db/local /etc/dconf/db/local.d \
&& echo "postsetup: system-db compiled ($(chroot "$ROOTFS" dconf --version 2>/dev/null))" \
|| echo "postsetup: system-db compile failed (non-fatal)"
# 2. Compile a USER dconf db directly into /etc/skel/.config/dconf/user.
# dracut's adduser.sh copies skel → /home/live on first boot, so the live
# user starts with the keyboard in their OWN user db — no dependency on
# dconf profile/system-db loading order.
mkdir -p "$ROOTFS/etc/skel/.config/dconf"
chroot "$ROOTFS" dconf compile /etc/skel/.config/dconf/user /etc/dconf/db/local.d \
&& echo "postsetup: skel user dconf db compiled" \
|| echo "postsetup: skel user dconf db compile failed (non-fatal)"
else
echo "postsetup: dconf or keyfile dir not found in rootfs — skipping dconf compile"
fi fi
PSEOF
chmod +x "$_DCONF_POSTSETUP"
cd "$MKLIVE_DIR" cd "$MKLIVE_DIR"
@@ -132,6 +153,13 @@ trap _cleanup_mklive_builds EXIT
-p "$ISO_PKGS" \ -p "$ISO_PKGS" \
-I "$INCLUDE_DIR" \ -I "$INCLUDE_DIR" \
-C "${BOOT_CMDLINE:-}" \ -C "${BOOT_CMDLINE:-}" \
-x "$_DCONF_POSTSETUP" \
-o "$OUT_ISO" -o "$OUT_ISO"
chown "$(stat -c '%u:%g' "$PROJECT_DIR")" "$OUT_ISO" "${OUT_ISO}".* 2>/dev/null || true # ownership of OUT_ISO is fixed unconditionally below with the include dir
# Fix ownership of include dir so the host user can clean up without sudo.
# u+rwX: sets read+write on all, execute only on directories (capital X).
chmod -R u+rwX "$INCLUDE_DIR" 2>/dev/null || true
chown -R "${HOST_UID:-1000}:${HOST_GID:-1000}" "$INCLUDE_DIR" 2>/dev/null || true
chown -R "${HOST_UID:-1000}:${HOST_GID:-1000}" "$OUT_ISO" "${OUT_ISO}".* 2>/dev/null || true

228
iso/_inner-build-niri-live.sh Executable file
View File

@@ -0,0 +1,228 @@
#!/bin/bash
# Runs INSIDE the docker container (as root). Invoked by iso/build-niri-live-iso.sh.
# Niri/Wayland variant: nix prebake (shared cache with Cinnamon), adds noctalia repo.
# Expects the project bind-mounted at /work and the cache at /cache.
#
# Required env (set by build-niri-live-iso.sh):
# ARCH, REPO_URL, KEYMAP, LOCALE, ISO_PKGS, ISO_TITLE, OUT_ISO_REL,
# INCLUDE_DIR_REL, NIX_PACKAGES_PREBAKE (optional)
set -Eeuo pipefail
: "${ARCH:?}"; : "${REPO_URL:?}"; : "${KEYMAP:?}"; : "${LOCALE:?}"
: "${ISO_PKGS:?}"; : "${ISO_TITLE:?}"; : "${OUT_ISO_REL:?}"
: "${INCLUDE_DIR_REL:?}"
CACHE_DIR=/cache
PROJECT_DIR=/work
MKLIVE_DIR="$CACHE_DIR/void-mklive-niri" # separate clone — avoids race with Cinnamon parallel build
INCLUDE_DIR="$PROJECT_DIR/$INCLUDE_DIR_REL"
OUT_ISO="$PROJECT_DIR/$OUT_ISO_REL"
# Third-party Void repo that ships noctalia-shell + noctalia-qs.
NOCTALIA_REPO="${NOCTALIA_REPO:-https://universalrepo.r1xelelo.workers.dev/void}"
export PATH="$CACHE_DIR/xbps-static/usr/bin:$PATH"
[[ -d "$MKLIVE_DIR" ]] || { echo "ERROR: $MKLIVE_DIR missing"; exit 1; }
[[ -d "$INCLUDE_DIR" ]] || { echo "ERROR: $INCLUDE_DIR missing"; exit 1; }
command -v xbps-install.static >/dev/null \
|| { echo "ERROR: xbps-install.static not on PATH"; exit 1; }
mkdir -p "$(dirname "$OUT_ISO")"
cd "$MKLIVE_DIR"
# ── Pre-bake nix packages ────────────────────────────────────────────────
# Shared with the Cinnamon build — same cache dir, same store.
if [[ -n "${NIX_PACKAGES_PREBAKE:-}" ]]; then
echo ">>> pre-baking nix packages"
read -r -a _NIX_PKGS <<< "$NIX_PACKAGES_PREBAKE"
_NIX_CACHE="$CACHE_DIR/nix-prebake"
_CACHE_KEY="$_NIX_CACHE/.done.$(printf '%s\n' "${_NIX_PKGS[@]}" | sort | md5sum | cut -c1-8)"
mkdir -p "$_NIX_CACHE"
if [[ -f "$_CACHE_KEY" ]] && [[ -d "$_NIX_CACHE/store" ]] && [[ -f "$_NIX_CACHE/.profile-path" ]]; then
echo " restoring cached nix store ($(du -sh "$_NIX_CACHE/store" 2>/dev/null | cut -f1))"
mkdir -p /nix
rsync -a "$_NIX_CACHE/" /nix/ 2>&1 | tail -1
else
echo " installing nix (single-user, no-daemon)..."
rm -rf /nix ~/.nix-profile ~/.nix-defexpr ~/.nix-channels
mkdir -m 0755 -p /nix
export NIX_CONFIG="build-users-group = "
curl -fsSL https://nixos.org/nix/install | \
NIX_INSTALLER_TRUST_INSTALLER=1 sh -s -- --no-daemon --no-channel-add
# shellcheck disable=SC1091
. /root/.nix-profile/etc/profile.d/nix.sh 2>/dev/null || true
export PATH="/root/.nix-profile/bin:/nix/var/nix/profiles/default/bin:$PATH"
export NIXPKGS_ALLOW_UNFREE=1
echo " nix profile install: ${_NIX_PKGS[*]}"
nix profile add --extra-experimental-features "nix-command flakes" \
--impure "${_NIX_PKGS[@]}" 2>&1
readlink -f /root/.nix-profile > "$_NIX_CACHE/.profile-path"
rsync -a /nix/ "$_NIX_CACHE/" 2>&1 | tail -1
touch "$_CACHE_KEY"
echo " cached nix store: $(du -sh "$_NIX_CACHE/store" 2>/dev/null | cut -f1)"
fi
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
_STORE_PROFILE=$(cat "$_NIX_CACHE/.profile-path" 2>/dev/null \
|| readlink -f /root/.nix-profile 2>/dev/null || echo "")
if [[ -n "$_STORE_PROFILE" && -d "$_STORE_PROFILE" ]]; then
mkdir -p "$INCLUDE_DIR/etc/skel"
ln -sf "$_STORE_PROFILE" "$INCLUDE_DIR/etc/skel/.nix-profile"
echo " skel/.nix-profile → $_STORE_PROFILE"
fi
fi
# ── end nix prebake ──────────────────────────────────────────────────────
# ── Noctalia: build a locally SIGNED XBPS repo ───────────────────────────
# noctalia-qs has a broken .sig2 on the CDN (RSA signature not valid).
# noctalia-shell's CDN key import also fails intermittently (EAGAIN).
# Solution: download both .xbps archives directly (no sig check), create a
# LOCAL SIGNED repo with a fresh keypair, register our public key in
# mklive/keys/ so copy_void_keys pre-trusts it in the rootfs.
# Our local signed repo gets HIGHEST priority (-r last = prepended first),
# so xbps resolves and verifies both packages against our trusted key.
echo ">>> building local signed noctalia XBPS repo (CDN .sig2 workaround)"
_NOC_LOCAL="/tmp/noctalia-local"
_NOC_HOME="/tmp/noc-sign-home"
_ARCH="${ARCH:-x86_64}"
mkdir -p "$_NOC_LOCAL" "$_NOC_HOME"
export HOME="$_NOC_HOME"
# Discover exact package versions from CDN repodata (try multiple User-Agents).
# Falls back to versions confirmed by previous build errors.
_NOC_VERS=$(python3 - <<'PYEOF' 2>/dev/null
import urllib.request, plistlib, tarfile, io, sys, os
repo = os.environ.get("NOCTALIA_REPO", "https://universalrepo.r1xelelo.workers.dev/void")
arch = os.environ.get("ARCH", "x86_64")
want = {"noctalia-qs", "noctalia-shell"}
found = {}
for ua in ("curl/8.0", "xbps/0.59.2", "Mozilla/5.0 (X11; Linux x86_64)"):
try:
req = urllib.request.Request(f"{repo}/{arch}-repodata", headers={"User-Agent": ua})
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
if len(found) >= len(want):
break
except Exception:
pass
# Fallback to versions confirmed by last build errors
defaults = {"noctalia-qs": "noctalia-qs-0.0.12_0", "noctalia-shell": "noctalia-shell-4.7.6_1"}
for pkg, ver in defaults.items():
if pkg not in found:
found[pkg] = ver
for ver in found.values():
print(ver)
PYEOF
)
# Download both .xbps archives directly (curl never checks .sig2)
for _ver in $_NOC_VERS; do
_fname="${_ver}.${_ARCH}.xbps"
# Clear any cached bad sig2 from a previous failed build
rm -f "$CACHE_DIR/xbps-niri-pkgs/${_ver}"* 2>/dev/null || true
if [[ ! -f "$_NOC_LOCAL/$_fname" ]]; then
echo " downloading $_fname ..."
curl -fsSL "$NOCTALIA_REPO/$_fname" -o "$_NOC_LOCAL/$_fname" \
|| { echo " WARNING: failed to download $_fname"; rm -f "$_NOC_LOCAL/$_fname"; }
else
echo " cached: $_fname"
fi
done
# Build index
xbps-rindex.static -a "$_NOC_LOCAL"/*.xbps
# Generate RSA keypair. xbps-rindex requires --privkey passed explicitly.
mkdir -p "$_NOC_HOME/.xbps-sign"
openssl genrsa -out "$_NOC_HOME/.xbps-sign/privkey.pem" 4096 2>/dev/null
[[ -s "$_NOC_HOME/.xbps-sign/privkey.pem" ]] \
|| { echo "ERROR: openssl genrsa failed"; exit 1; }
_NOC_PRIVKEY="$_NOC_HOME/.xbps-sign/privkey.pem"
# Sign repodata then each package (separate calls — xbps-rindex restriction).
xbps-rindex.static --sign --privkey "$_NOC_PRIVKEY" \
--signedby "noctalia-local" "$_NOC_LOCAL"
for _pkg in "$_NOC_LOCAL"/*.xbps; do
xbps-rindex.static --sign-pkg --privkey "$_NOC_PRIVKEY" \
--signedby "noctalia-local" "$_pkg"
done
# Register public key in mklive/keys/ so copy_void_keys installs it in the rootfs.
# xbps fingerprint = MD5 of DER-encoded public key, formatted as aa:bb:cc:...
openssl rsa -in "$_NOC_PRIVKEY" \
-pubout -outform DER -out "$_NOC_HOME/pubkey.der" 2>/dev/null
_FINGERPRINT=$(md5sum "$_NOC_HOME/pubkey.der" \
| cut -d' ' -f1 | sed 's/../&:/g; s/:$//')
# The plist <data> field = base64 of the PEM public key (headers included)
_PUBKEY_B64=$(openssl rsa -in "$_NOC_PRIVKEY" \
-pubout 2>/dev/null | base64 -w 0)
cat > "$MKLIVE_DIR/keys/$_FINGERPRINT.plist" <<KEOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>public-key</key>
<data>$_PUBKEY_B64</data>
<key>public-key-size</key>
<integer>4096</integer>
<key>signature-by</key>
<string>noctalia-local</string>
</dict>
</plist>
KEOF
echo " local signed noctalia repo ready — key: $_FINGERPRINT"
unset HOME
# ── end noctalia local repo ───────────────────────────────────────────────
_cleanup_mklive_builds() {
local d sub
for d in "$MKLIVE_DIR"/mklive-build.*/; do
[[ -d "$d" ]] || continue
for sub in tmp-rootfs/sys tmp-rootfs/proc tmp-rootfs/dev tmp-rootfs/run \
image/rootfs/sys image/rootfs/proc image/rootfs/dev image/rootfs/run; do
[[ -d "$d$sub" ]] && umount -R --lazy "$d$sub" 2>/dev/null || true
done
rm -rf "$d" 2>/dev/null || true
done
}
trap _cleanup_mklive_builds EXIT
# mklive prepends -r args: LAST -r = HIGHEST priority.
# Our local signed repo is last so xbps resolves noctalia-* from it.
./mklive.sh \
-a "$ARCH" \
-r "$REPO_URL" \
-r "${REPO_URL%/current}/current/nonfree" \
-r "$NOCTALIA_REPO" \
-r "$_NOC_LOCAL" \
-c "$CACHE_DIR/xbps-niri-pkgs" \
-H "$CACHE_DIR/xbps-host-pkgs" \
-k "$KEYMAP" \
-l "$LOCALE" \
-T "$ISO_TITLE" \
-p "$ISO_PKGS" \
-I "$INCLUDE_DIR" \
-C "${BOOT_CMDLINE:-}" \
-o "$OUT_ISO"
# Fix ownership so the host user can clean up without sudo.
# u+rwX: sets read+write on all, execute only on directories (capital X).
chmod -R u+rwX "$INCLUDE_DIR" 2>/dev/null || true
chown -R "${HOST_UID:-1000}:${HOST_GID:-1000}" "$INCLUDE_DIR" 2>/dev/null || true
chown -R "${HOST_UID:-1000}:${HOST_GID:-1000}" "$OUT_ISO" "${OUT_ISO}".* 2>/dev/null || true

View File

@@ -66,6 +66,8 @@ fi
# 3) build includes overlay # 3) build includes overlay
echo ">>> staging live includes overlay at $INCLUDE_DIR" echo ">>> staging live includes overlay at $INCLUDE_DIR"
# The nix store (staged by Docker/root) uses 444/555 permissions — chmod first.
chmod -R u+rwX "$INCLUDE_DIR" 2>/dev/null || true
rm -rf "$INCLUDE_DIR" rm -rf "$INCLUDE_DIR"
mkdir -p "$INCLUDE_DIR" mkdir -p "$INCLUDE_DIR"
@@ -408,7 +410,10 @@ picture-uri='file:///usr/share/backgrounds/void-installer/${WALLPAPER_FILE}'
picture-options='zoom' picture-options='zoom'
[org/gnome/desktop/input-sources] [org/gnome/desktop/input-sources]
sources=[('xkb', 'ch+fr')] sources=[('xkb', '${KEYMAP//-/+}')]
[org/gnome/desktop/interface]
color-scheme='prefer-dark'
[org/cinnamon/desktop/default-applications/terminal] [org/cinnamon/desktop/default-applications/terminal]
exec='alacritty' exec='alacritty'
@@ -530,8 +535,9 @@ gsettings set org.cinnamon.desktop.default-applications.terminal exec-arg '-e'
gsettings set org.gnome.desktop.default-applications.terminal exec '${DEFAULT_TERMINAL:-alacritty}' gsettings set org.gnome.desktop.default-applications.terminal exec '${DEFAULT_TERMINAL:-alacritty}'
gsettings set org.gnome.desktop.default-applications.terminal exec-arg '-e' gsettings set org.gnome.desktop.default-applications.terminal exec-arg '-e'
# Keyboard layout (Swiss French) # Keyboard layout — set explicitly via gsettings (belt-and-suspenders alongside
gsettings set org.gnome.desktop.input-sources sources "[('xkb', '${KEYMAP:-ch+fr_nodeadkeys}')]" # the user dconf db pre-baked in /etc/skel at build time).
gsettings set org.gnome.desktop.input-sources sources "[('xkb', '${KEYMAP//-/+}')]"
touch "\$DONE" touch "\$DONE"
EOF EOF

551
iso/build-niri-live-iso.sh Executable file
View File

@@ -0,0 +1,551 @@
#!/bin/bash
# Build a niri/Wayland LIVE desktop ISO (mainline-niri profile).
#
# Boots directly into a niri session with noctalia-shell as user 'live'
# (no password). agetty autologin on tty1 → .bash_profile → dbus-run-session niri.
# All themes, wallpapers, and the
# void-installer are pre-baked into the squashfs.
#
# Requires (host): bash, git, curl, docker, and Bibata-Modern-Ice cursor
# installed at /usr/share/icons/Bibata-Modern-Ice.
#
# Usage:
# iso/build-niri-live-iso.sh
# OUTPUT_ISO=/path/to/output.iso iso/build-niri-live-iso.sh
set -Eeuo pipefail
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CACHE_DIR="${CACHE_DIR:-$PROJECT_DIR/cache}"
OUT_DIR="${OUT_DIR:-$PROJECT_DIR/out}"
BUILD_DIR="${BUILD_DIR:-$PROJECT_DIR/build}"
INCLUDE_DIR="$BUILD_DIR/niri-live-includes"
MKLIVE_DIR="$CACHE_DIR/void-mklive-niri" # separate clone — avoids race with Cinnamon parallel build
MKLIVE_REPO="${MKLIVE_REPO:-https://github.com/void-linux/void-mklive.git}"
MKLIVE_REF="${MKLIVE_REF:-master}"
PATCH_DIR="$PROJECT_DIR/iso/patches"
DOCKER_IMAGE="${DOCKER_IMAGE:-void-installer-builder:latest}"
DOCKER="${DOCKER:-docker}"
LIVE_USER="${LIVE_USER:-live}"
# shellcheck disable=SC1091
source "$PROJECT_DIR/config/install.conf"
# Load niri profile settings (KERNEL_PKG, GTK_THEME, CURSOR_THEME, etc.)
# shellcheck disable=SC1091
source "$PROJECT_DIR/config/profiles/mainline-niri/profile.conf"
command -v "$DOCKER" >/dev/null \
|| { echo "ERROR: '$DOCKER' not in PATH"; exit 1; }
"$DOCKER" info >/dev/null 2>&1 \
|| { echo "ERROR: '$DOCKER' daemon unreachable"; exit 1; }
mkdir -p "$CACHE_DIR" "$OUT_DIR" "$BUILD_DIR"
# 1) clone + patch mklive (shared with Cinnamon build)
if [[ ! -d "$MKLIVE_DIR/.git" ]]; then
echo ">>> cloning void-mklive"
git clone --depth=1 --branch "$MKLIVE_REF" "$MKLIVE_REPO" "$MKLIVE_DIR"
fi
if compgen -G "$PATCH_DIR/*.patch" >/dev/null; then
echo ">>> resetting + applying iso/patches/"
( cd "$MKLIVE_DIR" && git checkout -- . )
for p in "$PATCH_DIR"/*.patch; do
echo " $(basename "$p")"
( cd "$MKLIVE_DIR" && patch -p1 --silent < "$p" )
done
fi
# 2) xbps-static (shared cache)
XBPS_STATIC_DIR="$CACHE_DIR/xbps-static"
if [[ ! -x "$XBPS_STATIC_DIR/usr/bin/xbps-install.static" ]]; then
echo ">>> downloading xbps-static"
mkdir -p "$XBPS_STATIC_DIR"
curl -fsSL "https://repo-default.voidlinux.org/static/xbps-static-latest.x86_64-musl.tar.xz" \
| tar xJf - -C "$XBPS_STATIC_DIR"
fi
# 3) build includes overlay (separate dir from Cinnamon build)
echo ">>> staging niri live includes overlay at $INCLUDE_DIR"
# The nix store (staged by Docker/root) uses 444/555 permissions — chmod first.
chmod -R u+rwX "$INCLUDE_DIR" 2>/dev/null || true
rm -rf "$INCLUDE_DIR"
mkdir -p "$INCLUDE_DIR"
install -d -m 0755 "$INCLUDE_DIR/etc"
# ── 3a) greetd config (fallback TUI on tty2) + agetty autologin on tty1 ─
# greetd's initial_session is not used: its PAM session setup fails silently
# in the live environment (pam_elogind ENOSYS, missing D-Bus session bus).
# Instead we autologin via agetty on tty1 and launch niri from .bash_profile.
install -d -m 0755 "$INCLUDE_DIR/etc/greetd"
cat > "$INCLUDE_DIR/etc/greetd/config.toml" <<'EOF'
[terminal]
vt = 2
[default_session]
command = "tuigreet --time --greeting 'Void Linux Live (niri)' --cmd 'niri --session'"
user = "_greeter"
EOF
# agetty-tty1 autologin: override conf so agetty-tty1 (mklive's default sv)
# automatically logs in the live user on tty1 without racing with a custom sv.
install -d -m 0755 "$INCLUDE_DIR/etc/sv/agetty-tty1"
cat > "$INCLUDE_DIR/etc/sv/agetty-tty1/conf" <<EOF
if [ -x /sbin/agetty -o -x /bin/agetty ]; then
if [ "\${tty}" = "tty1" ]; then
GETTY_ARGS="--noclear --autologin ${LIVE_USER}"
fi
fi
BAUD_RATE=38400
TERM_NAME=linux
EOF
# ── 3b) noctalia XBPS repo ──────────────────────────────────────────────
install -d -m 0755 "$INCLUDE_DIR/etc/xbps.d"
cat > "$INCLUDE_DIR/etc/xbps.d/10-noctalia.conf" <<'EOF'
repository=https://universalrepo.r1xelelo.workers.dev/void
EOF
# ── 3c) live-setup.sh (runs from runit/2 before any service starts) ────
install -d -m 0755 "$INCLUDE_DIR/etc/runit"
cat > "$INCLUDE_DIR/etc/runit/live-setup.sh" <<'SV_EOF'
#!/bin/sh
# Niri live session setup. Runs from /etc/runit/2 before services start.
LIVE_USER="${USERNAME:-live}"
[ -f /etc/default/live.conf ] && . /etc/default/live.conf
LIVE_USER="${LIVE_USER:-live}"
# Extra groups (dracut only adds audio,video,wheel)
for g in plugdev input network video audio _seatd; do
groupadd -f "$g" 2>/dev/null || true
usermod -aG "$g" "$LIVE_USER" 2>/dev/null || true
done
install -d -m 0755 /etc/sudoers.d
printf '%s ALL=(ALL) NOPASSWD: ALL\n' "$LIVE_USER" > /etc/sudoers.d/live
chmod 0440 /etc/sudoers.d/live
# nsswitch: remove mdns (library absent on Void; causes DNS lookup hangs)
if [ -f /etc/nsswitch.conf ]; then
sed -i '/^hosts:/s/mdns[^ ]* *//g' /etc/nsswitch.conf
fi
# Fallback DNS (QEMU's 10.0.2.3 is unreliable; NM will overwrite once DHCP settles)
if ! grep -q '^nameserver' /etc/resolv.conf 2>/dev/null || \
grep -q '^nameserver 10\.0\.2\.3' /etc/resolv.conf 2>/dev/null; then
printf 'nameserver 8.8.8.8\nnameserver 1.1.1.1\n' > /etc/resolv.conf
fi
# Ensure XDG_RUNTIME_DIR exists for the live user (greetd + elogind handle
# this normally, but set it here as a safety net).
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),
# then start elogind as a background daemon.
# NOTE: live-setup.sh runs BEFORE runsvdir, so dbus isn't up yet here.
# We start elogind from a one-shot at-startup hook instead — see runit/2.
echo "niri live-setup: done (user=$LIVE_USER)"
SV_EOF
chmod 0755 "$INCLUDE_DIR/etc/runit/live-setup.sh"
# runit/2: standard Void runit stage 2 — runs live-setup then hands off to runsvdir
cat > "$INCLUDE_DIR/etc/runit/2" <<'SV_EOF'
#!/bin/sh
PATH=/usr/bin:/usr/sbin
# Live session setup: groups, sudo, DNS
[ -x /etc/runit/live-setup.sh ] && /etc/runit/live-setup.sh
# Select runlevel from cmdline (default: default)
runlevel=default
for arg in $(cat /proc/cmdline); do
if [ -d /etc/runit/runsvdir/"$arg" ]; then
runlevel="$arg"
fi
done
[ -x /etc/rc.local ] && /etc/rc.local
runsvchdir "${runlevel}"
mkdir -p /run/runit/runsvdir
ln -sf /etc/runit/runsvdir/current /run/runit/runsvdir/current
exec env - PATH=$PATH \
runsvdir -P /run/runit/runsvdir/current \
'log: ...........................................................................................................................................................................................................................................................................................................................................................................................................'
SV_EOF
chmod 0755 "$INCLUDE_DIR/etc/runit/2"
# Enable services for the niri live session
install -d -m 0755 "$INCLUDE_DIR/etc/runit/runsvdir/default"
# Custom elogind sv: uses correct binary path and waits for dbus socket before
# starting (prevents "elogind is already running" spam from rapid runit restarts).
install -d -m 0755 "$INCLUDE_DIR/etc/sv/elogind"
cat > "$INCLUDE_DIR/etc/sv/elogind/run" <<'EOF'
#!/bin/sh
exec 2>&1
# Wait for dbus socket — poll every second, up to 30s
i=0
while [ $i -lt 30 ] && [ ! -S /run/dbus/system_bus_socket ]; do
sleep 1; i=$((i+1))
done
exec /usr/libexec/elogind/elogind.wrapper
EOF
chmod 0755 "$INCLUDE_DIR/etc/sv/elogind/run"
# finish: rate-limit restarts to avoid "already running" spam
cat > "$INCLUDE_DIR/etc/sv/elogind/finish" <<'EOF'
#!/bin/sh
sleep 3
EOF
chmod 0755 "$INCLUDE_DIR/etc/sv/elogind/finish"
for svc in dbus elogind NetworkManager sshd; do
ln -sf "/etc/sv/$svc" "$INCLUDE_DIR/etc/runit/runsvdir/default/$svc"
done
# ── 3d) Wayland environment ─────────────────────────────────────────────
install -d -m 0755 "$INCLUDE_DIR/etc/profile.d"
cat > "$INCLUDE_DIR/etc/profile.d/wayland.sh" <<'EOF'
# Wayland defaults (mainline-niri live session)
export QT_QPA_PLATFORM="wayland;xcb"
export GDK_BACKEND="wayland,x11"
export MOZ_ENABLE_WAYLAND=1
export _JAVA_AWT_WM_NONREPARENTING=1
export XDG_CURRENT_DESKTOP=niri
export XDG_SESSION_TYPE=wayland
# Use elogind's logind backend — works correctly on Void/runit via audit session IDs
export LIBSEAT_BACKEND=logind
EOF
chmod 0644 "$INCLUDE_DIR/etc/profile.d/wayland.sh"
# Nix profile.d: adds ~/.nix-profile/bin to PATH for interactive shells
cat > "$INCLUDE_DIR/etc/profile.d/nix-prebaked.sh" <<'EOF'
# Pre-baked nix profile — expose nix package binaries
if [[ -d "${HOME:-}/.nix-profile/bin" ]]; then
case ":$PATH:" in
*":$HOME/.nix-profile/bin:"*) ;;
*) export PATH="$HOME/.nix-profile/bin:$PATH" ;;
esac
fi
EOF
chmod 0644 "$INCLUDE_DIR/etc/profile.d/nix-prebaked.sh"
# Nix daemon config (trusted live user so nix commands work without root)
install -d -m 0755 "$INCLUDE_DIR/etc/nix"
cat > "$INCLUDE_DIR/etc/nix/nix.conf" <<EOF
experimental-features = nix-command flakes
sandbox = false
auto-optimise-store = true
trusted-users = root ${LIVE_USER}
max-jobs = 2
http-connections = 10
EOF
# ── 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.
# Pre-bake SSH authorized_keys from host so passwordless SSH just works in QEMU tests.
if _HOST_PUBKEY=$(ssh-add -L 2>/dev/null | head -1) && [ -n "$_HOST_PUBKEY" ]; then
install -d -m 0700 "$INCLUDE_DIR/etc/skel/.ssh"
echo "$_HOST_PUBKEY" > "$INCLUDE_DIR/etc/skel/.ssh/authorized_keys"
chmod 0600 "$INCLUDE_DIR/etc/skel/.ssh/authorized_keys"
fi
KEYMAP_XKB_LAYOUT="${KEYMAP%%-*}" # ch-fr_nodeadkeys → ch
KEYMAP_XKB_VARIANT="${KEYMAP#*-}" # ch-fr_nodeadkeys → fr_nodeadkeys
KEYMAP_XKB_VARIANT="${KEYMAP_XKB_VARIANT//_nodeadkeys/}" # strip trailing _nodeadkeys
install -d -m 0755 "$INCLUDE_DIR/etc/skel/.config/niri"
cat > "$INCLUDE_DIR/etc/skel/.config/niri/config.kdl" <<EOF
// niri config — generated by void-installer (mainline-niri live session).
input {
keyboard {
xkb {
layout "${KEYMAP_XKB_LAYOUT}"
variant "${KEYMAP_XKB_VARIANT}"
}
}
touchpad {
tap
natural-scroll
dwt
}
mouse {
accel-speed 0.0
}
}
layout {
gaps 12
center-focused-column "never"
preset-column-widths {
proportion 0.33333
proportion 0.5
proportion 0.66667
}
default-column-width { proportion 0.5; }
focus-ring {
width 2
active-color "#fabd2f"
inactive-color "#3c3836"
}
border { off; }
}
prefer-no-csd
cursor {
xcursor-theme "${CURSOR_THEME:-Bibata-Modern-Ice}"
xcursor-size 24
}
// Audio / screen session services — started by niri as the live user
spawn-at-startup "pipewire"
spawn-at-startup "pipewire-pulse"
spawn-at-startup "wireplumber"
// Background, notifications, network, bluetooth, 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"
binds {
Mod+T { spawn "alacritty"; }
Mod+D { spawn "fuzzel"; }
Mod+Q { close-window; }
Mod+Shift+E { quit; }
Print { screenshot; }
Mod+H { focus-column-left; }
Mod+L { focus-column-right; }
Mod+J { focus-window-down; }
Mod+K { focus-window-up; }
Mod+Shift+H { move-column-left; }
Mod+Shift+L { move-column-right; }
Mod+1 { focus-workspace 1; }
Mod+2 { focus-workspace 2; }
Mod+3 { focus-workspace 3; }
Mod+4 { focus-workspace 4; }
Mod+Shift+1 { move-column-to-workspace 1; }
Mod+Shift+2 { move-column-to-workspace 2; }
XF86AudioRaiseVolume { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "5%+"; }
XF86AudioLowerVolume { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "5%-"; }
XF86AudioMute { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SINK@" "toggle"; }
XF86MonBrightnessUp { spawn "brightnessctl" "set" "+5%"; }
XF86MonBrightnessDown { spawn "brightnessctl" "set" "5%-"; }
}
EOF
# ── 3f) Themes / icons / wallpapers overlay ─────────────────────────────
echo ">>> staging niri customizations overlay"
OVERLAY="$INCLUDE_DIR/etc/installer-overlay"
install -d -m 0755 "$OVERLAY" "$OVERLAY/wallpapers" \
"$OVERLAY/themes" "$OVERLAY/icons"
# Wallpapers
WP_SRC="${WALLPAPERS_SRC:-$HOME/Scaricati}"
shopt -s nullglob
for f in "$WP_SRC"/pxfuel*.jpg; do
install -m 0644 "$f" "$OVERLAY/wallpapers/$(basename "$f")"
done
shopt -u nullglob
echo " wallpapers: $(ls "$OVERLAY/wallpapers" 2>/dev/null | wc -l) file(s)"
# Gruvbox GTK theme (for GTK apps running under niri)
THEME_CACHE="$CACHE_DIR/gruvbox-gtk-theme"
THEME_BUILD="$CACHE_DIR/gruvbox-gtk-built"
if [[ ! -d "$THEME_CACHE/.git" ]]; then
git clone --depth=1 https://github.com/Fausto-Korpsvart/Gruvbox-GTK-Theme.git "$THEME_CACHE" || true
fi
if [[ -x "$THEME_CACHE/themes/install.sh" && ! -d "$THEME_BUILD" ]]; then
echo " building gruvbox themes"
install -d -m 0755 "$THEME_BUILD"
"$DOCKER" run --rm \
-v "$THEME_CACHE":/src \
-v "$THEME_BUILD":/out \
debian:stable-slim sh -c '
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq >/dev/null
apt-get install -y --no-install-recommends sassc bash >/dev/null
cd /src/themes && bash install.sh -d /out -t default -c dark -s standard
' || true
fi
if [[ -d "$THEME_BUILD" ]]; then
for d in "$THEME_BUILD"/Gruvbox-Dark*; do
[[ -d "$d" ]] && cp -a "$d" "$OVERLAY/themes/$(basename "$d")"
done
echo " themes: $(ls "$OVERLAY/themes" 2>/dev/null | wc -l) variant(s)"
fi
# Gruvbox Plus icons
ICON_CACHE="$CACHE_DIR/gruvbox-plus-icons"
if [[ ! -d "$ICON_CACHE/.git" ]]; then
git clone --depth=1 https://github.com/SylEleuth/gruvbox-plus-icon-pack.git "$ICON_CACHE" || true
fi
if [[ -d "$ICON_CACHE/Gruvbox-Plus-Dark" ]]; then
cp -a "$ICON_CACHE/Gruvbox-Plus-Dark" "$OVERLAY/icons/Gruvbox-Plus-Dark"
echo " icons: Gruvbox-Plus-Dark"
fi
# Bibata cursor
BIBATA_SRC="${BIBATA_SRC:-/usr/share/icons/Bibata-Modern-Ice}"
if [[ -d "$BIBATA_SRC" ]]; then
cp -a "$BIBATA_SRC" "$OVERLAY/icons/Bibata-Modern-Ice"
echo " cursor: Bibata-Modern-Ice"
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
install -d -m 0755 "$INCLUDE_DIR/usr/share/themes"
[[ -d "$OVERLAY/themes" ]] && cp -a "$OVERLAY/themes"/. "$INCLUDE_DIR/usr/share/themes/" 2>/dev/null || true
install -d -m 0755 "$INCLUDE_DIR/usr/share/icons"
[[ -d "$OVERLAY/icons" ]] && cp -a "$OVERLAY/icons"/. "$INCLUDE_DIR/usr/share/icons/" 2>/dev/null || true
# ── 3g) GTK settings for Wayland apps ───────────────────────────────────
# Write GTK2/3/4 settings to skel so the live user picks them up.
install -d -m 0755 "$INCLUDE_DIR/etc/skel/.config/gtk-3.0"
install -d -m 0755 "$INCLUDE_DIR/etc/skel/.config/gtk-4.0"
cat > "$INCLUDE_DIR/etc/skel/.config/gtk-3.0/settings.ini" <<EOF
[Settings]
gtk-theme-name=${GTK_THEME}
gtk-icon-theme-name=${ICON_THEME}
gtk-cursor-theme-name=${CURSOR_THEME:-Bibata-Modern-Ice}
gtk-cursor-theme-size=24
gtk-font-name=Noto Sans 11
EOF
cp "$INCLUDE_DIR/etc/skel/.config/gtk-3.0/settings.ini" \
"$INCLUDE_DIR/etc/skel/.config/gtk-4.0/settings.ini"
install -d -m 0755 "$INCLUDE_DIR/etc/skel"
cat > "$INCLUDE_DIR/etc/skel/.gtkrc-2.0" <<EOF
gtk-theme-name="${GTK_THEME}"
gtk-icon-theme-name="${ICON_THEME}"
gtk-cursor-theme-name="${CURSOR_THEME:-Bibata-Modern-Ice}"
gtk-cursor-theme-size=24
gtk-font-name="Noto Sans 11"
EOF
# XDG_DATA_DIRS so Wayland apps pick up installed theme/icon .desktop files
cat > "$INCLUDE_DIR/etc/environment" <<'ENVEOF'
XDG_DATA_DIRS=/usr/local/share:/usr/share
QT_QPA_PLATFORM=wayland;xcb
GDK_BACKEND=wayland,x11
MOZ_ENABLE_WAYLAND=1
LIBSEAT_BACKEND=logind
ENVEOF
# ── 3h) .bash_profile: source .bashrc + launch niri on tty1 ───────────
# agetty autologin on tty1 runs a login shell; .bash_profile execs niri
# via dbus-run-session so it gets a D-Bus session bus.
cat > "$INCLUDE_DIR/etc/skel/.bash_profile" <<'EOF'
[[ -f ~/.bashrc ]] && . ~/.bashrc
if [[ "$(tty)" == /dev/tty1 ]] && [[ -z "$WAYLAND_DISPLAY" ]] && [[ -z "$NIRI_SOCKET" ]]; then
exec dbus-run-session -- niri --session
fi
EOF
# ── 3i) Void Linux installer baked in ───────────────────────────────────
install -d -m 0755 "$INCLUDE_DIR/usr/local/lib/void-installer/lib"
install -d -m 0755 "$INCLUDE_DIR/usr/local/share/installer"
install -m 0755 "$PROJECT_DIR/installer/install.sh" \
"$INCLUDE_DIR/usr/local/lib/void-installer/install.sh"
for f in "$PROJECT_DIR/installer/lib/"*.sh; do
install -m 0755 "$f" \
"$INCLUDE_DIR/usr/local/lib/void-installer/lib/$(basename "$f")"
done
install -m 0644 "$PROJECT_DIR/config/install.conf" \
"$INCLUDE_DIR/usr/local/share/installer/install.conf"
install -m 0644 "$PROJECT_DIR/config/profiles/mainline-niri/packages.list" \
"$INCLUDE_DIR/usr/local/share/installer/packages.list"
if [[ -d "$PROJECT_DIR/config/profiles" ]]; then
cp -r "$PROJECT_DIR/config/profiles" \
"$INCLUDE_DIR/usr/local/share/installer/profiles"
fi
install -d -m 0755 "$INCLUDE_DIR/usr/local/bin"
cat > "$INCLUDE_DIR/usr/local/bin/void-install" <<'WRAPEOF'
#!/bin/sh
exec /usr/local/lib/void-installer/install.sh "$@"
WRAPEOF
chmod 0755 "$INCLUDE_DIR/usr/local/bin/void-install"
_SECRETS_SRC="${SECRETS_ENV:-$PROJECT_DIR/secrets.env}"
if [[ -r "$_SECRETS_SRC" ]]; then
install -d -m 0755 "$INCLUDE_DIR/etc"
install -m 0600 "$_SECRETS_SRC" "$INCLUDE_DIR/etc/installer-secrets.env"
echo " baked installer secrets from $_SECRETS_SRC"
else
echo " WARNING: no secrets.env found — installer will prompt at runtime"
fi
install -d -m 0755 "$INCLUDE_DIR/usr/share/applications"
cat > "$INCLUDE_DIR/usr/share/applications/void-installer.desktop" <<'DESKEOF'
[Desktop Entry]
Version=1.0
Type=Application
Name=Install Void Linux
Comment=Install Void Linux to this machine
Exec=alacritty --title "Void Linux Installer" -e sudo /usr/local/bin/void-install
Icon=system-software-install
Terminal=false
Categories=System;
StartupNotify=true
DESKEOF
# 4) build Docker image (reuse the same image as Cinnamon build)
echo ">>> building docker image $DOCKER_IMAGE"
if "$DOCKER" buildx version >/dev/null 2>&1; then
"$DOCKER" build -t "$DOCKER_IMAGE" "$PROJECT_DIR/iso"
else
DOCKER_BUILDKIT=0 "$DOCKER" build -t "$DOCKER_IMAGE" "$PROJECT_DIR/iso"
fi
# 5) packages + output filename
ISO_PKGS=$(grep -vE '^\s*(#|$)' \
"$PROJECT_DIR/config/profiles/mainline-niri/packages.live-desktop.list" \
| tr '\n' ' ')
TS="$(date -u +%Y%m%d)"
OUT_ISO="${OUTPUT_ISO:-$OUT_DIR/void-live-niri-${TS}.iso}"
BOOT_CMDLINE="${BOOT_CMDLINE:-live.user=${LIVE_USER} console=tty0 console=ttyS0,115200}"
echo ">>> running mklive.sh inside docker — output: $OUT_ISO"
"$DOCKER" run --rm --privileged \
-v "$PROJECT_DIR:/work:rw" \
-v "$CACHE_DIR:/cache:rw" \
-e ARCH="${ARCH:-x86_64}" \
-e REPO_URL="${REPO_URL:-https://repo-default.voidlinux.org/current}" \
-e KEYMAP="${KEYMAP:-us}" \
-e LOCALE="${LOCALE:-en_US.UTF-8}" \
-e ISO_PKGS="$ISO_PKGS" \
-e ISO_TITLE="Void Live (niri / noctalia-shell)" \
-e OUT_ISO_REL="${OUT_ISO#$PROJECT_DIR/}" \
-e BOOT_CMDLINE="${BOOT_CMDLINE:-}" \
-e INCLUDE_DIR_REL="${INCLUDE_DIR#$PROJECT_DIR/}" \
-e NIX_PACKAGES_PREBAKE="${NIX_USER_PACKAGES[*]}" \
-e HOST_UID="$(id -u)" \
-e HOST_GID="$(id -g)" \
"$DOCKER_IMAGE" \
bash /work/iso/_inner-build-niri-live.sh
echo
echo ">>> Niri live ISO built: $OUT_ISO"
sha256sum "$OUT_ISO" | tee "${OUT_ISO}.sha256" || true

39
tests/debug-qemu.sh Executable file
View File

@@ -0,0 +1,39 @@
#!/bin/bash
# Like interactive-qemu.sh but logs serial console to /tmp/qemu-serial.log
# for post-boot log inspection. Uses the niri ISO by default.
set -Eeuo pipefail
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
OUT_DIR="$PROJECT_DIR/out"
ISO="${ISO:-$(ls -t "$OUT_DIR"/void-live-niri-*.iso 2>/dev/null | head -1 || true)}"
DISK="${DISK:-$OUT_DIR/test-disk.img}"
RAM_MB="${RAM_MB:-6144}"
SMP="${SMP:-4}"
OVMF_CODE="${OVMF_CODE:-/usr/share/OVMF/OVMF_CODE.fd}"
OVMF_VARS_TPL="${OVMF_VARS_TPL:-/usr/share/OVMF/OVMF_VARS.fd}"
[[ -r "$OVMF_CODE" ]] || { echo "no OVMF — install ovmf"; exit 1; }
[[ -r "$ISO" ]] || { echo "no ISO at $ISO"; exit 1; }
if [[ ! -f "$DISK" ]]; then
"$PROJECT_DIR/tests/make-test-disk.sh" "$DISK"
fi
VARS="$OUT_DIR/OVMF_VARS.debug.fd"
[[ -f "$VARS" ]] || cp "$OVMF_VARS_TPL" "$VARS"
SERIAL_PORT="${SERIAL_PORT:-4444}"
echo ">>> serial console → TCP port $SERIAL_PORT (connect: socat -,raw,echo=0 TCP:127.0.0.1:$SERIAL_PORT)"
echo ">>> ISO: $ISO"
exec qemu-system-x86_64 \
-name void-niri-debug \
-machine q35,accel=kvm:tcg -cpu max \
-m "$RAM_MB" -smp "$SMP" \
-drive "if=pflash,format=raw,readonly=on,file=$OVMF_CODE" \
-drive "if=pflash,format=raw,file=$VARS" \
-drive "if=virtio,file=$DISK,format=raw,cache=none" \
-cdrom "$ISO" \
-boot menu=on \
-netdev user,id=n0,hostfwd=tcp:127.0.0.1:2222-:22 \
-device virtio-net-pci,netdev=n0 \
-serial "tcp::${SERIAL_PORT},server,nowait" \
-device virtio-gpu-gl -display gtk,gl=on