feat: initial commit — void-installer multi-profile (stable-cinnamon + mainline-niri)
This commit is contained in:
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# ── Secrets — NEVER commit ────────────────────────────────────────────
|
||||||
|
secrets.env
|
||||||
|
secrets.env.local
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
id_rsa*
|
||||||
|
id_ed25519*
|
||||||
|
id_ecdsa*
|
||||||
|
id_github*
|
||||||
|
id_gitea*
|
||||||
|
id_ovh*
|
||||||
|
authorized_keys
|
||||||
|
|
||||||
|
# ── Generated build staging (build-iso.sh populates this at build time) ─
|
||||||
|
build/includes/
|
||||||
|
build/first-login.sh
|
||||||
|
|
||||||
|
# ── Build artifacts ────────────────────────────────────────────────────
|
||||||
|
out/
|
||||||
|
*.iso
|
||||||
|
*.iso.sha256
|
||||||
|
*.qcow2
|
||||||
|
*.img
|
||||||
|
nohup.out
|
||||||
|
|
||||||
|
# ── Cached downloads / XBPS package cache ─────────────────────────────
|
||||||
|
cache/
|
||||||
|
|
||||||
|
# ── Editor ─────────────────────────────────────────────────────────────
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
.DS_Store
|
||||||
58
Makefile
Normal file
58
Makefile
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# Void Installer — XPS 17 (xps9700)
|
||||||
|
#
|
||||||
|
# Targets:
|
||||||
|
# make iso build the auto-installing ISO (uses docker)
|
||||||
|
# make test-disk create a fresh QEMU test disk that mimics XPS 17 layout
|
||||||
|
# make test full automated QEMU smoke test
|
||||||
|
# make test-iso rebuild only the TEST ISO variant
|
||||||
|
# make qemu launch QEMU interactively with the latest ISO
|
||||||
|
# make shellcheck lint all installer/build shell scripts
|
||||||
|
# make clean remove build/, out/ (cache stays)
|
||||||
|
# make distclean also remove cache/
|
||||||
|
|
||||||
|
PROJECT_DIR := $(CURDIR)
|
||||||
|
OUT := $(PROJECT_DIR)/out
|
||||||
|
SECRETS := $(PROJECT_DIR)/secrets.env
|
||||||
|
|
||||||
|
.PHONY: all iso test test-disk test-iso qemu shellcheck clean distclean check-secrets check-docker
|
||||||
|
|
||||||
|
all: iso
|
||||||
|
|
||||||
|
check-secrets:
|
||||||
|
@test -r $(SECRETS) || { echo "missing $(SECRETS) — copy from template"; exit 1; }
|
||||||
|
|
||||||
|
check-docker:
|
||||||
|
@command -v docker >/dev/null || { echo "ERROR: docker not installed"; exit 1; }
|
||||||
|
@docker info >/dev/null 2>&1 || { echo "ERROR: docker daemon unreachable (in 'docker' group? systemctl start docker?)"; exit 1; }
|
||||||
|
|
||||||
|
iso: check-secrets check-docker
|
||||||
|
$(PROJECT_DIR)/iso/build-iso.sh
|
||||||
|
|
||||||
|
test-iso: check-secrets check-docker
|
||||||
|
REBUILD_ISO=1 $(PROJECT_DIR)/tests/run-qemu-test.sh
|
||||||
|
|
||||||
|
test-disk:
|
||||||
|
$(PROJECT_DIR)/tests/make-test-disk.sh $(OUT)/test-disk.img
|
||||||
|
|
||||||
|
test: check-secrets check-docker
|
||||||
|
@mkdir -p $(OUT)
|
||||||
|
$(PROJECT_DIR)/tests/run-qemu-test.sh
|
||||||
|
|
||||||
|
qemu:
|
||||||
|
$(PROJECT_DIR)/tests/interactive-qemu.sh
|
||||||
|
|
||||||
|
shellcheck:
|
||||||
|
@command -v shellcheck >/dev/null || { echo "shellcheck not installed"; exit 1; }
|
||||||
|
shellcheck -x \
|
||||||
|
$(PROJECT_DIR)/installer/install.sh \
|
||||||
|
$(PROJECT_DIR)/installer/lib/*.sh \
|
||||||
|
$(PROJECT_DIR)/iso/build-iso.sh \
|
||||||
|
$(PROJECT_DIR)/iso/_inner-build.sh \
|
||||||
|
$(PROJECT_DIR)/tests/*.sh \
|
||||||
|
$(PROJECT_DIR)/tests/lib/*.sh
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf $(PROJECT_DIR)/build $(OUT)
|
||||||
|
|
||||||
|
distclean: clean
|
||||||
|
rm -rf $(PROJECT_DIR)/cache
|
||||||
195
README.md
Normal file
195
README.md
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
# void-installer — XPS 17 (xps9700)
|
||||||
|
|
||||||
|
Auto-installing Void Linux ISO targeted at a Dell XPS 17 9700 dual-booting Windows.
|
||||||
|
Final installed system ships with: **Cinnamon**, **Docker**, **VS Code**, your
|
||||||
|
SSH config, NVIDIA PRIME render-offload, and Nix for Spotify/Discord/LocalSend.
|
||||||
|
|
||||||
|
> ⚠️ **Destructive.** This installer wipes one partition. Read the
|
||||||
|
> [Safety](#safety) section before running on real hardware.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Layout produced
|
||||||
|
|
||||||
|
| What | Where |
|
||||||
|
|------------------------------|--------------------------------------|
|
||||||
|
| Void root (btrfs `@`) | `/dev/nvme0n1p5` (replaces Mint) |
|
||||||
|
| `/home`, `/.snapshots`, etc. | btrfs subvolumes on the same device |
|
||||||
|
| EFI System Partition | `/dev/nvme0n1p1` *(shared, untouched)* |
|
||||||
|
| GRUB target | `\EFI\Void\` (Windows entry preserved) |
|
||||||
|
| Windows | `/dev/nvme0n1p3` *(left alone)* |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Repo layout
|
||||||
|
|
||||||
|
```
|
||||||
|
config/install.conf # all knobs (hostname, locale, kbd, GPU, etc.)
|
||||||
|
config/packages.target.list # xbps packages installed into target
|
||||||
|
config/packages.live.list # extra packages added to the LIVE iso
|
||||||
|
secrets.env # USER_PASSWORD / ROOT_PASSWORD (gitignored)
|
||||||
|
installer/
|
||||||
|
install.sh # main entrypoint, runs in the live env
|
||||||
|
lib/ # tui, partition, bootstrap, grub, postinstall
|
||||||
|
iso/
|
||||||
|
build-iso.sh # host-side: stages overlay, then runs mklive in docker
|
||||||
|
_inner-build.sh # invoked inside the docker container as root
|
||||||
|
Dockerfile # debian:stable-slim + mtools/xorriso/squashfs-tools
|
||||||
|
patches/ # patches applied to upstream void-mklive
|
||||||
|
tests/
|
||||||
|
make-test-disk.sh # builds a qcow2 mimicking the XPS 17 partition table
|
||||||
|
run-qemu-test.sh # automated headless install + smoke tests
|
||||||
|
interactive-qemu.sh # GUI QEMU for manual exploration
|
||||||
|
lib/make-test-overlay.sh
|
||||||
|
Makefile # build / test entrypoints
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
### Build dependencies (host)
|
||||||
|
|
||||||
|
Any Linux with **bash, git, curl, docker** (and `qemu` + `ovmf` if you want to run
|
||||||
|
`make test` / `make qemu`). The mklive build runs inside a Debian container, so
|
||||||
|
you don't need `mtools`, `xorriso`, `squashfs-tools`, etc. on the host.
|
||||||
|
|
||||||
|
- **Void**: `xbps-install -S git curl docker qemu ovmf`
|
||||||
|
- **Debian/Ubuntu/Mint**: `apt install git curl docker.io qemu-system-x86 qemu-utils ovmf`
|
||||||
|
- **Arch**: `pacman -S git curl docker qemu-full edk2-ovmf`
|
||||||
|
|
||||||
|
Make sure your user is in the `docker` group (`sudo usermod -aG docker $USER`,
|
||||||
|
then log out / back in) so the build runs without `sudo`.
|
||||||
|
|
||||||
|
### 1. Provide secrets
|
||||||
|
|
||||||
|
`secrets.env` is gitignored and pre-populated for you with the values from the
|
||||||
|
chat. Edit if you want different passwords.
|
||||||
|
|
||||||
|
### 2. Build the ISO
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make iso
|
||||||
|
# -> out/void-install-xps9700-YYYYMMDD.iso
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Test in QEMU (recommended before flashing!)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make test # full headless install + smoke tests (~15-30 min)
|
||||||
|
# or
|
||||||
|
make qemu # interactive QEMU window with the ISO booted
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Flash to USB and boot the XPS 17
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo dd if=out/void-install-xps9700-*.iso of=/dev/sdX bs=4M status=progress conv=fsync
|
||||||
|
```
|
||||||
|
|
||||||
|
Boot the XPS from the USB (F12 boot menu). The TUI shows detected partitions
|
||||||
|
with `[WINDOWS]` and `[EFI]` markers. Pick `/dev/nvme0n1p5`, type the device
|
||||||
|
path verbatim to confirm, and let it run.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Safety
|
||||||
|
|
||||||
|
- The installer **refuses** to wipe any NTFS partition.
|
||||||
|
- A TUI confirmation step requires you to **type the full device path** before
|
||||||
|
any destructive action.
|
||||||
|
- The shared EFI partition is **mounted read-write but never reformatted**;
|
||||||
|
Windows boot files under `EFI/Microsoft/` are preserved.
|
||||||
|
- All passwords come from `/etc/installer-secrets.env` baked into the ISO at
|
||||||
|
build time (mode `0600`). They are not on the network and not in `argv`.
|
||||||
|
|
||||||
|
If you want to be paranoid, run `make test` first — the smoke test asserts
|
||||||
|
that `EFI/Microsoft/Boot/bootmgfw.efi` survives the install on a simulated
|
||||||
|
disk with the same layout as the XPS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Everything tunable lives in [config/install.conf](config/install.conf):
|
||||||
|
|
||||||
|
- locale `en_US.UTF-8`, keymap `ch-fr_nodeadkeys`, timezone `Europe/Zurich`
|
||||||
|
- hostname `xps9700`, user `moze` in `wheel,docker,video,audio,...`
|
||||||
|
- btrfs subvolumes `@`, `@home`, `@snapshots`, `@var_log`, `@var_cache`
|
||||||
|
- GPU mode `prime-offload` (Intel UHD primary, NVIDIA GTX 1650 Ti via `prime-run`)
|
||||||
|
- zram swap at 50% RAM (zramen)
|
||||||
|
- nonfree + multilib + multilib-nonfree repos enabled
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What's installed
|
||||||
|
|
||||||
|
**xbps:** base-system, linux, intel-ucode, NetworkManager, cinnamon, lightdm,
|
||||||
|
docker, docker-compose, vscode (Microsoft), firefox, vlc, obs, flameshot,
|
||||||
|
nvidia + nvidia-libs-32bit, pipewire, sudo, git, curl, vim, tlp, nix, …
|
||||||
|
(full list in [config/packages.target.list](config/packages.target.list))
|
||||||
|
|
||||||
|
**nix profile (first boot, as moze):** spotify, discord, localsend.
|
||||||
|
The first-boot service runs once after `nix-daemon` is up, then exits.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Post-install
|
||||||
|
|
||||||
|
After first reboot:
|
||||||
|
|
||||||
|
1. Log in as `moze` (password: `void`).
|
||||||
|
2. The Cinnamon greeter appears (LightDM).
|
||||||
|
3. The first-boot Nix service installs Spotify/Discord/LocalSend in the
|
||||||
|
background (~5–15 min depending on connection). Watch with
|
||||||
|
`journalctl -fu first-boot-nix` is replaced by `tail -f /var/log/socklog/everything/current` on Void's runit; or just check `nix profile list` later.
|
||||||
|
4. Run NVIDIA-accelerated apps with `prime-run <app>`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test harness
|
||||||
|
|
||||||
|
`tests/run-qemu-test.sh` runs end-to-end:
|
||||||
|
|
||||||
|
1. Builds a TEST ISO variant (overlay forces `UNATTENDED=1 TEST_MODE=1` and
|
||||||
|
bakes a one-off ssh keypair for the harness).
|
||||||
|
2. Builds a fresh `out/test-disk.qcow2` with the **same partition layout**
|
||||||
|
as the XPS (EFI + MSR + NTFS "Windows" placeholder + btrfs "Mint").
|
||||||
|
3. Boots the ISO under QEMU/OVMF; installer runs unattended; VM powers off.
|
||||||
|
4. Boots the installed disk; ssh's in as `moze`; runs ~20 smoke assertions.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make test # default
|
||||||
|
TIMEOUT_INSTALL=5400 make test # slower hosts
|
||||||
|
ACCEL=tcg make test # no KVM (e.g., nested virt without)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files of interest while debugging
|
||||||
|
|
||||||
|
| File on installed system | Purpose |
|
||||||
|
|------------------------------------|------------------------------------------|
|
||||||
|
| `/var/log/void-installer.log` | full installer log |
|
||||||
|
| `/etc/sv/first-boot-nix/run` | one-shot nix profile installer |
|
||||||
|
| `/usr/local/bin/prime-run` | NVIDIA PRIME offload wrapper |
|
||||||
|
| `/etc/X11/xorg.conf.d/20-nvidia.conf` | NVIDIA display config |
|
||||||
|
| `/etc/runit/runsvdir/default/` | enabled services |
|
||||||
|
|
||||||
|
| File on the live ISO | Purpose |
|
||||||
|
|------------------------------------|------------------------------------------|
|
||||||
|
| `/usr/local/share/installer/` | installer scripts + config + packages |
|
||||||
|
| `/etc/installer-secrets.env` | passwords (mode 0600) |
|
||||||
|
| `/etc/installer-ssh/` | snapshot of build-host `~/.ssh/` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Caveats
|
||||||
|
|
||||||
|
- 150 MB shared EFI is tight (was 91 % full before install). Void puts only
|
||||||
|
`\EFI\Void\grubx64.efi` (~150 KB) there; kernels live on btrfs `/boot`.
|
||||||
|
- Secure Boot is **off** (per the input). NVIDIA proprietary modules won't
|
||||||
|
load with SB on without manual MOK enrollment.
|
||||||
|
- The installer assumes `/dev/nvme0n1p5` is currently Linux Mint (btrfs).
|
||||||
|
If that ever changes, the TUI default will be wrong but the listing is
|
||||||
|
always live, and the confirmation step prevents accidents.
|
||||||
91
config/install.conf
Normal file
91
config/install.conf
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# Void installer configuration for the XPS 17 (xps9700)
|
||||||
|
# Sourced by installer/install.sh inside the live environment.
|
||||||
|
# All passwords come from /etc/installer-secrets.env (baked at ISO build).
|
||||||
|
|
||||||
|
# ---------- Identity ----------
|
||||||
|
HOSTNAME="xps9700"
|
||||||
|
USERNAME="moze"
|
||||||
|
USER_FULLNAME="moze"
|
||||||
|
USER_UID="1000"
|
||||||
|
USER_GROUPS="wheel,docker,video,audio,input,plugdev,network,kvm,users"
|
||||||
|
DEFAULT_SHELL="/bin/bash"
|
||||||
|
|
||||||
|
# ---------- Locale ----------
|
||||||
|
LOCALE="en_US.UTF-8"
|
||||||
|
LANG="en_US.UTF-8"
|
||||||
|
KEYMAP="ch-fr_nodeadkeys" # Swiss French keyboard
|
||||||
|
TIMEZONE="Europe/Zurich"
|
||||||
|
HARDWARECLOCK="UTC"
|
||||||
|
|
||||||
|
# ---------- Repository ----------
|
||||||
|
REPO_URL="https://repo-default.voidlinux.org/current"
|
||||||
|
ARCH="x86_64" # glibc
|
||||||
|
EXTRA_REPOS=(nonfree multilib multilib/nonfree)
|
||||||
|
|
||||||
|
# During install, packages can be fetched via a local caching proxy (set by
|
||||||
|
# the test harness to http://10.0.2.2:3142/current). Empty = use REPO_URL.
|
||||||
|
INSTALL_REPO_URL=""
|
||||||
|
|
||||||
|
# ---------- Disk layout ----------
|
||||||
|
# Defaults match the detected XPS 17 layout. The TUI overrides these
|
||||||
|
# after explicit user confirmation.
|
||||||
|
DEFAULT_DISK="/dev/nvme0n1"
|
||||||
|
DEFAULT_ROOT_PART="/dev/nvme0n1p5" # Linux Mint -> Void
|
||||||
|
DEFAULT_EFI_PART="/dev/nvme0n1p1" # SHARED with Windows; never reformatted
|
||||||
|
DEFAULT_FS="btrfs"
|
||||||
|
# Btrfs subvolume layout. Each entry: "<subvol-name>:<mountpoint>"
|
||||||
|
# ("@" is the root subvolume; mountpoint is relative to the install root).
|
||||||
|
BTRFS_SUBVOLS=(
|
||||||
|
"@:/"
|
||||||
|
"@home:/home"
|
||||||
|
"@snapshots:/.snapshots"
|
||||||
|
"@var_log:/var/log"
|
||||||
|
"@var_cache:/var/cache"
|
||||||
|
)
|
||||||
|
BTRFS_MOUNT_OPTS="rw,noatime,ssd,compress=zstd:3,space_cache=v2,discard=async"
|
||||||
|
EFI_MOUNTPOINT="/boot/efi"
|
||||||
|
|
||||||
|
# ---------- Boot ----------
|
||||||
|
BOOTLOADER="grub"
|
||||||
|
BOOTLOADER_ID="Void"
|
||||||
|
ENABLE_OS_PROBER="yes" # detect Windows on /dev/nvme0n1p3
|
||||||
|
|
||||||
|
# ---------- Hardware ----------
|
||||||
|
CPU_VENDOR="intel" # microcode -> intel-ucode
|
||||||
|
GPU_MODE="prime-offload" # Intel UHD primary, NVIDIA GTX 1650 Ti on demand
|
||||||
|
WIFI_FW="yes"
|
||||||
|
KERNEL_PKG="linux"
|
||||||
|
|
||||||
|
# ---------- Services ----------
|
||||||
|
SSHD_ENABLE="no"
|
||||||
|
NETWORK_MGR="NetworkManager"
|
||||||
|
DISPLAY_MANAGER="lightdm"
|
||||||
|
DESKTOP="cinnamon"
|
||||||
|
ZRAM_ENABLE="yes"
|
||||||
|
ZRAM_SIZE_PCT="50" # 50% of RAM
|
||||||
|
|
||||||
|
# ---------- SSH config ----------
|
||||||
|
SSH_SOURCE_DIR="/etc/installer-ssh" # baked into ISO from /home/moze/.ssh
|
||||||
|
SSH_TARGET_DIR_REL=".ssh"
|
||||||
|
|
||||||
|
# ---------- Nix ----------
|
||||||
|
ENABLE_NIX="yes"
|
||||||
|
# Apps installed via `nix profile install` after first boot for $USERNAME:
|
||||||
|
NIX_USER_PACKAGES=(
|
||||||
|
"nixpkgs#spotify"
|
||||||
|
"nixpkgs#discord"
|
||||||
|
"nixpkgs#localsend"
|
||||||
|
"nixpkgs#google-chrome"
|
||||||
|
"nixpkgs#mission-center"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------- Cinnamon customization ----------
|
||||||
|
GTK_THEME="Gruvbox-Dark"
|
||||||
|
ICON_THEME="Gruvbox-Plus-Dark"
|
||||||
|
CURSOR_THEME="Bibata-Modern-Ice"
|
||||||
|
DEFAULT_TERMINAL="alacritty"
|
||||||
|
INITIAL_WALLPAPER="pxfuel.jpg"
|
||||||
|
|
||||||
|
# ---------- Test mode flag ----------
|
||||||
|
# Set TEST_MODE=1 in env when running under QEMU smoke tests.
|
||||||
|
TEST_MODE="${TEST_MODE:-0}"
|
||||||
19
config/packages.live.list
Normal file
19
config/packages.live.list
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Extra packages included in the live ISO (installer environment only).
|
||||||
|
dialog
|
||||||
|
ncurses
|
||||||
|
util-linux
|
||||||
|
gptfdisk
|
||||||
|
parted
|
||||||
|
btrfs-progs
|
||||||
|
dosfstools
|
||||||
|
xtools
|
||||||
|
rsync
|
||||||
|
curl
|
||||||
|
git
|
||||||
|
vim
|
||||||
|
NetworkManager
|
||||||
|
xz
|
||||||
|
tar
|
||||||
|
ca-certificates
|
||||||
|
pciutils
|
||||||
|
usbutils
|
||||||
149
config/profiles/mainline-niri/customizations/niri.sh
Normal file
149
config/profiles/mainline-niri/customizations/niri.sh
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Niri-specific customizations. Sourced by customizations.sh after the generic
|
||||||
|
# helpers when PROFILE=mainline-niri.
|
||||||
|
# Available env: $TARGET, $USERNAME, $PROFILE, $PROFILE_DIR, all install.conf vars.
|
||||||
|
|
||||||
|
_niri_write_kdl() {
|
||||||
|
local TARGET="$1"
|
||||||
|
local cfg="$TARGET/etc/skel/.config/niri"
|
||||||
|
install -d -m 0755 "$cfg"
|
||||||
|
cat > "$cfg/config.kdl" <<'EOF'
|
||||||
|
// niri config — generated by void-installer (mainline-niri profile).
|
||||||
|
input {
|
||||||
|
keyboard {
|
||||||
|
xkb {
|
||||||
|
layout "ch"
|
||||||
|
variant "fr"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
cursor {
|
||||||
|
xcursor-theme "Bibata-Modern-Ice"
|
||||||
|
xcursor-size 24
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
# Mirror into the actual user home.
|
||||||
|
install -d -m 0755 "$TARGET/home/$USERNAME/.config/niri"
|
||||||
|
cp "$cfg/config.kdl" "$TARGET/home/$USERNAME/.config/niri/config.kdl"
|
||||||
|
run_chroot "chown -R $USERNAME:$USERNAME /home/$USERNAME/.config/niri" || true
|
||||||
|
log "niri KDL config installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
_niri_write_env() {
|
||||||
|
local TARGET="$1"
|
||||||
|
cat > "$TARGET/etc/profile.d/wayland.sh" <<'EOF'
|
||||||
|
# Wayland defaults installed by void-installer (mainline-niri profile).
|
||||||
|
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
|
||||||
|
EOF
|
||||||
|
chmod 0644 "$TARGET/etc/profile.d/wayland.sh"
|
||||||
|
log "wayland environment installed at /etc/profile.d/wayland.sh"
|
||||||
|
}
|
||||||
|
|
||||||
|
_niri_setup_greetd() {
|
||||||
|
local TARGET="$1"
|
||||||
|
install -d -m 0755 "$TARGET/etc/greetd"
|
||||||
|
cat > "$TARGET/etc/greetd/config.toml" <<EOF
|
||||||
|
[terminal]
|
||||||
|
vt = 1
|
||||||
|
|
||||||
|
[default_session]
|
||||||
|
command = "tuigreet --time --remember --cmd niri-session"
|
||||||
|
user = "_greeter"
|
||||||
|
EOF
|
||||||
|
log "greetd configured for niri-session"
|
||||||
|
}
|
||||||
|
|
||||||
|
_niri_install_noctalia() {
|
||||||
|
local TARGET="$1"
|
||||||
|
# Third-party Void repo that ships noctalia-shell + noctalia-qs.
|
||||||
|
# Source: https://docs.noctalia.dev/getting-started/installation/#void
|
||||||
|
install -d -m 0755 "$TARGET/etc/xbps.d"
|
||||||
|
cat > "$TARGET/etc/xbps.d/10-noctalia.conf" <<'EOF'
|
||||||
|
repository=https://universalrepo.r1xelelo.workers.dev/void
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# If quickshell is somehow installed it conflicts with noctalia-qs.
|
||||||
|
run_chroot "xbps-remove -y quickshell 2>/dev/null || true"
|
||||||
|
|
||||||
|
# Sync the new repo and install. Prefix with the proxy mirror configured
|
||||||
|
# by the installer environment so noctalia-shell deps still resolve fast.
|
||||||
|
if ! run_chroot "xbps-install -Sy"; then
|
||||||
|
log "WARN: noctalia repo sync failed; skipping noctalia-shell install"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if run_chroot "xbps-install -y noctalia-shell"; then
|
||||||
|
log "noctalia-shell installed from third-party repo"
|
||||||
|
else
|
||||||
|
log "WARN: noctalia-shell install failed (repo may be down); shell omitted"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_niri_write_kdl "$TARGET"
|
||||||
|
_niri_write_env "$TARGET"
|
||||||
|
_niri_setup_greetd "$TARGET"
|
||||||
|
_niri_install_noctalia "$TARGET"
|
||||||
187
config/profiles/mainline-niri/packages.list
Normal file
187
config/profiles/mainline-niri/packages.list
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
# Packages installed into the target system for the mainline-niri profile.
|
||||||
|
# Lines beginning with '#' or empty are skipped.
|
||||||
|
|
||||||
|
# --- base / boot ---
|
||||||
|
base-system
|
||||||
|
linux-mainline
|
||||||
|
linux-firmware
|
||||||
|
linux-firmware-network
|
||||||
|
intel-ucode
|
||||||
|
grub-x86_64-efi
|
||||||
|
efibootmgr
|
||||||
|
os-prober
|
||||||
|
dracut
|
||||||
|
gptfdisk
|
||||||
|
parted
|
||||||
|
btrfs-progs
|
||||||
|
dosfstools
|
||||||
|
|
||||||
|
# --- core userspace ---
|
||||||
|
sudo
|
||||||
|
bash
|
||||||
|
bash-completion
|
||||||
|
git
|
||||||
|
curl
|
||||||
|
wget
|
||||||
|
vim
|
||||||
|
nano
|
||||||
|
htop
|
||||||
|
tmux
|
||||||
|
unzip
|
||||||
|
zip
|
||||||
|
xz
|
||||||
|
rsync
|
||||||
|
pciutils
|
||||||
|
usbutils
|
||||||
|
lsof
|
||||||
|
strace
|
||||||
|
file
|
||||||
|
which
|
||||||
|
man-pages
|
||||||
|
mdocml
|
||||||
|
ca-certificates
|
||||||
|
xtools
|
||||||
|
|
||||||
|
# --- networking ---
|
||||||
|
NetworkManager
|
||||||
|
NetworkManager-openvpn
|
||||||
|
openssh
|
||||||
|
iwd
|
||||||
|
nftables
|
||||||
|
chrony
|
||||||
|
|
||||||
|
# --- audio (pipewire stack) ---
|
||||||
|
pipewire
|
||||||
|
wireplumber
|
||||||
|
alsa-pipewire
|
||||||
|
pavucontrol
|
||||||
|
alsa-utils
|
||||||
|
playerctl
|
||||||
|
|
||||||
|
# --- graphics / wayland ---
|
||||||
|
wayland
|
||||||
|
wayland-protocols
|
||||||
|
xorg-server-xwayland
|
||||||
|
mesa-dri
|
||||||
|
mesa-vulkan-intel
|
||||||
|
intel-video-accel
|
||||||
|
vulkan-loader
|
||||||
|
libxkbcommon
|
||||||
|
|
||||||
|
# --- nvidia (PRIME offload) ---
|
||||||
|
nvidia
|
||||||
|
nvidia-libs-32bit
|
||||||
|
nvidia-vaapi-driver
|
||||||
|
|
||||||
|
# --- niri compositor + wayland ecosystem ---
|
||||||
|
niri
|
||||||
|
fuzzel
|
||||||
|
mako
|
||||||
|
swaybg
|
||||||
|
swayidle
|
||||||
|
swaylock
|
||||||
|
grim
|
||||||
|
slurp
|
||||||
|
wl-clipboard
|
||||||
|
xdg-desktop-portal
|
||||||
|
xdg-desktop-portal-gtk
|
||||||
|
xdg-desktop-portal-wlr
|
||||||
|
polkit-gnome
|
||||||
|
brightnessctl
|
||||||
|
|
||||||
|
# --- noctalia shell runtime deps (noctalia-shell itself is installed in
|
||||||
|
# niri.sh from the third-party XBPS repo at universalrepo.r1xelelo.workers.dev).
|
||||||
|
ImageMagick
|
||||||
|
python3
|
||||||
|
ddcutil
|
||||||
|
power-profiles-daemon
|
||||||
|
upower
|
||||||
|
cliphist
|
||||||
|
wlsunset
|
||||||
|
evolution-data-server
|
||||||
|
|
||||||
|
# --- file manager (no nemo) ---
|
||||||
|
Thunar
|
||||||
|
thunar-volman
|
||||||
|
thunar-archive-plugin
|
||||||
|
gvfs
|
||||||
|
gvfs-mtp
|
||||||
|
gvfs-smb
|
||||||
|
file-roller
|
||||||
|
gnome-keyring
|
||||||
|
seahorse
|
||||||
|
network-manager-applet
|
||||||
|
blueman
|
||||||
|
bluez
|
||||||
|
|
||||||
|
# --- 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.
|
||||||
|
greetd
|
||||||
|
tuigreet
|
||||||
|
|
||||||
|
# --- fonts ---
|
||||||
|
noto-fonts-ttf
|
||||||
|
noto-fonts-emoji
|
||||||
|
noto-fonts-cjk
|
||||||
|
liberation-fonts-ttf
|
||||||
|
dejavu-fonts-ttf
|
||||||
|
font-awesome6
|
||||||
|
|
||||||
|
# --- containers ---
|
||||||
|
docker
|
||||||
|
docker-compose
|
||||||
|
|
||||||
|
# --- terminal ---
|
||||||
|
alacritty
|
||||||
|
|
||||||
|
# --- gtk theming deps ---
|
||||||
|
sassc
|
||||||
|
gnome-themes-extra
|
||||||
|
gtk-engine-murrine
|
||||||
|
dconf
|
||||||
|
|
||||||
|
# --- media / utilities ---
|
||||||
|
vlc
|
||||||
|
obs
|
||||||
|
|
||||||
|
# --- nix package manager ---
|
||||||
|
nix
|
||||||
|
|
||||||
|
# --- zram / swap ---
|
||||||
|
zramen
|
||||||
|
|
||||||
|
# --- power / laptop ---
|
||||||
|
tlp
|
||||||
|
tlp-rdw
|
||||||
|
acpi
|
||||||
|
acpid
|
||||||
|
upower
|
||||||
|
|
||||||
|
# --- printing ---
|
||||||
|
cups
|
||||||
|
cups-filters
|
||||||
|
cups-pk-helper
|
||||||
|
ghostscript
|
||||||
|
foomatic-db
|
||||||
|
gutenprint
|
||||||
|
hplip
|
||||||
|
system-config-printer
|
||||||
|
sane
|
||||||
|
simple-scan
|
||||||
|
|
||||||
|
# --- bluetooth audio ---
|
||||||
|
bluez-alsa
|
||||||
|
|
||||||
|
# --- backups / snapshots ---
|
||||||
|
timeshift
|
||||||
|
grub-btrfs
|
||||||
|
inotify-tools
|
||||||
|
|
||||||
|
# --- trackpad gestures ---
|
||||||
|
libinput-gestures
|
||||||
|
xdotool
|
||||||
|
python3-setproctitle
|
||||||
|
|
||||||
|
# NOTE: waybar is intentionally absent — niri ships with its own panel
|
||||||
|
# helpers; if you want a bar add `waybar` once it lands in void-packages.
|
||||||
31
config/profiles/mainline-niri/profile.conf
Normal file
31
config/profiles/mainline-niri/profile.conf
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Mainline-niri profile.
|
||||||
|
# Linux mainline kernel + niri Wayland tiling compositor.
|
||||||
|
PROFILE_NAME="mainline-niri"
|
||||||
|
PROFILE_DESC="Linux mainline kernel + niri Wayland (scrolling tiler)"
|
||||||
|
|
||||||
|
# Mainline kernel for best Wayland / GPU support.
|
||||||
|
KERNEL_PKG="linux-mainline"
|
||||||
|
|
||||||
|
# Display server / DE.
|
||||||
|
DISPLAY_SERVER="wayland"
|
||||||
|
DESKTOP="niri"
|
||||||
|
|
||||||
|
# niri has no dconf, no Cinnamon — most cinnamon helpers are skipped via
|
||||||
|
# this DESKTOP gate inside customizations.sh.
|
||||||
|
|
||||||
|
# Package list.
|
||||||
|
PROFILE_PACKAGES_FILE="config/profiles/mainline-niri/packages.list"
|
||||||
|
|
||||||
|
# Theme settings still apply (gtk3/gtk4 apps under wayland still read these).
|
||||||
|
GTK_THEME="Gruvbox-Dark"
|
||||||
|
ICON_THEME="Gruvbox-Plus-Dark"
|
||||||
|
DEFAULT_TERMINAL="alacritty"
|
||||||
|
CURSOR_THEME="Bibata-Modern-Ice"
|
||||||
|
|
||||||
|
# Wayland shell — noctalia (installed via third-party XBPS repo by niri.sh).
|
||||||
|
WAYLAND_SHELL="noctalia"
|
||||||
|
|
||||||
|
# Wayland env defaults (exported into /etc/environment by profile customisation).
|
||||||
|
QT_QPA_PLATFORM="wayland;xcb"
|
||||||
|
GDK_BACKEND="wayland,x11"
|
||||||
|
MOZ_ENABLE_WAYLAND="1"
|
||||||
174
config/profiles/stable-cinnamon/packages.list
Normal file
174
config/profiles/stable-cinnamon/packages.list
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
# Packages installed into the target system via xbps-install.
|
||||||
|
# Lines beginning with '#' or empty are skipped.
|
||||||
|
|
||||||
|
# --- base / boot ---
|
||||||
|
base-system
|
||||||
|
linux
|
||||||
|
linux-firmware
|
||||||
|
linux-firmware-network
|
||||||
|
intel-ucode
|
||||||
|
grub-x86_64-efi
|
||||||
|
efibootmgr
|
||||||
|
os-prober
|
||||||
|
dracut
|
||||||
|
gptfdisk
|
||||||
|
parted
|
||||||
|
btrfs-progs
|
||||||
|
dosfstools
|
||||||
|
|
||||||
|
# --- core userspace ---
|
||||||
|
sudo
|
||||||
|
bash
|
||||||
|
bash-completion
|
||||||
|
git
|
||||||
|
curl
|
||||||
|
wget
|
||||||
|
vim
|
||||||
|
nano
|
||||||
|
htop
|
||||||
|
tmux
|
||||||
|
unzip
|
||||||
|
zip
|
||||||
|
xz
|
||||||
|
rsync
|
||||||
|
pciutils
|
||||||
|
usbutils
|
||||||
|
lsof
|
||||||
|
strace
|
||||||
|
file
|
||||||
|
which
|
||||||
|
man-pages
|
||||||
|
mdocml
|
||||||
|
ca-certificates
|
||||||
|
xtools
|
||||||
|
|
||||||
|
# --- networking ---
|
||||||
|
NetworkManager
|
||||||
|
NetworkManager-openvpn
|
||||||
|
openssh
|
||||||
|
iwd
|
||||||
|
wpa_supplicant
|
||||||
|
nftables
|
||||||
|
chrony
|
||||||
|
|
||||||
|
# --- audio ---
|
||||||
|
pipewire
|
||||||
|
wireplumber
|
||||||
|
alsa-pipewire
|
||||||
|
pavucontrol
|
||||||
|
alsa-utils
|
||||||
|
|
||||||
|
# --- graphics / xorg ---
|
||||||
|
xorg-minimal
|
||||||
|
xorg-fonts
|
||||||
|
xorg-input-drivers
|
||||||
|
xf86-input-libinput
|
||||||
|
xf86-video-intel
|
||||||
|
mesa-dri
|
||||||
|
mesa-vulkan-intel
|
||||||
|
intel-video-accel
|
||||||
|
vulkan-loader
|
||||||
|
|
||||||
|
# --- nvidia (PRIME offload) ---
|
||||||
|
nvidia
|
||||||
|
nvidia-libs-32bit
|
||||||
|
nvidia-vaapi-driver
|
||||||
|
|
||||||
|
# --- desktop ---
|
||||||
|
cinnamon
|
||||||
|
xdg-user-dirs
|
||||||
|
xdg-utils
|
||||||
|
xdg-desktop-portal
|
||||||
|
xdg-desktop-portal-gtk
|
||||||
|
gvfs
|
||||||
|
gvfs-mtp
|
||||||
|
gvfs-smb
|
||||||
|
file-roller
|
||||||
|
gnome-keyring
|
||||||
|
seahorse
|
||||||
|
network-manager-applet
|
||||||
|
blueman
|
||||||
|
bluez
|
||||||
|
|
||||||
|
# --- display manager ---
|
||||||
|
lightdm
|
||||||
|
lightdm-gtk3-greeter
|
||||||
|
|
||||||
|
# --- fonts ---
|
||||||
|
noto-fonts-ttf
|
||||||
|
noto-fonts-emoji
|
||||||
|
noto-fonts-cjk
|
||||||
|
liberation-fonts-ttf
|
||||||
|
dejavu-fonts-ttf
|
||||||
|
font-awesome6
|
||||||
|
|
||||||
|
# --- containers ---
|
||||||
|
docker
|
||||||
|
docker-compose
|
||||||
|
|
||||||
|
# --- editor ---
|
||||||
|
# Real Microsoft VS Code is installed from the official tarball in postinstall
|
||||||
|
# (see install_vscode_real); the Void `vscode` package is actually code-oss.
|
||||||
|
|
||||||
|
# --- terminal ---
|
||||||
|
alacritty
|
||||||
|
|
||||||
|
# --- gtk theming deps (for gruvbox theme) ---
|
||||||
|
sassc
|
||||||
|
gnome-themes-extra
|
||||||
|
gtk-engine-murrine
|
||||||
|
dconf
|
||||||
|
dconf-editor
|
||||||
|
|
||||||
|
# --- media / utilities (xbps) ---
|
||||||
|
vlc
|
||||||
|
obs
|
||||||
|
flameshot
|
||||||
|
# firefox — replaced by google-chrome (installed via nix)
|
||||||
|
|
||||||
|
# --- nix package manager (for spotify/discord/localsend) ---
|
||||||
|
nix
|
||||||
|
|
||||||
|
# --- zram / swap ---
|
||||||
|
zramen
|
||||||
|
|
||||||
|
# --- power / laptop ---
|
||||||
|
tlp
|
||||||
|
tlp-rdw
|
||||||
|
acpi
|
||||||
|
acpid
|
||||||
|
upower
|
||||||
|
brightnessctl
|
||||||
|
|
||||||
|
# --- printing (CUPS + drivers + Cinnamon settings panel uses system-config-printer) ---
|
||||||
|
cups
|
||||||
|
cups-filters
|
||||||
|
cups-pk-helper
|
||||||
|
ghostscript
|
||||||
|
foomatic-db
|
||||||
|
gutenprint
|
||||||
|
hplip
|
||||||
|
system-config-printer
|
||||||
|
sane
|
||||||
|
simple-scan
|
||||||
|
|
||||||
|
# --- bluetooth (blueman+bluez already above, ensure bluez-alsa/obex tools) ---
|
||||||
|
bluez-alsa
|
||||||
|
|
||||||
|
# --- backups / snapshots ---
|
||||||
|
timeshift
|
||||||
|
grub-btrfs
|
||||||
|
inotify-tools
|
||||||
|
|
||||||
|
# --- trackpad gestures (libinput-gestures + GUI) ---
|
||||||
|
libinput-gestures
|
||||||
|
wmctrl
|
||||||
|
xdotool
|
||||||
|
python3-setproctitle
|
||||||
|
|
||||||
|
# --- screenshots (flameshot already above, also xclip for clipboard) ---
|
||||||
|
xclip
|
||||||
|
|
||||||
|
# --- system / package upgrade GUI helpers (Octoxbps available via xtools) ---
|
||||||
|
# nothing extra needed; we ship a small custom xbps-upgrade applet
|
||||||
|
|
||||||
20
config/profiles/stable-cinnamon/profile.conf
Normal file
20
config/profiles/stable-cinnamon/profile.conf
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Stable Cinnamon profile (default).
|
||||||
|
# This is the ORIGINAL setup: stable Void kernel + Cinnamon DE + X11.
|
||||||
|
PROFILE_NAME="stable-cinnamon"
|
||||||
|
PROFILE_DESC="Stable Void kernel + Cinnamon (X11) — current production profile"
|
||||||
|
|
||||||
|
# Kernel — use Void's stable LTS.
|
||||||
|
KERNEL_PKG="linux"
|
||||||
|
|
||||||
|
# Display server / DE.
|
||||||
|
DISPLAY_SERVER="x11"
|
||||||
|
DESKTOP="cinnamon"
|
||||||
|
|
||||||
|
# Package list (relative to repo root).
|
||||||
|
PROFILE_PACKAGES_FILE="config/profiles/stable-cinnamon/packages.list"
|
||||||
|
|
||||||
|
# Default GTK theme + icons (overrides install.conf if set there).
|
||||||
|
GTK_THEME="Gruvbox-Dark"
|
||||||
|
ICON_THEME="Gruvbox-Plus-Dark"
|
||||||
|
DEFAULT_TERMINAL="alacritty"
|
||||||
|
CURSOR_THEME="Bibata-Modern-Ice"
|
||||||
190
docs/TESTING-ON-REAL-HARDWARE.md
Normal file
190
docs/TESTING-ON-REAL-HARDWARE.md
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
# Testing the Void Installer ISO on Real Hardware
|
||||||
|
|
||||||
|
End-to-end guide to take the freshly built `out/void-install.iso`, write it to a
|
||||||
|
USB stick, and run the installer on a real machine (e.g. the XPS 17 9700).
|
||||||
|
|
||||||
|
> ⚠️ **DESTRUCTIVE.** The installer will reformat the partition you select.
|
||||||
|
> Make a Timeshift / dd backup of any disk you care about before booting from
|
||||||
|
> the stick. The installer **never** touches the EFI System Partition — your
|
||||||
|
> existing Windows bootloader stays put.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1 · Prerequisites
|
||||||
|
|
||||||
|
| Item | Detail |
|
||||||
|
| --- | --- |
|
||||||
|
| USB stick | ≥ 2 GiB, **all data on it will be lost** |
|
||||||
|
| Target machine | UEFI firmware, x86_64, ≥ 4 GiB RAM, ≥ 30 GiB free on the target partition |
|
||||||
|
| Host (this laptop) | Linux with `lsblk`, `dd`, `sync`, root access |
|
||||||
|
| Built ISO | `out/void-install.iso` (run `make iso` first if missing) |
|
||||||
|
| Network | Ethernet **or** known-good Wi-Fi credentials (NetworkManager nmtui works in the live env) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2 · Build the production ISO
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/Sources/void-installer
|
||||||
|
make iso # builds out/void-install.iso (~1.3 GiB)
|
||||||
|
sha256sum out/void-install.iso
|
||||||
|
```
|
||||||
|
|
||||||
|
The build runs entirely inside Docker, so the host stays clean.
|
||||||
|
Output: `out/void-install.iso`. Keep the sha256 — you'll verify it after `dd`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3 · Identify the USB device
|
||||||
|
|
||||||
|
Plug the USB stick in. Then:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
lsblk -o NAME,SIZE,MODEL,TRAN,MOUNTPOINTS
|
||||||
|
```
|
||||||
|
|
||||||
|
Find the line with `usb` in the `TRAN` column. Typical names: `/dev/sdb`,
|
||||||
|
`/dev/sdc`. **Never** pick `/dev/sda` (your system disk) or anything starting
|
||||||
|
with `nvme0n1` (your NVMe). Confirm with the size (matches your stick).
|
||||||
|
|
||||||
|
Set a shell variable so you can't typo it later:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
USB=/dev/sdX # ← replace X with the actual letter you saw above
|
||||||
|
echo "WILL ERASE: $USB"
|
||||||
|
lsblk "$USB" # double-check
|
||||||
|
```
|
||||||
|
|
||||||
|
If the stick has any mounted partitions, unmount them all:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for p in $(lsblk -nro NAME "$USB" | tail -n +2); do
|
||||||
|
sudo umount "/dev/$p" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4 · Flash the ISO
|
||||||
|
|
||||||
|
The Void ISO is a **hybrid ISO** (xorriso-isohybrid), so plain `dd` works for
|
||||||
|
both UEFI and legacy BIOS. **No partitioning, no formatting, no GUI tool
|
||||||
|
needed.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo dd if=out/void-install.iso of="$USB" bs=4M status=progress conv=fsync oflag=direct
|
||||||
|
sync
|
||||||
|
```
|
||||||
|
|
||||||
|
When it finishes (~1-3 min on USB 3.x), verify the write actually landed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo dd if="$USB" bs=4M count=$(( $(stat -c%s out/void-install.iso) / 4 / 1024 / 1024 )) \
|
||||||
|
status=none | sha256sum
|
||||||
|
sha256sum out/void-install.iso
|
||||||
|
```
|
||||||
|
|
||||||
|
The two hashes should match (truncated read length matters; if they differ try
|
||||||
|
adding `iflag=fullblock`).
|
||||||
|
|
||||||
|
> Alternative GUI tools that also work: GNOME Disks → "Restore Disk Image…",
|
||||||
|
> balenaEtcher, Ventoy. Avoid Rufus' "ISO mode" → it rebuilds the partition
|
||||||
|
> table and can break UEFI boot.
|
||||||
|
|
||||||
|
Eject the stick cleanly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo eject "$USB"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5 · Boot the target machine from USB
|
||||||
|
|
||||||
|
1. Plug the stick into the **target laptop** (not the host).
|
||||||
|
2. Power on while spamming the firmware boot-menu key:
|
||||||
|
- Dell XPS 17: **F12**
|
||||||
|
- ThinkPad: **F12**
|
||||||
|
- HP: **F9**
|
||||||
|
- Most others: **F8**, **F10**, **F11**, **Esc**
|
||||||
|
3. From the one-shot boot menu, select the entry that contains your USB stick's
|
||||||
|
model name **and** the prefix `UEFI:` (NOT the legacy/MBR entry).
|
||||||
|
4. The Void Linux GRUB menu appears — keep the default "Void Linux installer"
|
||||||
|
entry (or wait for the timeout).
|
||||||
|
5. After ~10-30 s you land on a console as `root` and the installer banner
|
||||||
|
appears.
|
||||||
|
|
||||||
|
> **Secure Boot:** The ISO is **not** signed for Secure Boot. Disable Secure
|
||||||
|
> Boot in the firmware setup before the first boot, or shim signing will fail.
|
||||||
|
> You can re-enable it after the install completes (Void's GRUB will not
|
||||||
|
> validate, so leave it off unless you sign the kernel yourself).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6 · Run the installer
|
||||||
|
|
||||||
|
When you see the banner, follow the on-screen prompts. The installer is
|
||||||
|
**unattended-by-default** for the developer profile and will:
|
||||||
|
|
||||||
|
1. Bring up networking (DHCP on Ethernet, or `nmtui` for Wi-Fi if no link).
|
||||||
|
2. Show the disk picker — select your **target btrfs partition** (e.g.
|
||||||
|
`/dev/nvme0n1p5`). The picker refuses to format the EFI partition or
|
||||||
|
anything labelled NTFS / Windows.
|
||||||
|
3. Confirm the summary by typing `YES` (case-sensitive). Last chance to abort.
|
||||||
|
4. Wipe the chosen partition, mkfs.btrfs, mount subvolumes.
|
||||||
|
5. Bootstrap base-system + ~140 packages from the public Void mirror.
|
||||||
|
6. Configure users, sudo, services, GRUB chained next to the existing Windows
|
||||||
|
loader.
|
||||||
|
7. Reboot. Remove the USB stick **before** GRUB starts.
|
||||||
|
|
||||||
|
Total install time on a Gen-12 laptop with NVMe + 1 Gbps internet: **8-15 min**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7 · First-boot checks
|
||||||
|
|
||||||
|
After the system reboots, log in as **moze** (password from your `secrets.env`).
|
||||||
|
|
||||||
|
| Check | Command | Expected |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Cinnamon session | (lightdm shows it on login) | green Cinnamon wallpaper |
|
||||||
|
| Network | `nmcli c` | active connection |
|
||||||
|
| Audio | open `pavucontrol` | sink listed, no errors |
|
||||||
|
| Bluetooth | tray icon → "Devices…" | scan starts |
|
||||||
|
| Printer | Settings → Printers | CUPS server reachable |
|
||||||
|
| Trackpad gestures | three-finger swipe up | virtual-desktop overview |
|
||||||
|
| Screenshot | press **PrintScreen** | flameshot UI appears |
|
||||||
|
| Snapshots | `sudo timeshift --list` | (empty list, no errors) |
|
||||||
|
| Pre-upgrade snapshot | `sudo xbps-install -Sun \| tail` | wrapper prints `[snapshot]` line |
|
||||||
|
| GPU PRIME offload | `nvidia-smi` | NVIDIA GPU detected |
|
||||||
|
| Docker | `docker run --rm hello-world` | "Hello from Docker!" |
|
||||||
|
| VS Code | `code --version` | version printed |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8 · Recovery (if something goes wrong)
|
||||||
|
|
||||||
|
The installer **never overwrites your EFI partition** and **never touches
|
||||||
|
partitions other than the one you selected**. To roll back:
|
||||||
|
|
||||||
|
- **Boot Windows again**: in firmware setup, move the Windows boot entry above
|
||||||
|
Void in the EFI boot order. Or hit the one-shot boot menu and pick Windows.
|
||||||
|
- **Wipe the Void partition**: boot back into the live USB, mount your old
|
||||||
|
filesystem (e.g. `mount /dev/nvme0n1p5 /mnt`), and either restore from your
|
||||||
|
backup or `mkfs.<oldfs> /dev/nvme0n1p5`.
|
||||||
|
- **Restore the GRUB menu only**: in the live USB,
|
||||||
|
`mount /dev/<EFI> /mnt/efi && grub-install --efi-directory=/mnt/efi --bootloader-id=Windows --removable` (after chrooting into your old install) or use Microsoft's "Startup Repair" from a Windows install media.
|
||||||
|
- **Restore a Timeshift snapshot** (post-install): boot into the live USB,
|
||||||
|
`xbps-install -Sy timeshift`, then `timeshift --restore --snapshot <name>`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9 · Reporting bugs
|
||||||
|
|
||||||
|
If the installer aborts:
|
||||||
|
|
||||||
|
1. The serial / TUI shows the failing line and a log path.
|
||||||
|
2. Copy `/tmp/installer.log` and `/var/log/void-installer.log` off the live
|
||||||
|
USB (e.g. via `scp` over Ethernet) and attach them to the bug report.
|
||||||
|
3. Note: which step failed, exact partition layout (`lsblk -f`), firmware mode
|
||||||
|
(UEFI vs BIOS), and whether Secure Boot was on.
|
||||||
77
installer/first-login.sh
Normal file
77
installer/first-login.sh
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# First-login one-shot setup for the user.
|
||||||
|
# Installs Claude Code (official) + NVM + node LTS + vscode extensions.
|
||||||
|
# Idempotent: creates ~/.first-login-done marker on success.
|
||||||
|
|
||||||
|
# NOTE: do NOT use `set -u` here — nvm.sh references unbound vars.
|
||||||
|
LOG="$HOME/.first-login.log"
|
||||||
|
exec > >(tee -a "$LOG") 2>&1
|
||||||
|
|
||||||
|
echo "==> [$(date)] first-login setup starting"
|
||||||
|
|
||||||
|
# Need network. Wait up to 60s for default route + DNS.
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
getent hosts github.com >/dev/null 2>&1 && break
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
if ! getent hosts github.com >/dev/null 2>&1; then
|
||||||
|
echo "!! no network; aborting first-login setup (will retry next login)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Claude Code (official native installer) — runs FIRST so failures in
|
||||||
|
# downstream NVM/node/etc. don't block claude installation. ---
|
||||||
|
mkdir -p "$HOME/.local/bin"
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
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
|
||||||
|
|
||||||
|
# --- NVM (best effort; nvm.sh has unbound vars so isolate it) ---
|
||||||
|
if [[ ! -s "$HOME/.nvm/nvm.sh" ]]; then
|
||||||
|
echo "==> installing NVM"
|
||||||
|
export NVM_DIR="$HOME/.nvm"
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash || \
|
||||||
|
echo "!! NVM install failed (continuing)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -s "$HOME/.nvm/nvm.sh" ]]; then
|
||||||
|
export NVM_DIR="$HOME/.nvm"
|
||||||
|
# nvm.sh trips `set -u` on STABLE/PROVIDED_VERSION; isolate in subshell.
|
||||||
|
(
|
||||||
|
set +u
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
. "$NVM_DIR/nvm.sh"
|
||||||
|
if ! nvm ls --no-colors 2>/dev/null | grep -qE 'lts/'; then
|
||||||
|
echo "==> installing node LTS"
|
||||||
|
nvm install --lts || echo "!! node install failed"
|
||||||
|
fi
|
||||||
|
nvm use --lts >/dev/null 2>&1 || true
|
||||||
|
) || true
|
||||||
|
|
||||||
|
# Symlink the resulting node/npm into ~/.local/bin so they're on PATH
|
||||||
|
# for non-nvm shells.
|
||||||
|
NODE_BIN_DIR="$(ls -d "$HOME"/.nvm/versions/node/v*/bin 2>/dev/null | sort -V | tail -1)"
|
||||||
|
if [[ -n "$NODE_BIN_DIR" && -d "$NODE_BIN_DIR" ]]; then
|
||||||
|
for bin in node npm npx; do
|
||||||
|
[[ -x "$NODE_BIN_DIR/$bin" ]] && ln -sf "$NODE_BIN_DIR/$bin" "$HOME/.local/bin/$bin"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- VS Code extensions ---
|
||||||
|
EXT_FILE=/etc/installer-vscode-extensions.txt
|
||||||
|
if [[ -r "$EXT_FILE" ]] && command -v code >/dev/null 2>&1; then
|
||||||
|
echo "==> installing VS Code extensions"
|
||||||
|
while read -r ext; do
|
||||||
|
[[ -z "$ext" || "$ext" =~ ^# ]] && continue
|
||||||
|
echo " -> $ext"
|
||||||
|
code --install-extension "$ext" --force >/dev/null 2>&1 || \
|
||||||
|
echo " (failed: $ext)"
|
||||||
|
done < "$EXT_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
touch "$HOME/.first-login-done"
|
||||||
|
echo "==> [$(date)] first-login setup done"
|
||||||
118
installer/install.sh
Executable file
118
installer/install.sh
Executable file
@@ -0,0 +1,118 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Void Linux unattended-friendly installer for the XPS 17 (xps9700).
|
||||||
|
# Runs inside the Void live ISO. Reads /etc/installer-secrets.env for passwords
|
||||||
|
# and /usr/local/share/installer/install.conf for everything else.
|
||||||
|
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
# Resolve the real script path so this works whether invoked directly or via
|
||||||
|
# the /usr/local/sbin/install-void symlink.
|
||||||
|
INSTALLER_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
|
||||||
|
SHARE_DIR="${INSTALLER_SHARE_DIR:-/usr/local/share/installer}"
|
||||||
|
|
||||||
|
# shellcheck source=lib/common.sh
|
||||||
|
source "$INSTALLER_DIR/lib/common.sh"
|
||||||
|
|
||||||
|
# ---------- load config ----------
|
||||||
|
CONFIG_FILE="${CONFIG_FILE:-$SHARE_DIR/install.conf}"
|
||||||
|
[[ -r "$CONFIG_FILE" ]] || die "config file $CONFIG_FILE missing"
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$CONFIG_FILE"
|
||||||
|
log "loaded config from $CONFIG_FILE"
|
||||||
|
|
||||||
|
# Drop-in overrides (used by the QEMU test harness).
|
||||||
|
if [[ -d "${CONFIG_FILE}.d" ]]; then
|
||||||
|
for f in "${CONFIG_FILE}.d"/*.conf; do
|
||||||
|
[[ -r "$f" ]] || continue
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$f"
|
||||||
|
log "loaded override $f"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
PKG_LIST_FILE="${PKG_LIST_FILE:-$SHARE_DIR/packages.list}"
|
||||||
|
export PKG_LIST_FILE
|
||||||
|
[[ -r "$PKG_LIST_FILE" ]] || die "packages.list $PKG_LIST_FILE missing"
|
||||||
|
|
||||||
|
# ---------- profile ----------
|
||||||
|
PROJECT_DIR="${PROJECT_DIR:-$SHARE_DIR}"
|
||||||
|
PROFILES_DIR="${PROFILES_DIR:-$SHARE_DIR/profiles}"
|
||||||
|
export PROJECT_DIR PROFILES_DIR
|
||||||
|
# shellcheck source=lib/profiles.sh
|
||||||
|
source "$INSTALLER_DIR/lib/profiles.sh"
|
||||||
|
load_profile || die "could not load profile '${PROFILE:-stable-cinnamon}'"
|
||||||
|
# Profile may override the package list.
|
||||||
|
[[ -r "$PROFILE_PACKAGES_FILE" ]] && PKG_LIST_FILE="$PROFILE_PACKAGES_FILE"
|
||||||
|
log "using packages list: $PKG_LIST_FILE"
|
||||||
|
|
||||||
|
load_secrets
|
||||||
|
|
||||||
|
# ---------- pre-flight ----------
|
||||||
|
require_root
|
||||||
|
|
||||||
|
# Default DEFAULT_ROOT_PART/EFI from config; will be confirmed/overridden by TUI.
|
||||||
|
ROOT_PART="${ROOT_PART:-$DEFAULT_ROOT_PART}"
|
||||||
|
EFI_PART="${EFI_PART:-$DEFAULT_EFI_PART}"
|
||||||
|
TARGET="${TARGET:-/mnt}"
|
||||||
|
export TARGET
|
||||||
|
|
||||||
|
# ---------- modules ----------
|
||||||
|
# shellcheck source=lib/tui.sh
|
||||||
|
source "$INSTALLER_DIR/lib/tui.sh"
|
||||||
|
# shellcheck source=lib/partition.sh
|
||||||
|
source "$INSTALLER_DIR/lib/partition.sh"
|
||||||
|
# shellcheck source=lib/bootstrap.sh
|
||||||
|
source "$INSTALLER_DIR/lib/bootstrap.sh"
|
||||||
|
# shellcheck source=lib/grub.sh
|
||||||
|
source "$INSTALLER_DIR/lib/grub.sh"
|
||||||
|
# shellcheck source=lib/postinstall.sh
|
||||||
|
source "$INSTALLER_DIR/lib/postinstall.sh"
|
||||||
|
# shellcheck source=lib/customizations.sh
|
||||||
|
source "$INSTALLER_DIR/lib/customizations.sh"
|
||||||
|
|
||||||
|
# ---------- run ----------
|
||||||
|
banner() {
|
||||||
|
cat <<'EOF'
|
||||||
|
|
||||||
|
╔══════════════════════════════════════════════════════════════╗
|
||||||
|
║ Void Linux Installer (xps9700) ║
|
||||||
|
║ target: btrfs on a single partition, dual-boot Windows ║
|
||||||
|
║ desktop: cinnamon | docker | vscode | nvidia PRIME ║
|
||||||
|
╚══════════════════════════════════════════════════════════════╝
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
banner
|
||||||
|
log "starting installer (UNATTENDED=${UNATTENDED:-0}, TEST_MODE=${TEST_MODE:-0})"
|
||||||
|
|
||||||
|
tui_select_install_target
|
||||||
|
setup_filesystems
|
||||||
|
bootstrap_base
|
||||||
|
generate_fstab
|
||||||
|
mount_pseudo_fs
|
||||||
|
|
||||||
|
configure_system
|
||||||
|
configure_users
|
||||||
|
configure_ssh_config
|
||||||
|
configure_nvidia_prime
|
||||||
|
configure_zram
|
||||||
|
configure_nix
|
||||||
|
install_vscode_real
|
||||||
|
install_customizations
|
||||||
|
enable_services
|
||||||
|
install_grub
|
||||||
|
reconfigure_all
|
||||||
|
|
||||||
|
unmount_target
|
||||||
|
|
||||||
|
ok "Installation complete."
|
||||||
|
log "Log file: $LOG_FILE"
|
||||||
|
if [[ "${TEST_MODE:-0}" != "1" && "${UNATTENDED:-0}" != "1" ]]; then
|
||||||
|
if confirm "Reboot now?"; then
|
||||||
|
systemctl reboot 2>/dev/null || reboot
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
123
installer/lib/bootstrap.sh
Executable file
123
installer/lib/bootstrap.sh
Executable file
@@ -0,0 +1,123 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Bootstrap base system into $TARGET via xbps-install.
|
||||||
|
|
||||||
|
# shellcheck source=common.sh
|
||||||
|
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
||||||
|
|
||||||
|
bootstrap_base() {
|
||||||
|
step "Bootstrapping base system into $TARGET (this takes a while)"
|
||||||
|
|
||||||
|
local TARGET="${TARGET:-/mnt}"
|
||||||
|
local pkg_list=()
|
||||||
|
mapfile -t pkg_list < <(grep -vE '^\s*(#|$)' "${PKG_LIST_FILE:-/usr/local/share/installer/packages.list}")
|
||||||
|
|
||||||
|
mkdir -p "$TARGET/var/db/xbps/keys"
|
||||||
|
cp -a /var/db/xbps/keys/* "$TARGET/var/db/xbps/keys/" 2>/dev/null || \
|
||||||
|
warn "could not copy xbps keys (running outside Void live env?)"
|
||||||
|
|
||||||
|
# Use the caching proxy during install if available; fall back to real repo.
|
||||||
|
# The proxy URL (if set) is only used during installation — the installed
|
||||||
|
# system's xbps.d always gets the real REPO_URL.
|
||||||
|
local _repo="${INSTALL_REPO_URL:-$REPO_URL}"
|
||||||
|
local _nonfree="${_repo%/current}/current/nonfree"
|
||||||
|
local _multilib="${_repo%/current}/current/multilib"
|
||||||
|
local _multilib_nonfree="${_repo%/current}/current/multilib/nonfree"
|
||||||
|
# Normalise: if _repo doesn't end in /current the above substitution
|
||||||
|
# leaves it unchanged; append nonfree/multilib paths directly.
|
||||||
|
[[ "$_repo" == */current ]] || {
|
||||||
|
_nonfree="${_repo}/nonfree"
|
||||||
|
_multilib="${_repo}/multilib"
|
||||||
|
_multilib_nonfree="${_repo}/multilib/nonfree"
|
||||||
|
}
|
||||||
|
log "install repo: $_repo (proxy=${INSTALL_REPO_URL:-none})"
|
||||||
|
|
||||||
|
# Bootstrap base-system via proxy/repo.
|
||||||
|
XBPS_ARCH="$ARCH" xbps-install -y -S -r "$TARGET" -R "$_repo" base-system
|
||||||
|
ok "base-system installed"
|
||||||
|
|
||||||
|
# Enable extra repos in target xbps.d — always write the REAL URLs so the
|
||||||
|
# installed system never depends on the proxy.
|
||||||
|
mkdir -p "$TARGET/etc/xbps.d"
|
||||||
|
cat > "$TARGET/etc/xbps.d/00-repository-main.conf" <<EOF
|
||||||
|
repository=$REPO_URL
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log "enabling extra repos: ${EXTRA_REPOS[*]}"
|
||||||
|
XBPS_ARCH="$ARCH" xbps-install -y -S -r "$TARGET" \
|
||||||
|
-R "$_repo" -R "$_nonfree" -R "$_multilib" -R "$_multilib_nonfree" \
|
||||||
|
void-repo-nonfree void-repo-multilib void-repo-multilib-nonfree
|
||||||
|
|
||||||
|
# Sync from all newly enabled repos.
|
||||||
|
XBPS_ARCH="$ARCH" xbps-install -y -S -r "$TARGET" \
|
||||||
|
-R "$_repo" -R "$_nonfree" -R "$_multilib" -R "$_multilib_nonfree"
|
||||||
|
|
||||||
|
log "installing extra packages from $PKG_LIST_FILE"
|
||||||
|
XBPS_ARCH="$ARCH" xbps-install -y -r "$TARGET" \
|
||||||
|
-R "$_repo" -R "$_nonfree" -R "$_multilib" -R "$_multilib_nonfree" \
|
||||||
|
"${pkg_list[@]}"
|
||||||
|
|
||||||
|
# Ensure the installed system's xbps config points only at real repos.
|
||||||
|
cat > "$TARGET/etc/xbps.d/00-repository-main.conf" <<EOF
|
||||||
|
repository=$REPO_URL
|
||||||
|
EOF
|
||||||
|
|
||||||
|
ok "all xbps packages installed (proxy removed from target xbps.d)"
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_fstab() {
|
||||||
|
step "Generating /etc/fstab"
|
||||||
|
local TARGET="${TARGET:-/mnt}"
|
||||||
|
if command -v xgenfstab >/dev/null 2>&1; then
|
||||||
|
xgenfstab -U "$TARGET" > "$TARGET/etc/fstab"
|
||||||
|
else
|
||||||
|
# fallback minimal generator (uses BTRFS_SUBVOLS array from install.conf)
|
||||||
|
: > "$TARGET/etc/fstab"
|
||||||
|
local uuid entry sv mp pass
|
||||||
|
uuid=$(blkid -s UUID -o value "$ROOT_PART")
|
||||||
|
for entry in "${BTRFS_SUBVOLS[@]}"; do
|
||||||
|
sv="${entry%%:*}"
|
||||||
|
mp="${entry##*:}"
|
||||||
|
# root gets fsck pass 1, others 2
|
||||||
|
pass=2; [[ "$mp" == "/" ]] && pass=1
|
||||||
|
printf 'UUID=%s %-12s btrfs %s,subvol=%s 0 %d\n' \
|
||||||
|
"$uuid" "$mp" "$BTRFS_MOUNT_OPTS" "$sv" "$pass" >> "$TARGET/etc/fstab"
|
||||||
|
done
|
||||||
|
local efi_uuid
|
||||||
|
efi_uuid=$(blkid -s UUID -o value "$EFI_PART")
|
||||||
|
printf 'UUID=%s /boot/efi vfat defaults,noatime,umask=0077 0 2\n' "$efi_uuid" >> "$TARGET/etc/fstab"
|
||||||
|
printf 'tmpfs /tmp tmpfs defaults,nosuid,nodev 0 0\n' >> "$TARGET/etc/fstab"
|
||||||
|
fi
|
||||||
|
ok "fstab generated"
|
||||||
|
}
|
||||||
|
|
||||||
|
mount_pseudo_fs() {
|
||||||
|
local TARGET="${TARGET:-/mnt}"
|
||||||
|
step "Mounting pseudo-filesystems for chroot"
|
||||||
|
for d in dev proc sys run; do
|
||||||
|
mkdir -p "$TARGET/$d"
|
||||||
|
mount --rbind "/$d" "$TARGET/$d"
|
||||||
|
mount --make-rslave "$TARGET/$d"
|
||||||
|
done
|
||||||
|
# /dev/shm must be a real tmpfs for python multiprocessing (sem_open)
|
||||||
|
# used by xbps-reconfigure → compileall. Without it the byte-compile
|
||||||
|
# step fails with FileNotFoundError on every python package.
|
||||||
|
mkdir -p "$TARGET/dev/shm"
|
||||||
|
mountpoint -q "$TARGET/dev/shm" || mount -t tmpfs -o nosuid,nodev tmpfs "$TARGET/dev/shm" 2>/dev/null || true
|
||||||
|
if is_uefi; then
|
||||||
|
mkdir -p "$TARGET/sys/firmware/efi/efivars"
|
||||||
|
mount -t efivarfs efivarfs "$TARGET/sys/firmware/efi/efivars" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
# Make DNS work inside the chroot (needed for VS Code download, etc.).
|
||||||
|
if [[ -r /etc/resolv.conf ]]; then
|
||||||
|
install -Dm644 /etc/resolv.conf "$TARGET/etc/resolv.conf" 2>/dev/null \
|
||||||
|
|| cp /etc/resolv.conf "$TARGET/etc/resolv.conf" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
unmount_target() {
|
||||||
|
local TARGET="${TARGET:-/mnt}"
|
||||||
|
step "Unmounting $TARGET"
|
||||||
|
sync
|
||||||
|
umount -R "$TARGET" 2>/dev/null || warn "lazy umount fallback"
|
||||||
|
umount -lR "$TARGET" 2>/dev/null || true
|
||||||
|
}
|
||||||
98
installer/lib/common.sh
Executable file
98
installer/lib/common.sh
Executable file
@@ -0,0 +1,98 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Common helpers: logging, error handling, run-as-root checks.
|
||||||
|
# Sourced by all installer modules.
|
||||||
|
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
# --- colors ---
|
||||||
|
if [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then
|
||||||
|
C_RED="$(tput setaf 1)"; C_GREEN="$(tput setaf 2)"
|
||||||
|
C_YEL="$(tput setaf 3)"; C_BLUE="$(tput setaf 4)"
|
||||||
|
C_BOLD="$(tput bold)"; C_RESET="$(tput sgr0)"
|
||||||
|
else
|
||||||
|
C_RED=""; C_GREEN=""; C_YEL=""; C_BLUE=""; C_BOLD=""; C_RESET=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
LOG_FILE="${LOG_FILE:-/var/log/void-installer.log}"
|
||||||
|
mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true
|
||||||
|
: > "$LOG_FILE" 2>/dev/null || LOG_FILE="/tmp/void-installer.log"
|
||||||
|
|
||||||
|
log() { printf '%s [%s] %s\n' "$(date +%H:%M:%S)" "INFO" "$*" | tee -a "$LOG_FILE" >&2; }
|
||||||
|
warn() { printf '%s%s [%s] %s%s\n' "$C_YEL" "$(date +%H:%M:%S)" "WARN" "$*" "$C_RESET" | tee -a "$LOG_FILE" >&2; }
|
||||||
|
err() { printf '%s%s [%s] %s%s\n' "$C_RED" "$(date +%H:%M:%S)" "ERR " "$*" "$C_RESET" | tee -a "$LOG_FILE" >&2; }
|
||||||
|
ok() { printf '%s%s [%s] %s%s\n' "$C_GREEN" "$(date +%H:%M:%S)" " OK " "$*" "$C_RESET" | tee -a "$LOG_FILE" >&2; }
|
||||||
|
step() { printf '\n%s%s==> %s%s\n' "$C_BOLD" "$C_BLUE" "$*" "$C_RESET" | tee -a "$LOG_FILE" >&2; }
|
||||||
|
|
||||||
|
die() { err "$*"; exit 1; }
|
||||||
|
|
||||||
|
trap 'err "Installer aborted at line $LINENO (exit=$?). Log: $LOG_FILE"' ERR
|
||||||
|
|
||||||
|
require_root() {
|
||||||
|
[[ "${EUID:-$(id -u)}" -eq 0 ]] || die "must run as root"
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm() {
|
||||||
|
# confirm "Question" -> returns 0 on yes
|
||||||
|
local prompt="${1:-Proceed?}"
|
||||||
|
if [[ "${UNATTENDED:-0}" == "1" ]]; then
|
||||||
|
log "[unattended] auto-yes: $prompt"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
local ans
|
||||||
|
read -r -p "$prompt [y/N] " ans
|
||||||
|
[[ "$ans" =~ ^[Yy]$ ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
run_chroot() {
|
||||||
|
# run a command inside the target chroot
|
||||||
|
local target="${TARGET:-/mnt}"
|
||||||
|
chroot "$target" /usr/bin/env -i \
|
||||||
|
HOME=/root TERM="${TERM:-linux}" \
|
||||||
|
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
|
||||||
|
LANG="${LANG:-en_US.UTF-8}" \
|
||||||
|
/bin/bash -c "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
is_uefi() {
|
||||||
|
[[ -d /sys/firmware/efi/efivars ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set a password inside the chroot without exposing it to the shell argv
|
||||||
|
# or to ${} expansion in command strings (bash injection-safe).
|
||||||
|
# Uses openssl to pre-hash, then usermod -p, because chpasswd on Void can
|
||||||
|
# silently no-op for freshly-created (locked) accounts depending on the
|
||||||
|
# default crypt method in /etc/login.defs.
|
||||||
|
set_chroot_password() {
|
||||||
|
local user="$1" password="$2"
|
||||||
|
local target="${TARGET:-/mnt}"
|
||||||
|
# Generate a SHA-512 crypt hash on the host (openssl is in the live ISO).
|
||||||
|
local hash
|
||||||
|
hash="$(openssl passwd -6 "$password")" || {
|
||||||
|
warn "openssl passwd failed for $user; falling back to chpasswd"
|
||||||
|
chroot "$target" /usr/bin/env -i \
|
||||||
|
HOME=/root TERM="${TERM:-linux}" \
|
||||||
|
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
|
||||||
|
chpasswd <<EOF
|
||||||
|
$user:$password
|
||||||
|
EOF
|
||||||
|
return
|
||||||
|
}
|
||||||
|
# usermod -p writes the hash directly into /etc/shadow and unlocks.
|
||||||
|
chroot "$target" /usr/bin/env -i \
|
||||||
|
HOME=/root TERM="${TERM:-linux}" \
|
||||||
|
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
|
||||||
|
usermod -p "$hash" "$user"
|
||||||
|
}
|
||||||
|
|
||||||
|
load_secrets() {
|
||||||
|
local f="${1:-/etc/installer-secrets.env}"
|
||||||
|
if [[ -r "$f" ]]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$f"
|
||||||
|
log "loaded secrets from $f"
|
||||||
|
else
|
||||||
|
warn "no secrets file at $f; passwords must be set in env"
|
||||||
|
fi
|
||||||
|
: "${USER_PASSWORD:?USER_PASSWORD missing}"
|
||||||
|
: "${ROOT_PASSWORD:?ROOT_PASSWORD missing}"
|
||||||
|
}
|
||||||
432
installer/lib/customizations.sh
Normal file
432
installer/lib/customizations.sh
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# User-environment customizations: themes, icons, wallpapers, dotfiles,
|
||||||
|
# vscode config, cinnamon dconf defaults, default terminal, keymap.
|
||||||
|
# Reads pre-baked overlay from /etc/installer-overlay/ on the live ISO.
|
||||||
|
|
||||||
|
# shellcheck source=common.sh
|
||||||
|
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
||||||
|
|
||||||
|
OVERLAY_SRC="${OVERLAY_SRC:-/etc/installer-overlay}"
|
||||||
|
|
||||||
|
install_customizations() {
|
||||||
|
step "Installing customizations (themes / icons / wallpapers / dotfiles)"
|
||||||
|
local TARGET="${TARGET:-/mnt}"
|
||||||
|
|
||||||
|
if [[ ! -d "$OVERLAY_SRC" ]]; then
|
||||||
|
warn "no overlay at $OVERLAY_SRC; skipping customizations"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
_deploy_themes "$TARGET"
|
||||||
|
_deploy_icons "$TARGET"
|
||||||
|
_deploy_wallpapers "$TARGET"
|
||||||
|
_deploy_user_dotfiles "$TARGET"
|
||||||
|
_deploy_vscode_config "$TARGET"
|
||||||
|
_deploy_first_login "$TARGET"
|
||||||
|
_set_default_terminal "$TARGET"
|
||||||
|
_install_gestures "$TARGET"
|
||||||
|
_install_snapshot_hook "$TARGET"
|
||||||
|
_install_upgrade_applet "$TARGET"
|
||||||
|
|
||||||
|
# Cinnamon/X11-specific helpers — only run on the cinnamon profile.
|
||||||
|
if [[ "${DESKTOP:-cinnamon}" == "cinnamon" ]]; then
|
||||||
|
_write_dconf_defaults "$TARGET"
|
||||||
|
_install_nemo_actions "$TARGET"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Profile-specific hooks (e.g. niri KDL config, waybar, greetd).
|
||||||
|
if declare -F run_profile_customizations >/dev/null 2>&1; then
|
||||||
|
run_profile_customizations
|
||||||
|
fi
|
||||||
|
|
||||||
|
ok "customizations installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
_deploy_themes() {
|
||||||
|
local TARGET="$1"
|
||||||
|
local src="$OVERLAY_SRC/themes"
|
||||||
|
[[ -d "$src" ]] || { log "no themes overlay; skipping"; return 0; }
|
||||||
|
|
||||||
|
install -d -m 0755 "$TARGET/usr/share/themes"
|
||||||
|
# Each subdir of overlay/themes is a complete theme dir; just rsync them.
|
||||||
|
cp -a "$src"/. "$TARGET/usr/share/themes/" 2>/dev/null || true
|
||||||
|
log "themes deployed -> /usr/share/themes/"
|
||||||
|
}
|
||||||
|
|
||||||
|
_deploy_icons() {
|
||||||
|
local TARGET="$1"
|
||||||
|
local src="$OVERLAY_SRC/icons"
|
||||||
|
[[ -d "$src" ]] || { log "no icons overlay; skipping"; return 0; }
|
||||||
|
|
||||||
|
install -d -m 0755 "$TARGET/usr/share/icons"
|
||||||
|
cp -a "$src"/. "$TARGET/usr/share/icons/" 2>/dev/null || true
|
||||||
|
# Refresh icon cache (best-effort).
|
||||||
|
for d in "$TARGET"/usr/share/icons/*/; do
|
||||||
|
[[ -d "$d" ]] || continue
|
||||||
|
run_chroot "gtk-update-icon-cache -f -t /usr/share/icons/$(basename "$d") 2>/dev/null || true"
|
||||||
|
done
|
||||||
|
log "icons deployed -> /usr/share/icons/"
|
||||||
|
}
|
||||||
|
|
||||||
|
_deploy_wallpapers() {
|
||||||
|
local TARGET="$1"
|
||||||
|
local src="$OVERLAY_SRC/wallpapers"
|
||||||
|
[[ -d "$src" ]] || { log "no wallpapers overlay; skipping"; return 0; }
|
||||||
|
|
||||||
|
local dst="$TARGET/usr/share/backgrounds/void-installer"
|
||||||
|
install -d -m 0755 "$dst"
|
||||||
|
cp -a "$src"/. "$dst/"
|
||||||
|
chmod 0644 "$dst"/* 2>/dev/null || true
|
||||||
|
log "wallpapers -> /usr/share/backgrounds/void-installer/"
|
||||||
|
}
|
||||||
|
|
||||||
|
_deploy_user_dotfiles() {
|
||||||
|
local TARGET="$1"
|
||||||
|
local src="$OVERLAY_SRC/skel"
|
||||||
|
[[ -d "$src" ]] || { log "no skel overlay; skipping dotfiles"; return 0; }
|
||||||
|
|
||||||
|
local home="$TARGET/home/$USERNAME"
|
||||||
|
install -d -m 0755 "$home"
|
||||||
|
|
||||||
|
# Copy dotfiles, preserving structure. Don't clobber .ssh (already set).
|
||||||
|
(
|
||||||
|
cd "$src" && find . -mindepth 1 -maxdepth 1 \
|
||||||
|
! -name '.ssh' -print0
|
||||||
|
) | while IFS= read -r -d '' rel; do
|
||||||
|
cp -a "$src/$rel" "$home/" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
|
||||||
|
run_chroot "chown -R $USERNAME:$USERNAME /home/$USERNAME"
|
||||||
|
log "dotfiles deployed -> /home/$USERNAME/"
|
||||||
|
}
|
||||||
|
|
||||||
|
_deploy_vscode_config() {
|
||||||
|
local TARGET="$1"
|
||||||
|
local src="$OVERLAY_SRC/vscode-user"
|
||||||
|
[[ -d "$src" ]] || { log "no vscode-user overlay; skipping"; return 0; }
|
||||||
|
|
||||||
|
local dst="$TARGET/home/$USERNAME/.config/Code/User"
|
||||||
|
install -d -m 0755 "$dst"
|
||||||
|
cp -a "$src"/. "$dst/"
|
||||||
|
run_chroot "chown -R $USERNAME:$USERNAME /home/$USERNAME/.config"
|
||||||
|
log "vscode user config -> ~/.config/Code/User/"
|
||||||
|
}
|
||||||
|
|
||||||
|
_deploy_first_login() {
|
||||||
|
local TARGET="$1"
|
||||||
|
local src="$OVERLAY_SRC/first-login.sh"
|
||||||
|
local ext_list="$OVERLAY_SRC/vscode-extensions.txt"
|
||||||
|
|
||||||
|
install -d -m 0755 "$TARGET/usr/local/libexec"
|
||||||
|
[[ -r "$src" ]] && {
|
||||||
|
install -m 0755 "$src" "$TARGET/usr/local/libexec/first-login.sh"
|
||||||
|
log "first-login script staged"
|
||||||
|
}
|
||||||
|
[[ -r "$ext_list" ]] && {
|
||||||
|
install -m 0644 "$ext_list" "$TARGET/etc/installer-vscode-extensions.txt"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Inject one-shot trigger into ~/.bash_profile (appended; idempotent guard
|
||||||
|
# in the script itself via /var/lib/first-login.done).
|
||||||
|
local home="$TARGET/home/$USERNAME"
|
||||||
|
install -d -m 0755 "$home"
|
||||||
|
if [[ -x "$TARGET/usr/local/libexec/first-login.sh" ]]; then
|
||||||
|
if ! grep -q "first-login.sh" "$home/.bash_profile" 2>/dev/null; then
|
||||||
|
cat >> "$home/.bash_profile" <<'EOF'
|
||||||
|
|
||||||
|
# Auto-run user environment setup on first interactive login.
|
||||||
|
if [[ -z "$_FIRST_LOGIN_RAN" && -x /usr/local/libexec/first-login.sh \
|
||||||
|
&& ! -f "$HOME/.first-login-done" ]]; then
|
||||||
|
export _FIRST_LOGIN_RAN=1
|
||||||
|
/usr/local/libexec/first-login.sh 2>&1 | tee -a "$HOME/.first-login.log"
|
||||||
|
fi
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
run_chroot "chown $USERNAME:$USERNAME /home/$USERNAME/.bash_profile"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Also autostart it from the desktop session so GUI-only users get it.
|
||||||
|
if [[ -x "$TARGET/usr/local/libexec/first-login.sh" ]]; then
|
||||||
|
local autostart="$TARGET/etc/xdg/autostart"
|
||||||
|
install -d -m 0755 "$autostart"
|
||||||
|
cat > "$autostart/void-installer-first-login.desktop" <<'EOF'
|
||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=Void Installer First-Login Setup
|
||||||
|
Exec=/usr/local/libexec/first-login.sh
|
||||||
|
NoDisplay=true
|
||||||
|
X-GNOME-Autostart-enabled=true
|
||||||
|
OnlyShowIn=X-Cinnamon;GNOME;XFCE;KDE;
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure ~/.local/bin is on PATH for every shell (login + non-login).
|
||||||
|
install -d -m 0755 "$TARGET/etc/profile.d"
|
||||||
|
cat > "$TARGET/etc/profile.d/local-bin.sh" <<'EOF'
|
||||||
|
# Prepend the per-user bin dir so first-login symlinks (claude, node, npm)
|
||||||
|
# are visible to every interactive shell.
|
||||||
|
case ":$PATH:" in
|
||||||
|
*":$HOME/.local/bin:"*) ;;
|
||||||
|
*) export PATH="$HOME/.local/bin:$PATH" ;;
|
||||||
|
esac
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Claude Code config (auth tokens) bundled from host overlay.
|
||||||
|
local claude_src="$OVERLAY_SRC/claude"
|
||||||
|
if [[ -d "$claude_src" ]]; then
|
||||||
|
local cdst="$TARGET/home/$USERNAME/.claude"
|
||||||
|
install -d -m 0700 "$cdst"
|
||||||
|
cp -a "$claude_src"/. "$cdst/"
|
||||||
|
log "claude config -> ~/.claude/"
|
||||||
|
fi
|
||||||
|
if [[ -r "$OVERLAY_SRC/claude.json" ]]; then
|
||||||
|
install -m 0600 "$OVERLAY_SRC/claude.json" "$TARGET/home/$USERNAME/.claude.json"
|
||||||
|
fi
|
||||||
|
run_chroot "chown -R $USERNAME:$USERNAME /home/$USERNAME/.claude /home/$USERNAME/.claude.json 2>/dev/null || true"
|
||||||
|
}
|
||||||
|
|
||||||
|
_write_dconf_defaults() {
|
||||||
|
local TARGET="$1"
|
||||||
|
install -d -m 0755 "$TARGET/etc/dconf/db/local.d" \
|
||||||
|
"$TARGET/etc/dconf/profile"
|
||||||
|
|
||||||
|
cat > "$TARGET/etc/dconf/profile/user" <<'EOF'
|
||||||
|
user-db:user
|
||||||
|
system-db:local
|
||||||
|
EOF
|
||||||
|
|
||||||
|
local wallpaper="${INITIAL_WALLPAPER:-pxfuel.jpg}"
|
||||||
|
cat > "$TARGET/etc/dconf/db/local.d/00-cinnamon" <<EOF
|
||||||
|
# Cinnamon system-wide defaults — generated by void-installer.
|
||||||
|
[org/cinnamon/desktop/interface]
|
||||||
|
gtk-theme='${GTK_THEME}'
|
||||||
|
icon-theme='${ICON_THEME}'
|
||||||
|
cursor-theme='${CURSOR_THEME}'
|
||||||
|
|
||||||
|
[org/cinnamon/desktop/wm/preferences]
|
||||||
|
theme='${GTK_THEME}'
|
||||||
|
|
||||||
|
[org/cinnamon/theme]
|
||||||
|
name='${GTK_THEME}'
|
||||||
|
|
||||||
|
[org/cinnamon/desktop/background]
|
||||||
|
picture-uri='file:///usr/share/backgrounds/void-installer/${wallpaper}'
|
||||||
|
picture-options='zoom'
|
||||||
|
|
||||||
|
[org/gnome/desktop/background]
|
||||||
|
picture-uri='file:///usr/share/backgrounds/void-installer/${wallpaper}'
|
||||||
|
picture-options='zoom'
|
||||||
|
|
||||||
|
[org/gnome/desktop/input-sources]
|
||||||
|
xkb-options=['']
|
||||||
|
sources=[('xkb', 'ch+fr')]
|
||||||
|
|
||||||
|
[org/cinnamon/desktop/keybindings]
|
||||||
|
custom-list=['custom0', 'custom1']
|
||||||
|
|
||||||
|
[org/cinnamon/desktop/keybindings/custom-keybindings/custom0]
|
||||||
|
name='Open Terminal'
|
||||||
|
command='${DEFAULT_TERMINAL:-alacritty}'
|
||||||
|
binding=['<Primary><Alt>t']
|
||||||
|
|
||||||
|
[org/cinnamon/desktop/keybindings/custom-keybindings/custom1]
|
||||||
|
name='Flameshot'
|
||||||
|
command='flameshot gui'
|
||||||
|
binding=['Print']
|
||||||
|
|
||||||
|
[org/cinnamon/desktop/keybindings/media-keys]
|
||||||
|
screenshot=['']
|
||||||
|
area-screenshot=['']
|
||||||
|
window-screenshot=['']
|
||||||
|
|
||||||
|
[org/cinnamon]
|
||||||
|
enabled-applets=['panel1:left:0:menu@cinnamon.org', 'panel1:left:1:show-desktop@cinnamon.org', 'panel1:left:2:grouped-window-list@cinnamon.org', 'panel1:right:0:systray@cinnamon.org', 'panel1:right:1:xapp-status@cinnamon.org', 'panel1:right:2:notifications@cinnamon.org', 'panel1:right:3:printers@cinnamon.org', 'panel1:right:4:removable-drives@cinnamon.org', 'panel1:right:5:keyboard@cinnamon.org', 'panel1:right:6:favorites@cinnamon.org', 'panel1:right:7:network@cinnamon.org', 'panel1:right:8:sound@cinnamon.org', 'panel1:right:9:power@cinnamon.org', 'panel1:right:10:calendar@cinnamon.org', 'panel1:right:11:user@cinnamon.org']
|
||||||
|
|
||||||
|
[org/cinnamon/desktop/default-applications/terminal]
|
||||||
|
exec='${DEFAULT_TERMINAL:-alacritty}'
|
||||||
|
exec-arg='-e'
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Compile dconf db inside the chroot.
|
||||||
|
run_chroot "dconf update 2>/dev/null || true"
|
||||||
|
log "dconf cinnamon defaults written"
|
||||||
|
}
|
||||||
|
|
||||||
|
_set_default_terminal() {
|
||||||
|
local TARGET="$1"
|
||||||
|
local term="${DEFAULT_TERMINAL:-alacritty}"
|
||||||
|
|
||||||
|
# x-terminal-emulator-style alternative — Void doesn't ship update-alternatives
|
||||||
|
# for terminals, so just symlink in /usr/local/bin.
|
||||||
|
if [[ -x "$TARGET/usr/bin/$term" ]]; then
|
||||||
|
ln -sf "/usr/bin/$term" "$TARGET/usr/local/bin/x-terminal-emulator"
|
||||||
|
log "default terminal set to $term"
|
||||||
|
else
|
||||||
|
warn "$term not found in target; skipping default-terminal symlink"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Persistent X keymap (works for non-cinnamon login too).
|
||||||
|
install -d -m 0755 "$TARGET/etc/X11/xorg.conf.d"
|
||||||
|
cat > "$TARGET/etc/X11/xorg.conf.d/00-keyboard.conf" <<EOF
|
||||||
|
Section "InputClass"
|
||||||
|
Identifier "system-keyboard"
|
||||||
|
MatchIsKeyboard "on"
|
||||||
|
Option "XkbLayout" "ch"
|
||||||
|
Option "XkbVariant" "fr"
|
||||||
|
EndSection
|
||||||
|
EOF
|
||||||
|
log "X11 keymap pinned: ch(fr)"
|
||||||
|
}
|
||||||
|
|
||||||
|
_install_gestures() {
|
||||||
|
local TARGET="$1"
|
||||||
|
# System-wide default libinput-gestures config (workspace switching, overview, etc.)
|
||||||
|
install -d -m 0755 "$TARGET/etc/skel/.config"
|
||||||
|
cat > "$TARGET/etc/skel/.config/libinput-gestures.conf" <<'EOF'
|
||||||
|
# void-installer defaults
|
||||||
|
gesture swipe up 3 wmctrl -k on
|
||||||
|
gesture swipe down 3 wmctrl -k off
|
||||||
|
gesture swipe left 3 xdotool key super+Right
|
||||||
|
gesture swipe right 3 xdotool key super+Left
|
||||||
|
gesture swipe left 4 xdotool key ctrl+alt+Right
|
||||||
|
gesture swipe right 4 xdotool key ctrl+alt+Left
|
||||||
|
gesture swipe up 4 xdotool key super
|
||||||
|
gesture swipe down 4 xdotool key super+d
|
||||||
|
gesture pinch in xdotool key ctrl+minus
|
||||||
|
gesture pinch out xdotool key ctrl+plus
|
||||||
|
EOF
|
||||||
|
# Ensure user is in 'input' group (libinput-gestures needs /dev/input/event*).
|
||||||
|
run_chroot "groupadd -f input; usermod -aG input $USERNAME" || true
|
||||||
|
|
||||||
|
# Autostart for cinnamon session.
|
||||||
|
install -d -m 0755 "$TARGET/etc/skel/.config/autostart"
|
||||||
|
cat > "$TARGET/etc/skel/.config/autostart/libinput-gestures.desktop" <<'EOF'
|
||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=Libinput Gestures
|
||||||
|
Exec=libinput-gestures-setup start
|
||||||
|
X-GNOME-Autostart-enabled=true
|
||||||
|
NoDisplay=false
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Mirror into existing user's home (skel only applies to NEW users).
|
||||||
|
if id "$USERNAME" >/dev/null 2>&1 || [[ -d "$TARGET/home/$USERNAME" ]]; then
|
||||||
|
install -d -m 0755 "$TARGET/home/$USERNAME/.config/autostart"
|
||||||
|
cp -a "$TARGET/etc/skel/.config/libinput-gestures.conf" \
|
||||||
|
"$TARGET/home/$USERNAME/.config/libinput-gestures.conf" 2>/dev/null || true
|
||||||
|
cp -a "$TARGET/etc/skel/.config/autostart/libinput-gestures.desktop" \
|
||||||
|
"$TARGET/home/$USERNAME/.config/autostart/" 2>/dev/null || true
|
||||||
|
run_chroot "chown -R $USERNAME:$USERNAME /home/$USERNAME/.config" || true
|
||||||
|
fi
|
||||||
|
log "trackpad gestures configured (libinput-gestures)"
|
||||||
|
}
|
||||||
|
|
||||||
|
_install_snapshot_hook() {
|
||||||
|
local TARGET="$1"
|
||||||
|
# Pre-upgrade btrfs snapshot via xbps-install hook.
|
||||||
|
install -d -m 0755 "$TARGET/etc/xbps.d"
|
||||||
|
install -d -m 0755 "$TARGET/usr/local/sbin"
|
||||||
|
|
||||||
|
cat > "$TARGET/usr/local/sbin/xbps-pre-upgrade-snapshot.sh" <<'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
# Take a read-only btrfs snapshot of @ before an xbps upgrade/install.
|
||||||
|
# Snapshots live in /.snapshots/auto-YYYYmmdd-HHMMSS so timeshift can manage them.
|
||||||
|
set -e
|
||||||
|
ROOT_SUBVOL=/run/btrfs-root/@
|
||||||
|
SNAP_DIR=/.snapshots
|
||||||
|
[ -d "$SNAP_DIR" ] || mkdir -p "$SNAP_DIR"
|
||||||
|
TS=$(date +%Y%m%d-%H%M%S)
|
||||||
|
NAME="auto-pre-xbps-$TS"
|
||||||
|
# Mount the toplevel of the btrfs FS once so we can snapshot @.
|
||||||
|
ROOT_DEV=$(findmnt -no SOURCE /)
|
||||||
|
mkdir -p /run/btrfs-root
|
||||||
|
if ! mountpoint -q /run/btrfs-root; then
|
||||||
|
mount -o subvolid=5 "$ROOT_DEV" /run/btrfs-root 2>/dev/null || exit 0
|
||||||
|
fi
|
||||||
|
btrfs subvolume snapshot -r /run/btrfs-root/@ "/run/btrfs-root/.snapshots/$NAME" 2>/dev/null \
|
||||||
|
|| mkdir -p /run/btrfs-root/.snapshots && \
|
||||||
|
btrfs subvolume snapshot -r /run/btrfs-root/@ "/run/btrfs-root/.snapshots/$NAME"
|
||||||
|
echo "[snapshot] created $NAME"
|
||||||
|
EOF
|
||||||
|
chmod 0755 "$TARGET/usr/local/sbin/xbps-pre-upgrade-snapshot.sh"
|
||||||
|
|
||||||
|
# xbps doesn't have native pre-hooks; wrap xbps-install via /usr/local/bin shim.
|
||||||
|
cat > "$TARGET/usr/local/bin/xbps-install" <<'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
# Wrapper that snapshots @ before any state-changing xbps-install run.
|
||||||
|
case " $* " in
|
||||||
|
*" -S "*|*" --sync "*|*" -u "*|*" --update "*) /usr/local/sbin/xbps-pre-upgrade-snapshot.sh || true ;;
|
||||||
|
*) [ -n "$1" ] && /usr/local/sbin/xbps-pre-upgrade-snapshot.sh || true ;;
|
||||||
|
esac
|
||||||
|
exec /usr/bin/xbps-install "$@"
|
||||||
|
EOF
|
||||||
|
chmod 0755 "$TARGET/usr/local/bin/xbps-install"
|
||||||
|
log "btrfs pre-upgrade snapshot hook installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
_install_upgrade_applet() {
|
||||||
|
local TARGET="$1"
|
||||||
|
install -d -m 0755 "$TARGET/usr/local/bin" \
|
||||||
|
"$TARGET/usr/share/applications" \
|
||||||
|
"$TARGET/etc/skel/.config/autostart"
|
||||||
|
|
||||||
|
# Tiny GUI wrapper: uses zenity if available, else xterm.
|
||||||
|
cat > "$TARGET/usr/local/bin/void-upgrade-gui" <<'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
# Check for upgrades, prompt user, run xbps-install -Su (with snapshot via wrapper).
|
||||||
|
set -e
|
||||||
|
PENDING=$(xbps-install -Sun 2>/dev/null | wc -l)
|
||||||
|
if [ "$PENDING" -eq 0 ]; then
|
||||||
|
notify-send "Void Upgrade" "System is up to date." 2>/dev/null || true
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
MSG="There are $PENDING package updates available.\nRun system upgrade now?\n(A btrfs snapshot will be taken automatically.)"
|
||||||
|
if command -v zenity >/dev/null 2>&1; then
|
||||||
|
zenity --question --title="Void Upgrade" --text="$MSG" || exit 0
|
||||||
|
fi
|
||||||
|
pkexec xbps-install -Suy 2>&1 | tee /tmp/void-upgrade.log
|
||||||
|
notify-send "Void Upgrade" "Upgrade finished. See /tmp/void-upgrade.log" 2>/dev/null || true
|
||||||
|
EOF
|
||||||
|
chmod 0755 "$TARGET/usr/local/bin/void-upgrade-gui"
|
||||||
|
|
||||||
|
cat > "$TARGET/usr/share/applications/void-upgrade.desktop" <<'EOF'
|
||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=Void Upgrade
|
||||||
|
Comment=Check and apply system upgrades (with btrfs snapshot)
|
||||||
|
Exec=void-upgrade-gui
|
||||||
|
Icon=system-software-update
|
||||||
|
Categories=System;PackageManager;
|
||||||
|
Terminal=false
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Daily check on login (autostart, runs once per day).
|
||||||
|
cat > "$TARGET/etc/skel/.config/autostart/void-upgrade-check.desktop" <<'EOF'
|
||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=Void Upgrade Check
|
||||||
|
Exec=sh -c '[ "$(date +%F)" != "$(cat ~/.cache/void-upgrade.last 2>/dev/null)" ] && void-upgrade-gui && date +%F > ~/.cache/void-upgrade.last'
|
||||||
|
X-GNOME-Autostart-enabled=true
|
||||||
|
NoDisplay=true
|
||||||
|
EOF
|
||||||
|
cp -a "$TARGET/etc/skel/.config/autostart/void-upgrade-check.desktop" \
|
||||||
|
"$TARGET/home/$USERNAME/.config/autostart/" 2>/dev/null || true
|
||||||
|
run_chroot "chown -R $USERNAME:$USERNAME /home/$USERNAME/.config" || true
|
||||||
|
log "void-upgrade GUI applet installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
_install_nemo_actions() {
|
||||||
|
local TARGET="$1"
|
||||||
|
install -d -m 0755 "$TARGET/usr/share/nemo/actions"
|
||||||
|
cat > "$TARGET/usr/share/nemo/actions/open-vscode.nemo_action" <<'EOF'
|
||||||
|
[Nemo Action]
|
||||||
|
Active=true
|
||||||
|
Name=Open with VS Code
|
||||||
|
Comment=Open the selected file or folder in Visual Studio Code
|
||||||
|
Exec=code %F
|
||||||
|
Icon-Name=com.visualstudio.code
|
||||||
|
Selection=Any
|
||||||
|
Extensions=any;
|
||||||
|
EOF
|
||||||
|
log "Nemo 'Open with VS Code' action installed"
|
||||||
|
}
|
||||||
56
installer/lib/grub.sh
Executable file
56
installer/lib/grub.sh
Executable file
@@ -0,0 +1,56 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# GRUB UEFI install with dual-boot (Windows on /dev/nvme0n1p3 via os-prober).
|
||||||
|
|
||||||
|
# shellcheck source=common.sh
|
||||||
|
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
||||||
|
|
||||||
|
install_grub() {
|
||||||
|
step "Installing GRUB (UEFI, bootloader-id=$BOOTLOADER_ID)"
|
||||||
|
|
||||||
|
local TARGET="${TARGET:-/mnt}"
|
||||||
|
|
||||||
|
if ! is_uefi; then
|
||||||
|
die "non-UEFI boot detected; this installer only supports UEFI"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Configure /etc/default/grub
|
||||||
|
cat > "$TARGET/etc/default/grub" <<'GRUBEOF'
|
||||||
|
GRUB_DEFAULT=0
|
||||||
|
GRUB_TIMEOUT=5
|
||||||
|
GRUB_DISTRIBUTOR="Void"
|
||||||
|
GRUB_CMDLINE_LINUX_DEFAULT="loglevel=4 nvidia-drm.modeset=1"
|
||||||
|
GRUB_CMDLINE_LINUX=""
|
||||||
|
GRUB_DISABLE_OS_PROBER=false
|
||||||
|
GRUB_TERMINAL_OUTPUT="gfxterm"
|
||||||
|
GRUB_GFXMODE=auto
|
||||||
|
GRUBEOF
|
||||||
|
|
||||||
|
# Make sure os-prober can see the NTFS partitions to enumerate Windows.
|
||||||
|
run_chroot "modprobe efivarfs 2>/dev/null || true"
|
||||||
|
run_chroot "xbps-install -y os-prober ntfs-3g >/dev/null 2>&1 || true"
|
||||||
|
|
||||||
|
run_chroot "grub-install \
|
||||||
|
--target=x86_64-efi \
|
||||||
|
--efi-directory=/boot/efi \
|
||||||
|
--bootloader-id='$BOOTLOADER_ID' \
|
||||||
|
--recheck"
|
||||||
|
|
||||||
|
# Ensure os-prober actually runs (some hosts skip it without this).
|
||||||
|
mkdir -p "$TARGET/etc/grub.d"
|
||||||
|
|
||||||
|
# Generate config
|
||||||
|
run_chroot "grub-mkconfig -o /boot/grub/grub.cfg"
|
||||||
|
|
||||||
|
# Verify Windows entry was found (best-effort, non-fatal in test mode)
|
||||||
|
if grep -q -i 'windows\|microsoft' "$TARGET/boot/grub/grub.cfg"; then
|
||||||
|
ok "Windows boot entry detected in grub.cfg"
|
||||||
|
else
|
||||||
|
if [[ "${TEST_MODE:-0}" == "1" ]]; then
|
||||||
|
log "no Windows entry (expected in test mode)"
|
||||||
|
else
|
||||||
|
warn "no Windows entry in grub.cfg — os-prober may have failed; you can re-run grub-mkconfig later"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
ok "GRUB installed"
|
||||||
|
}
|
||||||
130
installer/lib/partition.sh
Executable file
130
installer/lib/partition.sh
Executable file
@@ -0,0 +1,130 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Partition / filesystem setup.
|
||||||
|
# - Reformats ROOT_PART as btrfs with subvolumes
|
||||||
|
# - Mounts everything under TARGET (default /mnt)
|
||||||
|
# - Mounts existing EFI partition read-write at $TARGET/boot/efi (NEVER reformatted)
|
||||||
|
|
||||||
|
# shellcheck source=common.sh
|
||||||
|
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
||||||
|
|
||||||
|
setup_filesystems() {
|
||||||
|
step "Filesystem setup on $ROOT_PART (btrfs) + EFI share $EFI_PART"
|
||||||
|
|
||||||
|
[[ -b "$ROOT_PART" ]] || die "ROOT_PART $ROOT_PART not a block device"
|
||||||
|
[[ -b "$EFI_PART" ]] || die "EFI_PART $EFI_PART not a block device"
|
||||||
|
|
||||||
|
# Sanity: EFI must already be vfat. We never format it.
|
||||||
|
local efi_fs
|
||||||
|
efi_fs=$(lsblk -no FSTYPE "$EFI_PART")
|
||||||
|
[[ "$efi_fs" == "vfat" ]] \
|
||||||
|
|| die "EFI partition $EFI_PART is '$efi_fs', expected vfat — refusing"
|
||||||
|
|
||||||
|
# Force unmount anything currently using ROOT_PART (live ISO, prior run).
|
||||||
|
local mp
|
||||||
|
while read -r mp; do
|
||||||
|
[[ -n "$mp" ]] && umount -R "$mp" 2>/dev/null || true
|
||||||
|
done < <(lsblk -nro MOUNTPOINTS "$ROOT_PART" 2>/dev/null | grep -v '^$' || true)
|
||||||
|
umount -R "$ROOT_PART" 2>/dev/null || true
|
||||||
|
swapoff -a 2>/dev/null || true
|
||||||
|
|
||||||
|
# If the live ISO's initramfs already auto-discovered a btrfs on this
|
||||||
|
# partition (mklive runs `btrfs device scan` early), the device is
|
||||||
|
# registered with the in-kernel btrfs module and any later mkfs sees EBUSY.
|
||||||
|
# Forget the registration BEFORE wiping so it doesn't get re-claimed.
|
||||||
|
btrfs device scan --forget 2>/dev/null || true
|
||||||
|
|
||||||
|
log "wiping filesystem signatures on $ROOT_PART"
|
||||||
|
wipefs -af "$ROOT_PART" 2>/dev/null || true
|
||||||
|
# Zero the first 64 MiB and last 4 MiB to obliterate btrfs primary AND
|
||||||
|
# backup superblocks. Without this, a half-written btrfs from an
|
||||||
|
# interrupted prior run can be auto-mounted between commands.
|
||||||
|
dd if=/dev/zero of="$ROOT_PART" bs=1M count=64 conv=fsync 2>/dev/null || true
|
||||||
|
local part_size_b
|
||||||
|
part_size_b=$(blockdev --getsize64 "$ROOT_PART" 2>/dev/null || echo 0)
|
||||||
|
if [[ "$part_size_b" -gt $((8 * 1024 * 1024)) ]]; then
|
||||||
|
local seek_mb=$(( part_size_b / 1024 / 1024 - 4 ))
|
||||||
|
dd if=/dev/zero of="$ROOT_PART" bs=1M count=4 seek="$seek_mb" \
|
||||||
|
conv=fsync 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
sync
|
||||||
|
udevadm settle 2>/dev/null || true
|
||||||
|
btrfs device scan --forget 2>/dev/null || true
|
||||||
|
|
||||||
|
log "lsblk view of $ROOT_PART:"
|
||||||
|
lsblk -fno NAME,FSTYPE,LABEL,MOUNTPOINTS "$ROOT_PART" 2>&1 \
|
||||||
|
| while read -r l; do log " $l"; done
|
||||||
|
|
||||||
|
log "creating btrfs on $ROOT_PART"
|
||||||
|
# Stop udev from auto-claiming the device mid-format (race that causes
|
||||||
|
# mkfs.btrfs to fail its final O_EXCL reopen with EBUSY even though the
|
||||||
|
# superblock was written successfully).
|
||||||
|
udevadm control --stop-exec-queue 2>/dev/null || true
|
||||||
|
local mkfs_rc=0
|
||||||
|
mkfs.btrfs -f -L void "$ROOT_PART" || mkfs_rc=$?
|
||||||
|
sync
|
||||||
|
udevadm control --start-exec-queue 2>/dev/null || true
|
||||||
|
udevadm settle 2>/dev/null || true
|
||||||
|
if [[ $mkfs_rc -ne 0 ]]; then
|
||||||
|
# Tolerate nonzero exit if a valid btrfs was in fact written
|
||||||
|
# (EBUSY-on-close race observed with btrfs-progs 6.11).
|
||||||
|
local actual_fs
|
||||||
|
actual_fs=$(blkid -o value -s TYPE "$ROOT_PART" 2>/dev/null || echo "")
|
||||||
|
if [[ "$actual_fs" != "btrfs" ]]; then
|
||||||
|
die "mkfs.btrfs failed on $ROOT_PART (rc=$mkfs_rc, fs='$actual_fs')"
|
||||||
|
fi
|
||||||
|
log "mkfs.btrfs exited rc=$mkfs_rc but blkid reports btrfs — continuing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local TARGET="${TARGET:-/mnt}"
|
||||||
|
mkdir -p "$TARGET"
|
||||||
|
mount -o "${BTRFS_MOUNT_OPTS}" "$ROOT_PART" "$TARGET"
|
||||||
|
|
||||||
|
log "creating subvolumes: ${BTRFS_SUBVOLS[*]%%:*}"
|
||||||
|
local entry sv
|
||||||
|
for entry in "${BTRFS_SUBVOLS[@]}"; do
|
||||||
|
sv="${entry%%:*}"
|
||||||
|
# Idempotent: skip if a stale subvolume already exists from a
|
||||||
|
# previous interrupted run (mkfs -f recreates the FS but on a fresh
|
||||||
|
# mount the directory listing should be empty; this is defensive).
|
||||||
|
if [[ -e "$TARGET/$sv" ]]; then
|
||||||
|
log " subvolume $sv already present — skipping"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
btrfs subvolume create "$TARGET/$sv"
|
||||||
|
done
|
||||||
|
umount "$TARGET"
|
||||||
|
|
||||||
|
log "remounting subvolumes"
|
||||||
|
# First mount the root subvolume (must be the first entry, conventionally @).
|
||||||
|
local root_entry="${BTRFS_SUBVOLS[0]}"
|
||||||
|
local root_sv="${root_entry%%:*}"
|
||||||
|
mount -o "${BTRFS_MOUNT_OPTS},subvol=${root_sv}" "$ROOT_PART" "$TARGET"
|
||||||
|
|
||||||
|
# Pre-create mountpoints + EFI dir.
|
||||||
|
mkdir -p "$TARGET/boot/efi"
|
||||||
|
for entry in "${BTRFS_SUBVOLS[@]:1}"; do
|
||||||
|
local mp="${entry##*:}"
|
||||||
|
mkdir -p "$TARGET$mp"
|
||||||
|
done
|
||||||
|
|
||||||
|
for entry in "${BTRFS_SUBVOLS[@]:1}"; do
|
||||||
|
sv="${entry%%:*}"
|
||||||
|
local mp="${entry##*:}"
|
||||||
|
mount -o "${BTRFS_MOUNT_OPTS},subvol=${sv}" "$ROOT_PART" "$TARGET$mp"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Mount shared EFI WITHOUT formatting.
|
||||||
|
mount "$EFI_PART" "$TARGET/boot/efi"
|
||||||
|
|
||||||
|
# Make sure we're not about to clobber Windows' EFI loader.
|
||||||
|
if [[ -d "$TARGET/boot/efi/EFI/Microsoft" ]]; then
|
||||||
|
log "Windows EFI files detected — will preserve EFI/Microsoft/* untouched"
|
||||||
|
else
|
||||||
|
warn "no EFI/Microsoft dir found on $EFI_PART — proceeding anyway"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ok "filesystems mounted under $TARGET"
|
||||||
|
findmnt -R "$TARGET" >> "$LOG_FILE"
|
||||||
|
|
||||||
|
export TARGET
|
||||||
|
}
|
||||||
351
installer/lib/postinstall.sh
Executable file
351
installer/lib/postinstall.sh
Executable file
@@ -0,0 +1,351 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Post-install configuration:
|
||||||
|
# - hostname / locale / keyboard / timezone / hwclock
|
||||||
|
# - users (root + moze) + sudo
|
||||||
|
# - services (NetworkManager, lightdm, dbus, polkitd, docker, bluetoothd,
|
||||||
|
# acpid, tlp, sshd[disabled], dhcpcd[disabled in favor of NM])
|
||||||
|
# - zram swap (zramen)
|
||||||
|
# - NVIDIA PRIME render-offload setup
|
||||||
|
# - SSH config copy
|
||||||
|
# - Nix bootstrap (nix-daemon service + first-boot user package install)
|
||||||
|
|
||||||
|
# shellcheck source=common.sh
|
||||||
|
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
||||||
|
|
||||||
|
configure_system() {
|
||||||
|
step "System configuration"
|
||||||
|
local TARGET="${TARGET:-/mnt}"
|
||||||
|
|
||||||
|
# ----- hostname -----
|
||||||
|
echo "$HOSTNAME" > "$TARGET/etc/hostname"
|
||||||
|
cat > "$TARGET/etc/hosts" <<EOF
|
||||||
|
127.0.0.1 localhost
|
||||||
|
::1 localhost
|
||||||
|
127.0.1.1 $HOSTNAME.localdomain $HOSTNAME
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ----- rc.conf -----
|
||||||
|
cat > "$TARGET/etc/rc.conf" <<EOF
|
||||||
|
KEYMAP="$KEYMAP"
|
||||||
|
HARDWARECLOCK="$HARDWARECLOCK"
|
||||||
|
TIMEZONE="$TIMEZONE"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ----- timezone (also create symlink for systems checking it) -----
|
||||||
|
run_chroot "ln -sf /usr/share/zoneinfo/$TIMEZONE /etc/localtime"
|
||||||
|
|
||||||
|
# ----- locales (glibc) -----
|
||||||
|
if [[ -f "$TARGET/etc/default/libc-locales" ]]; then
|
||||||
|
sed -i "s/^#\($LOCALE.*\)/\1/" "$TARGET/etc/default/libc-locales"
|
||||||
|
run_chroot "xbps-reconfigure -f glibc-locales"
|
||||||
|
fi
|
||||||
|
cat > "$TARGET/etc/locale.conf" <<EOF
|
||||||
|
LANG=$LANG
|
||||||
|
LC_ALL=$LANG
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ----- vconsole (for early TTY keymap) -----
|
||||||
|
cat > "$TARGET/etc/vconsole.conf" <<EOF
|
||||||
|
KEYMAP=$KEYMAP
|
||||||
|
EOF
|
||||||
|
|
||||||
|
ok "locale / keymap / tz configured"
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_users() {
|
||||||
|
step "Creating users"
|
||||||
|
local TARGET="${TARGET:-/mnt}"
|
||||||
|
|
||||||
|
# ----- root password (chpasswd via stdin — no shell expansion of $) -----
|
||||||
|
set_chroot_password root "$ROOT_PASSWORD"
|
||||||
|
|
||||||
|
# ----- ensure groups exist -----
|
||||||
|
run_chroot "groupadd -f docker"
|
||||||
|
run_chroot "groupadd -f plugdev"
|
||||||
|
|
||||||
|
# ----- create user (idempotent: tolerate 'already exists', fail on real errors) -----
|
||||||
|
if ! run_chroot "id -u $USERNAME >/dev/null 2>&1"; then
|
||||||
|
run_chroot "useradd -m -u $USER_UID -G $USER_GROUPS -s $DEFAULT_SHELL -c '$USER_FULLNAME' $USERNAME"
|
||||||
|
else
|
||||||
|
log "user $USERNAME already exists — skipping useradd"
|
||||||
|
run_chroot "usermod -G $USER_GROUPS -s $DEFAULT_SHELL $USERNAME"
|
||||||
|
fi
|
||||||
|
set_chroot_password "$USERNAME" "$USER_PASSWORD"
|
||||||
|
|
||||||
|
# ----- sudoers: wheel group -----
|
||||||
|
mkdir -p "$TARGET/etc/sudoers.d"
|
||||||
|
if [[ "${TEST_MODE:-0}" == "1" ]]; then
|
||||||
|
# Test harness needs passwordless sudo to run smoke checks via SSH.
|
||||||
|
cat > "$TARGET/etc/sudoers.d/10-wheel" <<'EOF'
|
||||||
|
%wheel ALL=(ALL:ALL) NOPASSWD: ALL
|
||||||
|
Defaults env_keep += "EDITOR"
|
||||||
|
EOF
|
||||||
|
else
|
||||||
|
cat > "$TARGET/etc/sudoers.d/10-wheel" <<'EOF'
|
||||||
|
%wheel ALL=(ALL:ALL) ALL
|
||||||
|
Defaults env_keep += "EDITOR"
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
chmod 440 "$TARGET/etc/sudoers.d/10-wheel"
|
||||||
|
|
||||||
|
ok "user '$USERNAME' created and added to: $USER_GROUPS"
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_ssh_config() {
|
||||||
|
step "Installing SSH config for $USERNAME"
|
||||||
|
local TARGET="${TARGET:-/mnt}"
|
||||||
|
local src="$SSH_SOURCE_DIR"
|
||||||
|
local dst="$TARGET/home/$USERNAME/$SSH_TARGET_DIR_REL"
|
||||||
|
|
||||||
|
if [[ ! -d "$src" ]]; then
|
||||||
|
warn "no SSH source dir at $src — skipping"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
install -d -m 0700 "$dst"
|
||||||
|
cp -a "$src"/. "$dst/"
|
||||||
|
# Tighten perms.
|
||||||
|
find "$dst" -type d -exec chmod 700 {} +
|
||||||
|
find "$dst" -type f -exec chmod 600 {} +
|
||||||
|
find "$dst" -type f -name '*.pub' -exec chmod 644 {} +
|
||||||
|
[[ -f "$dst/known_hosts" ]] && chmod 644 "$dst/known_hosts"
|
||||||
|
[[ -f "$dst/known_hosts.old" ]] && chmod 644 "$dst/known_hosts.old"
|
||||||
|
[[ -f "$dst/config" ]] && chmod 600 "$dst/config"
|
||||||
|
|
||||||
|
run_chroot "chown -R $USERNAME:$USERNAME /home/$USERNAME/$SSH_TARGET_DIR_REL"
|
||||||
|
ok "SSH config installed at /home/$USERNAME/$SSH_TARGET_DIR_REL"
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_nvidia_prime() {
|
||||||
|
step "Configuring NVIDIA PRIME render-offload"
|
||||||
|
local TARGET="${TARGET:-/mnt}"
|
||||||
|
|
||||||
|
# 1) Xorg config: Intel as primary, NVIDIA as PRIME provider.
|
||||||
|
install -d -m 0755 "$TARGET/etc/X11/xorg.conf.d"
|
||||||
|
cat > "$TARGET/etc/X11/xorg.conf.d/10-intel.conf" <<'EOF'
|
||||||
|
Section "OutputClass"
|
||||||
|
Identifier "intel"
|
||||||
|
MatchDriver "i915"
|
||||||
|
Driver "modesetting"
|
||||||
|
EndSection
|
||||||
|
EOF
|
||||||
|
cat > "$TARGET/etc/X11/xorg.conf.d/20-nvidia.conf" <<'EOF'
|
||||||
|
Section "OutputClass"
|
||||||
|
Identifier "nvidia"
|
||||||
|
MatchDriver "nvidia-drm"
|
||||||
|
Driver "nvidia"
|
||||||
|
Option "AllowEmptyInitialConfiguration"
|
||||||
|
Option "PrimaryGPU" "no"
|
||||||
|
ModulePath "/usr/lib/nvidia/xorg"
|
||||||
|
ModulePath "/usr/lib/xorg/modules"
|
||||||
|
EndSection
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 2) Modules to load early for KMS.
|
||||||
|
install -d -m 0755 "$TARGET/etc/modules-load.d"
|
||||||
|
cat > "$TARGET/etc/modules-load.d/nvidia.conf" <<'EOF'
|
||||||
|
nvidia
|
||||||
|
nvidia_modeset
|
||||||
|
nvidia_uvm
|
||||||
|
nvidia_drm
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 3) Wrapper script + desktop file: run any app on NVIDIA via `prime-run`.
|
||||||
|
install -d -m 0755 "$TARGET/usr/local/bin"
|
||||||
|
cat > "$TARGET/usr/local/bin/prime-run" <<'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
# Run a program on the NVIDIA dGPU via PRIME render offload.
|
||||||
|
exec env __NV_PRIME_RENDER_OFFLOAD=1 \
|
||||||
|
__VK_LAYER_NV_optimus=NVIDIA_only \
|
||||||
|
__GLX_VENDOR_LIBRARY_NAME=nvidia \
|
||||||
|
"$@"
|
||||||
|
EOF
|
||||||
|
chmod 0755 "$TARGET/usr/local/bin/prime-run"
|
||||||
|
|
||||||
|
# 4) Make sure dracut picks up nvidia modules.
|
||||||
|
install -d -m 0755 "$TARGET/etc/dracut.conf.d"
|
||||||
|
cat > "$TARGET/etc/dracut.conf.d/10-nvidia.conf" <<'EOF'
|
||||||
|
add_drivers+=" nvidia nvidia_modeset nvidia_uvm nvidia_drm "
|
||||||
|
EOF
|
||||||
|
|
||||||
|
ok "NVIDIA PRIME offload configured (use 'prime-run <app>')"
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_zram() {
|
||||||
|
[[ "${ZRAM_ENABLE:-yes}" == "yes" ]] || return 0
|
||||||
|
step "Configuring zram (zramen)"
|
||||||
|
local TARGET="${TARGET:-/mnt}"
|
||||||
|
install -d -m 0755 "$TARGET/etc/default"
|
||||||
|
cat > "$TARGET/etc/default/zramen" <<EOF
|
||||||
|
ALGO=zstd
|
||||||
|
PERCENT=$ZRAM_SIZE_PCT
|
||||||
|
PRIORITY=100
|
||||||
|
EOF
|
||||||
|
ok "zram configured at $ZRAM_SIZE_PCT% RAM"
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_nix() {
|
||||||
|
[[ "${ENABLE_NIX:-yes}" == "yes" ]] || return 0
|
||||||
|
step "Configuring nix multi-user (daemon mode)"
|
||||||
|
local TARGET="${TARGET:-/mnt}"
|
||||||
|
# Void's `nix` xbps package installs to /usr and ships a runit service.
|
||||||
|
install -d -m 0755 "$TARGET/etc/nix"
|
||||||
|
cat > "$TARGET/etc/nix/nix.conf" <<'EOF'
|
||||||
|
experimental-features = nix-command flakes
|
||||||
|
build-users-group = nixbld
|
||||||
|
auto-optimise-store = true
|
||||||
|
sandbox = true
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# First-boot script: as $USERNAME, install user packages.
|
||||||
|
install -d -m 0755 "$TARGET/usr/local/libexec"
|
||||||
|
cat > "$TARGET/usr/local/libexec/first-boot-nix.sh" <<EOF
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
mark=/var/lib/first-boot-nix.done
|
||||||
|
[[ -f "\$mark" ]] && exit 0
|
||||||
|
|
||||||
|
# Wait for nix-daemon to be available.
|
||||||
|
# The Void package puts the socket at /var/nix/daemon-socket/nix-daemon.sock
|
||||||
|
# (NOT /nix/var/nix/...).
|
||||||
|
for _ in \$(seq 1 30); do
|
||||||
|
[[ -S /var/nix/daemon-socket/nix-daemon.sock ]] && break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ! -S /var/nix/daemon-socket/nix-daemon.sock ]]; then
|
||||||
|
echo "nix-daemon not available; aborting first-boot nix install" >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
su - $USERNAME -c '
|
||||||
|
set -e
|
||||||
|
. /etc/profile.d/nix.sh 2>/dev/null || true
|
||||||
|
# google-chrome / spotify / discord are unfree -> need allow-unfree + --impure.
|
||||||
|
export NIXPKGS_ALLOW_UNFREE=1
|
||||||
|
nix profile install --impure ${NIX_USER_PACKAGES[*]} || true
|
||||||
|
'
|
||||||
|
|
||||||
|
mkdir -p "\$(dirname "\$mark")"
|
||||||
|
touch "\$mark"
|
||||||
|
EOF
|
||||||
|
chmod 0755 "$TARGET/usr/local/libexec/first-boot-nix.sh"
|
||||||
|
|
||||||
|
# runit one-shot service.
|
||||||
|
install -d -m 0755 "$TARGET/etc/sv/first-boot-nix"
|
||||||
|
cat > "$TARGET/etc/sv/first-boot-nix/run" <<'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
exec 2>&1
|
||||||
|
/usr/local/libexec/first-boot-nix.sh
|
||||||
|
exec chpst -b first-boot-nix pause
|
||||||
|
EOF
|
||||||
|
chmod 0755 "$TARGET/etc/sv/first-boot-nix/run"
|
||||||
|
cat > "$TARGET/etc/sv/first-boot-nix/finish" <<'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
sv down first-boot-nix
|
||||||
|
EOF
|
||||||
|
chmod 0755 "$TARGET/etc/sv/first-boot-nix/finish"
|
||||||
|
|
||||||
|
ok "Nix configured; user packages will install on first boot"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_vscode_real() {
|
||||||
|
# Install official Microsoft VS Code (the real proprietary build), NOT the
|
||||||
|
# `vscode` xbps package which is actually code-oss and ships `code-oss`.
|
||||||
|
step "Installing official Microsoft VS Code"
|
||||||
|
local TARGET="${TARGET:-/mnt}"
|
||||||
|
local url="https://update.code.visualstudio.com/latest/linux-x64/stable"
|
||||||
|
local tmp="$TARGET/tmp/vscode.tar.gz"
|
||||||
|
|
||||||
|
install -d -m 0755 "$TARGET/opt" "$TARGET/usr/local/bin"
|
||||||
|
|
||||||
|
if ! run_chroot "curl -fsSL --retry 3 -o /tmp/vscode.tar.gz '$url'"; then
|
||||||
|
warn "failed to download VS Code; skipping (install manually later)"
|
||||||
|
rm -f "$tmp"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Tarball extracts to VSCode-linux-x64/. Move it to /opt/vscode.
|
||||||
|
rm -rf "$TARGET/opt/vscode"
|
||||||
|
if ! run_chroot "tar -xzf /tmp/vscode.tar.gz -C /opt && mv /opt/VSCode-linux-x64 /opt/vscode"; then
|
||||||
|
warn "failed to extract VS Code tarball; skipping"
|
||||||
|
rm -f "$tmp"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
rm -f "$tmp"
|
||||||
|
|
||||||
|
# `code` shim on PATH.
|
||||||
|
ln -sf /opt/vscode/bin/code "$TARGET/usr/local/bin/code"
|
||||||
|
|
||||||
|
# Desktop entry so it shows up in the Cinnamon menu.
|
||||||
|
install -d -m 0755 "$TARGET/usr/local/share/applications"
|
||||||
|
cat > "$TARGET/usr/local/share/applications/code.desktop" <<'EOF'
|
||||||
|
[Desktop Entry]
|
||||||
|
Name=Visual Studio Code
|
||||||
|
Comment=Code Editing. Redefined.
|
||||||
|
GenericName=Text Editor
|
||||||
|
Exec=/opt/vscode/bin/code %F
|
||||||
|
Icon=/opt/vscode/resources/app/resources/linux/code.png
|
||||||
|
Type=Application
|
||||||
|
StartupNotify=false
|
||||||
|
StartupWMClass=Code
|
||||||
|
Categories=TextEditor;Development;IDE;
|
||||||
|
MimeType=text/plain;inode/directory;application/x-code-workspace;
|
||||||
|
Actions=new-empty-window;
|
||||||
|
Keywords=vscode;
|
||||||
|
|
||||||
|
[Desktop Action new-empty-window]
|
||||||
|
Name=New Empty Window
|
||||||
|
Exec=/opt/vscode/bin/code --new-window %F
|
||||||
|
Icon=/opt/vscode/resources/app/resources/linux/code.png
|
||||||
|
EOF
|
||||||
|
|
||||||
|
ok "VS Code installed at /opt/vscode (use 'code' on PATH)"
|
||||||
|
}
|
||||||
|
|
||||||
|
enable_services() {
|
||||||
|
step "Enabling services (runit)"
|
||||||
|
local TARGET="${TARGET:-/mnt}"
|
||||||
|
local svdir="$TARGET/etc/runit/runsvdir/default"
|
||||||
|
install -d -m 0755 "$svdir"
|
||||||
|
|
||||||
|
local svc
|
||||||
|
local enabled=(
|
||||||
|
dbus
|
||||||
|
NetworkManager
|
||||||
|
lightdm
|
||||||
|
polkitd
|
||||||
|
docker
|
||||||
|
bluetoothd
|
||||||
|
acpid
|
||||||
|
tlp
|
||||||
|
elogind
|
||||||
|
chronyd
|
||||||
|
nix-daemon
|
||||||
|
first-boot-nix
|
||||||
|
zramen
|
||||||
|
cupsd
|
||||||
|
cups-browsed
|
||||||
|
)
|
||||||
|
[[ "${SSHD_ENABLE:-no}" == "yes" ]] && enabled+=(sshd)
|
||||||
|
|
||||||
|
for svc in "${enabled[@]}"; do
|
||||||
|
if [[ -d "$TARGET/etc/sv/$svc" ]]; then
|
||||||
|
ln -sf "/etc/sv/$svc" "$svdir/$svc"
|
||||||
|
log "enabled $svc"
|
||||||
|
else
|
||||||
|
warn "no service dir /etc/sv/$svc — skipping"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Disable dhcpcd if it's there (NetworkManager handles it).
|
||||||
|
rm -f "$svdir/dhcpcd" 2>/dev/null || true
|
||||||
|
|
||||||
|
ok "services enabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
reconfigure_all() {
|
||||||
|
step "Reconfiguring all packages (initramfs + grub artifacts)"
|
||||||
|
run_chroot "xbps-reconfigure -fa"
|
||||||
|
ok "reconfigure complete"
|
||||||
|
}
|
||||||
53
installer/lib/profiles.sh
Normal file
53
installer/lib/profiles.sh
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Profile loading. PROFILE env var (default: stable-cinnamon) selects which
|
||||||
|
# config/profiles/<name>/profile.conf is sourced and which packages.list is
|
||||||
|
# used. Every variable defined in profile.conf overrides install.conf.
|
||||||
|
|
||||||
|
PROFILES_DIR="${PROFILES_DIR:-${PROJECT_DIR:-/usr/local/share/installer}/profiles}"
|
||||||
|
|
||||||
|
load_profile() {
|
||||||
|
local profile="${PROFILE:-stable-cinnamon}"
|
||||||
|
local pdir="$PROFILES_DIR/$profile"
|
||||||
|
|
||||||
|
if [[ ! -d "$pdir" ]]; then
|
||||||
|
echo "[ERR] profile '$profile' not found at $pdir" >&2
|
||||||
|
echo "Available profiles:" >&2
|
||||||
|
ls -1 "$PROFILES_DIR" 2>/dev/null | sed 's/^/ - /' >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -r "$pdir/profile.conf" ]]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$pdir/profile.conf"
|
||||||
|
fi
|
||||||
|
|
||||||
|
export PROFILE="$profile"
|
||||||
|
export PROFILE_DIR="$pdir"
|
||||||
|
# Resolve packages list path (profile.conf may set it relative or absolute).
|
||||||
|
if [[ -n "$PROFILE_PACKAGES_FILE" && ! -r "$PROFILE_PACKAGES_FILE" ]]; then
|
||||||
|
# Try resolving relative to profile dir, then project root.
|
||||||
|
if [[ -r "$pdir/$(basename "$PROFILE_PACKAGES_FILE")" ]]; then
|
||||||
|
PROFILE_PACKAGES_FILE="$pdir/$(basename "$PROFILE_PACKAGES_FILE")"
|
||||||
|
elif [[ -r "${PROJECT_DIR:-.}/$PROFILE_PACKAGES_FILE" ]]; then
|
||||||
|
PROFILE_PACKAGES_FILE="${PROJECT_DIR:-.}/$PROFILE_PACKAGES_FILE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
[[ -z "$PROFILE_PACKAGES_FILE" ]] && PROFILE_PACKAGES_FILE="$pdir/packages.list"
|
||||||
|
export PROFILE_PACKAGES_FILE
|
||||||
|
|
||||||
|
echo "[INFO] profile loaded: $PROFILE ($PROFILE_DESC)"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
run_profile_customizations() {
|
||||||
|
# Source every *.sh under <profile>/customizations/ in name order.
|
||||||
|
local cdir="$PROFILE_DIR/customizations"
|
||||||
|
[[ -d "$cdir" ]] || { echo "[INFO] no profile customizations dir at $cdir"; return 0; }
|
||||||
|
local hook
|
||||||
|
for hook in "$cdir"/*.sh; do
|
||||||
|
[[ -r "$hook" ]] || continue
|
||||||
|
echo "[INFO] running profile hook: $(basename "$hook")"
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$hook"
|
||||||
|
done
|
||||||
|
}
|
||||||
77
installer/lib/tui.sh
Executable file
77
installer/lib/tui.sh
Executable file
@@ -0,0 +1,77 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# TUI disk selection. Uses `dialog` to show detected disks/partitions
|
||||||
|
# and require explicit user confirmation before any destructive action.
|
||||||
|
# Sets globals: TARGET_DISK, ROOT_PART, EFI_PART
|
||||||
|
|
||||||
|
# shellcheck source=common.sh
|
||||||
|
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
||||||
|
|
||||||
|
tui_select_install_target() {
|
||||||
|
step "Disk selection"
|
||||||
|
|
||||||
|
local default_root="${DEFAULT_ROOT_PART:-}"
|
||||||
|
local default_efi="${DEFAULT_EFI_PART:-}"
|
||||||
|
|
||||||
|
# Build a human menu of partitions (skip loop/ram/zram, only TYPE=part).
|
||||||
|
local menu_items=()
|
||||||
|
local dev type fstype size label
|
||||||
|
while read -r dev type fstype size label; do
|
||||||
|
[[ "$type" == "part" ]] || continue
|
||||||
|
[[ "$dev" =~ ^/dev/(sd|nvme|vd|mmcblk|hd|xvd) ]] || continue
|
||||||
|
local marker=""
|
||||||
|
[[ "$dev" == "$default_root" ]] && marker=" (DEFAULT ROOT)"
|
||||||
|
[[ "$fstype" == "vfat" ]] && marker+=" [EFI?]"
|
||||||
|
[[ "$fstype" == "ntfs" ]] && marker+=" [WINDOWS - DO NOT TOUCH]"
|
||||||
|
menu_items+=("$dev" "${fstype:-?} ${size} '${label:-}'${marker}")
|
||||||
|
done < <(lsblk -lnpo NAME,TYPE,FSTYPE,SIZE,LABEL 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [[ ${#menu_items[@]} -eq 0 ]]; then
|
||||||
|
log "lsblk output for diagnosis:"
|
||||||
|
lsblk -lnpo NAME,TYPE,FSTYPE,SIZE,LABEL 2>&1 | while read -r l; do log " $l"; done
|
||||||
|
die "no candidate partitions found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local choice
|
||||||
|
if [[ "${UNATTENDED:-0}" == "1" ]]; then
|
||||||
|
choice="$default_root"
|
||||||
|
log "[unattended] target root partition = $choice"
|
||||||
|
else
|
||||||
|
choice=$(dialog --stdout --title "Void Installer — SELECT ROOT PARTITION" \
|
||||||
|
--backtitle "WARNING: the chosen partition will be WIPED. Windows partitions show [WINDOWS]." \
|
||||||
|
--default-item "$default_root" \
|
||||||
|
--menu "Choose the partition to install Void Linux onto.\nDefault highlights $default_root (current Linux Mint).\nAbsolutely DO NOT pick a partition labelled [WINDOWS]." \
|
||||||
|
25 90 14 "${menu_items[@]}") \
|
||||||
|
|| die "user cancelled disk selection"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -b "$choice" ]] || die "selected device $choice is not a block device"
|
||||||
|
local fstype
|
||||||
|
fstype=$(lsblk -no FSTYPE "$choice" 2>/dev/null | head -1)
|
||||||
|
if [[ "$fstype" == "ntfs" ]]; then
|
||||||
|
die "REFUSING to wipe NTFS partition $choice (looks like Windows)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ROOT_PART="$choice"
|
||||||
|
EFI_PART="${default_efi}"
|
||||||
|
# Resolve the parent block device robustly (works for nvme, mmcblk, sd*, vd*).
|
||||||
|
TARGET_DISK="/dev/$(lsblk -no PKNAME "$choice" 2>/dev/null | head -1)"
|
||||||
|
[[ "$TARGET_DISK" == "/dev/" ]] && TARGET_DISK="$DEFAULT_DISK"
|
||||||
|
|
||||||
|
# Confirmation step: must type the partition device name verbatim.
|
||||||
|
if [[ "${UNATTENDED:-0}" != "1" ]]; then
|
||||||
|
local typed
|
||||||
|
typed=$(dialog --stdout --title "FINAL CONFIRMATION" \
|
||||||
|
--backtitle "Type the device name to confirm WIPE" \
|
||||||
|
--inputbox "About to:\n - WIPE : $ROOT_PART (will become btrfs)\n - SHARE : $EFI_PART (kept intact, only adds /EFI/Void)\n - LEAVE : everything else (Windows, recovery)\n\nType the FULL device path of the partition to wipe to continue:" \
|
||||||
|
18 75) \
|
||||||
|
|| die "user cancelled confirmation"
|
||||||
|
[[ "$typed" == "$ROOT_PART" ]] \
|
||||||
|
|| die "confirmation mismatch (typed '$typed' != '$ROOT_PART')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ok "target root : $ROOT_PART"
|
||||||
|
ok "shared EFI : $EFI_PART"
|
||||||
|
ok "parent disk : $TARGET_DISK"
|
||||||
|
|
||||||
|
export TARGET_DISK ROOT_PART EFI_PART
|
||||||
|
}
|
||||||
19
iso/Dockerfile
Normal file
19
iso/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Container used to run void-mklive with real root.
|
||||||
|
# This avoids the user-namespace CAP_MKNOD wall (dracut needs mknod /dev/null
|
||||||
|
# inside the initramfs staging dir) and lets losetup/mount/chroot work
|
||||||
|
# unconditionally. Host stays clean — no sudo, no host package installs.
|
||||||
|
|
||||||
|
FROM debian:stable-slim
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
bash git curl ca-certificates xz-utils tar patch python3 \
|
||||||
|
mtools xorriso squashfs-tools dosfstools e2fsprogs \
|
||||||
|
kmod \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# xbps-static is downloaded into /cache by the host script and added to PATH
|
||||||
|
# at runtime by /work/iso/_inner-build.sh — no Void packages baked here.
|
||||||
|
|
||||||
|
WORKDIR /work
|
||||||
61
iso/_inner-build.sh
Executable file
61
iso/_inner-build.sh
Executable file
@@ -0,0 +1,61 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Runs INSIDE the docker container (as root). Invoked by iso/build-iso.sh.
|
||||||
|
# Expects the project bind-mounted at /work and the cache at /cache.
|
||||||
|
#
|
||||||
|
# Required env (set by build-iso.sh):
|
||||||
|
# ARCH, REPO_URL, KEYMAP, LOCALE, ISO_PKGS, ISO_TITLE, OUT_ISO_REL
|
||||||
|
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
: "${ARCH:?}"; : "${REPO_URL:?}"; : "${KEYMAP:?}"; : "${LOCALE:?}"
|
||||||
|
: "${ISO_PKGS:?}"; : "${ISO_TITLE:?}"; : "${OUT_ISO_REL:?}"
|
||||||
|
|
||||||
|
CACHE_DIR=/cache
|
||||||
|
PROJECT_DIR=/work
|
||||||
|
MKLIVE_DIR="$CACHE_DIR/void-mklive"
|
||||||
|
INCLUDE_DIR="$PROJECT_DIR/build/includes"
|
||||||
|
OUT_ISO="$PROJECT_DIR/$OUT_ISO_REL"
|
||||||
|
|
||||||
|
# xbps-static was downloaded by the host script; put it on PATH so mklive
|
||||||
|
# uses it instead of expecting a system xbps.
|
||||||
|
export PATH="$CACHE_DIR/xbps-static/usr/bin:$PATH"
|
||||||
|
|
||||||
|
# Sanity checks.
|
||||||
|
[[ -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"
|
||||||
|
|
||||||
|
# Cleanup trap: lazy-unmount any leftover pseudo-fs from a previous abort.
|
||||||
|
_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.sh \
|
||||||
|
-a "$ARCH" \
|
||||||
|
-r "$REPO_URL" \
|
||||||
|
-c "$CACHE_DIR/xbps-live-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"
|
||||||
|
|
||||||
|
# Ensure the resulting file is writable by the host user.
|
||||||
|
chown "$(stat -c '%u:%g' "$PROJECT_DIR")" "$OUT_ISO" "${OUT_ISO}".* 2>/dev/null || true
|
||||||
296
iso/build-iso.sh
Executable file
296
iso/build-iso.sh
Executable file
@@ -0,0 +1,296 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Build a custom Void Linux live ISO with the installer baked in.
|
||||||
|
#
|
||||||
|
# Strategy:
|
||||||
|
# 1. Stage everything possible AS THE HOST USER (overlay, mklive clone, patches, xbps-static).
|
||||||
|
# 2. Run void-mklive's mklive.sh AS ROOT INSIDE A DOCKER CONTAINER.
|
||||||
|
# Avoids the user-namespace CAP_MKNOD/CAP_SYS_ADMIN walls.
|
||||||
|
#
|
||||||
|
# Requires (host): bash, git, curl, docker.
|
||||||
|
|
||||||
|
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/includes"
|
||||||
|
MKLIVE_DIR="$CACHE_DIR/void-mklive"
|
||||||
|
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}"
|
||||||
|
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source "$PROJECT_DIR/config/install.conf"
|
||||||
|
|
||||||
|
SECRETS_FILE="${SECRETS_FILE:-$PROJECT_DIR/secrets.env}"
|
||||||
|
[[ -r "$SECRETS_FILE" ]] || { echo "missing $SECRETS_FILE"; exit 1; }
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$SECRETS_FILE"
|
||||||
|
: "${USER_PASSWORD:?}"; : "${ROOT_PASSWORD:?}"
|
||||||
|
|
||||||
|
SSH_SRC_DIR="${SSH_SRC_DIR:-$HOME/.ssh}"
|
||||||
|
[[ -d "$SSH_SRC_DIR" ]] || { echo "no SSH dir at $SSH_SRC_DIR"; exit 1; }
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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) overlay
|
||||||
|
echo ">>> staging includes overlay at $INCLUDE_DIR"
|
||||||
|
rm -rf "$INCLUDE_DIR"
|
||||||
|
mkdir -p "$INCLUDE_DIR"
|
||||||
|
|
||||||
|
install -d -m 0755 "$INCLUDE_DIR/usr/local/share/installer/lib"
|
||||||
|
install -m 0644 "$PROJECT_DIR/config/install.conf" "$INCLUDE_DIR/usr/local/share/installer/install.conf"
|
||||||
|
# Default package list = stable-cinnamon profile (kept in /share for back-compat).
|
||||||
|
install -m 0644 "$PROJECT_DIR/config/profiles/stable-cinnamon/packages.list" \
|
||||||
|
"$INCLUDE_DIR/usr/local/share/installer/packages.list"
|
||||||
|
install -m 0755 "$PROJECT_DIR/installer/install.sh" "$INCLUDE_DIR/usr/local/share/installer/install.sh"
|
||||||
|
for f in "$PROJECT_DIR"/installer/lib/*.sh; do
|
||||||
|
install -m 0755 "$f" "$INCLUDE_DIR/usr/local/share/installer/lib/$(basename "$f")"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Ship every profile so the live ISO can install any of them via PROFILE=<name>.
|
||||||
|
install -d -m 0755 "$INCLUDE_DIR/usr/local/share/installer/profiles"
|
||||||
|
cp -a "$PROJECT_DIR/config/profiles/." "$INCLUDE_DIR/usr/local/share/installer/profiles/"
|
||||||
|
|
||||||
|
install -d -m 0755 "$INCLUDE_DIR/usr/local/sbin"
|
||||||
|
ln -sf /usr/local/share/installer/install.sh "$INCLUDE_DIR/usr/local/sbin/install-void"
|
||||||
|
|
||||||
|
install -d -m 0700 "$INCLUDE_DIR/etc"
|
||||||
|
{
|
||||||
|
printf "USER_PASSWORD=%q\n" "$USER_PASSWORD"
|
||||||
|
printf "ROOT_PASSWORD=%q\n" "$ROOT_PASSWORD"
|
||||||
|
} > "$INCLUDE_DIR/etc/installer-secrets.env"
|
||||||
|
chmod 0600 "$INCLUDE_DIR/etc/installer-secrets.env"
|
||||||
|
|
||||||
|
install -d -m 0700 "$INCLUDE_DIR/etc/installer-ssh"
|
||||||
|
cp -a "$SSH_SRC_DIR"/. "$INCLUDE_DIR/etc/installer-ssh/"
|
||||||
|
find "$INCLUDE_DIR/etc/installer-ssh" -type f -exec chmod 0600 {} +
|
||||||
|
find "$INCLUDE_DIR/etc/installer-ssh" -type d -exec chmod 0700 {} +
|
||||||
|
|
||||||
|
install -d -m 0755 "$INCLUDE_DIR/etc/sv/installer"
|
||||||
|
cat > "$INCLUDE_DIR/etc/sv/installer/run" <<'SV_EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
exec /sbin/agetty --autologin root --noclear tty1 linux
|
||||||
|
SV_EOF
|
||||||
|
chmod 0755 "$INCLUDE_DIR/etc/sv/installer/run"
|
||||||
|
|
||||||
|
install -d -m 0755 "$INCLUDE_DIR/etc/sv/agetty-tty1"
|
||||||
|
: > "$INCLUDE_DIR/etc/sv/agetty-tty1/down"
|
||||||
|
|
||||||
|
install -d -m 0755 "$INCLUDE_DIR/etc/runit/runsvdir/default"
|
||||||
|
ln -sf /etc/sv/installer "$INCLUDE_DIR/etc/runit/runsvdir/default/installer"
|
||||||
|
|
||||||
|
install -d -m 0700 "$INCLUDE_DIR/root"
|
||||||
|
cat > "$INCLUDE_DIR/root/.bash_profile" <<'PROFILE_EOF'
|
||||||
|
case "$(tty)" in
|
||||||
|
/dev/tty1)
|
||||||
|
if [ ! -f /tmp/.installer-done ]; then
|
||||||
|
touch /tmp/.installer-done
|
||||||
|
clear
|
||||||
|
echo
|
||||||
|
echo " Void Linux Installer (xps9700)"
|
||||||
|
echo " Press ENTER to start, or Ctrl-C within 5s for a shell."
|
||||||
|
sleep 5 || true
|
||||||
|
/usr/local/sbin/install-void || {
|
||||||
|
echo "Installer exited with $?. Dropping to shell."
|
||||||
|
exec /bin/bash
|
||||||
|
}
|
||||||
|
echo "Install complete. Type 'reboot' or 'poweroff'."
|
||||||
|
exec /bin/bash
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
PROFILE_EOF
|
||||||
|
chmod 0644 "$INCLUDE_DIR/root/.bash_profile"
|
||||||
|
|
||||||
|
cat > "$INCLUDE_DIR/etc/motd" <<MOTD_EOF
|
||||||
|
|
||||||
|
Void Linux installer for $HOSTNAME (XPS 17 9700)
|
||||||
|
Run: install-void
|
||||||
|
Logs: /var/log/void-installer.log
|
||||||
|
|
||||||
|
MOTD_EOF
|
||||||
|
|
||||||
|
if [[ -n "${EXTRA_INCLUDE_DIR:-}" && -d "$EXTRA_INCLUDE_DIR" ]]; then
|
||||||
|
echo ">>> merging EXTRA_INCLUDE_DIR=$EXTRA_INCLUDE_DIR"
|
||||||
|
cp -a "$EXTRA_INCLUDE_DIR/." "$INCLUDE_DIR/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3b) Customizations overlay (themes / icons / wallpapers / dotfiles / vscode)
|
||||||
|
echo ">>> staging user customizations overlay"
|
||||||
|
OVERLAY="$INCLUDE_DIR/etc/installer-overlay"
|
||||||
|
install -d -m 0755 "$OVERLAY" "$OVERLAY/wallpapers" \
|
||||||
|
"$OVERLAY/themes" "$OVERLAY/icons" \
|
||||||
|
"$OVERLAY/skel" "$OVERLAY/vscode-user"
|
||||||
|
|
||||||
|
# Wallpapers from ~/Scaricati/pxfuel*.jpg (literal parens in filenames)
|
||||||
|
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" | wc -l) file(s)"
|
||||||
|
|
||||||
|
# Theme: clone Gruvbox-GTK-Theme and run install.sh into a staging dir.
|
||||||
|
THEME_CACHE="$CACHE_DIR/gruvbox-gtk-theme"
|
||||||
|
if [[ ! -d "$THEME_CACHE/.git" ]]; then
|
||||||
|
git clone --depth=1 https://github.com/Fausto-Korpsvart/Gruvbox-GTK-Theme.git "$THEME_CACHE" || \
|
||||||
|
echo " (warning: could not clone theme repo)"
|
||||||
|
fi
|
||||||
|
THEME_BUILD="$CACHE_DIR/gruvbox-gtk-built"
|
||||||
|
if [[ -x "$THEME_CACHE/themes/install.sh" && ! -d "$THEME_BUILD" ]]; then
|
||||||
|
echo " building gruvbox themes -> $THEME_BUILD (via docker)"
|
||||||
|
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
|
||||||
|
' || echo " (warning: theme build failed; continuing without)"
|
||||||
|
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
|
||||||
|
|
||||||
|
# Icons: clone gruvbox-plus-icon-pack
|
||||||
|
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" || \
|
||||||
|
echo " (warning: could not clone icon repo)"
|
||||||
|
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 deployed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Bibata cursor — copied from host (no Void package).
|
||||||
|
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 deployed"
|
||||||
|
else
|
||||||
|
echo " (warning: $BIBATA_SRC missing on host; cursor theme will be skipped)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Dotfiles (skel)
|
||||||
|
DOTFILES_SRC="${DOTFILES_SRC:-$HOME}"
|
||||||
|
for f in .bashrc .bash_aliases .gitconfig; do
|
||||||
|
[[ -r "$DOTFILES_SRC/$f" ]] && install -m 0644 "$DOTFILES_SRC/$f" "$OVERLAY/skel/$f"
|
||||||
|
done
|
||||||
|
echo " dotfiles: $(ls -A "$OVERLAY/skel" 2>/dev/null | wc -l) file(s)"
|
||||||
|
|
||||||
|
# VS Code user config
|
||||||
|
VSCODE_SRC="${VSCODE_USER_SRC:-$HOME/.config/Code/User}"
|
||||||
|
if [[ -d "$VSCODE_SRC" ]]; then
|
||||||
|
for f in settings.json keybindings.json mcp.json tasks.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"
|
||||||
|
# Copy the whole globalStorage so Copilot/extensions keep their state
|
||||||
|
# (note: GitHub OAuth tokens live in libsecret, NOT here — user re-signs once).
|
||||||
|
[[ -d "$VSCODE_SRC/globalStorage" ]] && cp -a "$VSCODE_SRC/globalStorage" "$OVERLAY/vscode-user/globalStorage"
|
||||||
|
echo " vscode-user: $(ls -A "$OVERLAY/vscode-user" 2>/dev/null | wc -l) item(s)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# VS Code extensions list (so first-login can re-install them).
|
||||||
|
if command -v code >/dev/null 2>&1; then
|
||||||
|
code --list-extensions > "$OVERLAY/vscode-extensions.txt" 2>/dev/null || true
|
||||||
|
echo " vscode extensions: $(wc -l < "$OVERLAY/vscode-extensions.txt" 2>/dev/null) listed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Claude Code config + auth (~/.claude lives in $HOME on Linux).
|
||||||
|
CLAUDE_SRC="${CLAUDE_SRC:-$HOME/.claude}"
|
||||||
|
if [[ -d "$CLAUDE_SRC" ]]; then
|
||||||
|
cp -a "$CLAUDE_SRC" "$OVERLAY/claude"
|
||||||
|
echo " claude: ~/.claude bundled"
|
||||||
|
fi
|
||||||
|
CLAUDE_JSON="${CLAUDE_JSON:-$HOME/.claude.json}"
|
||||||
|
if [[ -r "$CLAUDE_JSON" ]]; then
|
||||||
|
install -m 0600 "$CLAUDE_JSON" "$OVERLAY/claude.json"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# VS Code extensions list (host)
|
||||||
|
if command -v code >/dev/null 2>&1; then
|
||||||
|
code --list-extensions > "$OVERLAY/vscode-extensions.txt" 2>/dev/null || true
|
||||||
|
echo " vscode extensions: $(wc -l < "$OVERLAY/vscode-extensions.txt") to install"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# First-login one-shot
|
||||||
|
[[ -r "$PROJECT_DIR/installer/first-login.sh" ]] && \
|
||||||
|
install -m 0755 "$PROJECT_DIR/installer/first-login.sh" "$OVERLAY/first-login.sh"
|
||||||
|
|
||||||
|
|
||||||
|
# 4) packages, output filename
|
||||||
|
ISO_PKGS=$(grep -vE '^\s*(#|$)' "$PROJECT_DIR/config/packages.live.list" | tr '\n' ' ')
|
||||||
|
TS="$(date -u +%Y%m%d)"
|
||||||
|
OUT_ISO="${OUTPUT_ISO:-$OUT_DIR/void-install-${HOSTNAME}-${TS}.iso}"
|
||||||
|
|
||||||
|
# 5) docker
|
||||||
|
echo ">>> building docker image $DOCKER_IMAGE (cached)"
|
||||||
|
# Force the legacy builder if buildx is missing (Docker 29 removed it from
|
||||||
|
# the default `docker build` path; users without docker-buildx-plugin
|
||||||
|
# detected on PATH will otherwise see "unknown command: docker buildx").
|
||||||
|
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
|
||||||
|
|
||||||
|
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" \
|
||||||
|
-e REPO_URL="$REPO_URL" \
|
||||||
|
-e KEYMAP="$KEYMAP" \
|
||||||
|
-e LOCALE="$LOCALE" \
|
||||||
|
-e ISO_PKGS="$ISO_PKGS" \
|
||||||
|
-e ISO_TITLE="Void Installer ($HOSTNAME)" \
|
||||||
|
-e OUT_ISO_REL="${OUT_ISO#$PROJECT_DIR/}" \
|
||||||
|
-e BOOT_CMDLINE="${BOOT_CMDLINE:-}" \
|
||||||
|
-e HOST_UID="$(id -u)" \
|
||||||
|
-e HOST_GID="$(id -g)" \
|
||||||
|
"$DOCKER_IMAGE" \
|
||||||
|
bash /work/iso/_inner-build.sh
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo ">>> ISO built: $OUT_ISO"
|
||||||
|
sha256sum "$OUT_ISO" | tee "${OUT_ISO}.sha256"
|
||||||
38
iso/patches/0001-cgroupv2-lazy-umount.patch
Normal file
38
iso/patches/0001-cgroupv2-lazy-umount.patch
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
--- a/lib.sh
|
||||||
|
+++ b/lib.sh
|
||||||
|
@@ -103,12 +103,16 @@ umount_pseudofs() {
|
||||||
|
# deletable instead throwing the error "Device or Resource Busy".
|
||||||
|
# The '-f' option is passed to umount to account for the
|
||||||
|
# contingency where the psuedofs mounts are not present.
|
||||||
|
+ # On cgroupv2 hosts (e.g. Ubuntu/Mint) some submounts stay busy;
|
||||||
|
+ # fall back to lazy unmount so the build continues cleanly.
|
||||||
|
if [ -d "${ROOTFS}" ]; then
|
||||||
|
for f in dev proc sys; do
|
||||||
|
- umount -R -f "$ROOTFS/$f" >/dev/null 2>&1
|
||||||
|
+ umount -R -f "$ROOTFS/$f" >/dev/null 2>&1 || \
|
||||||
|
+ umount -R -l "$ROOTFS/$f" >/dev/null 2>&1 || true
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
- umount -f "$ROOTFS/tmp" >/dev/null 2>&1
|
||||||
|
+ umount -f "$ROOTFS/tmp" >/dev/null 2>&1 || \
|
||||||
|
+ umount -l "$ROOTFS/tmp" >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
run_cmd_target() {
|
||||||
|
--- a/mklive.sh
|
||||||
|
+++ b/mklive.sh
|
||||||
|
@@ -55,10 +55,11 @@ mount_pseudofs() {
|
||||||
|
}
|
||||||
|
|
||||||
|
umount_pseudofs() {
|
||||||
|
+ # lazy unmount fallback for cgroupv2 hosts (Ubuntu/Mint)
|
||||||
|
for f in sys dev proc; do
|
||||||
|
- if [ -d "$ROOTFS/$f" ] && ! umount -R -f "$ROOTFS/$f"; then
|
||||||
|
- info_msg "ERROR: failed to unmount $ROOTFS/$f/"
|
||||||
|
- return 1
|
||||||
|
+ if [ -d "$ROOTFS/$f" ]; then
|
||||||
|
+ umount -R -f "$ROOTFS/$f" >/dev/null 2>&1 || \
|
||||||
|
+ umount -R -l "$ROOTFS/$f" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
62
iso/patches/0002-mtools-efi-and-onefs-cp.patch
Normal file
62
iso/patches/0002-mtools-efi-and-onefs-cp.patch
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
--- a/mklive.sh
|
||||||
|
+++ b/mklive.sh
|
||||||
|
@@ -386,13 +387,16 @@ EOF
|
||||||
|
|
||||||
|
modprobe -q loop || :
|
||||||
|
|
||||||
|
- # Create EFI vfat image.
|
||||||
|
- truncate -s 32M "$GRUB_DIR"/efiboot.img >/dev/null 2>&1
|
||||||
|
- mkfs.vfat -F12 -S 512 -n "grub_uefi" "$GRUB_DIR/efiboot.img" >/dev/null 2>&1
|
||||||
|
+ # Create EFI vfat image — use mtools so the build does not depend on
|
||||||
|
+ # losetup (avoids CAP_SYS_ADMIN on init userns / works inside containers).
|
||||||
|
+ truncate -s 64M "$GRUB_DIR"/efiboot.img >/dev/null 2>&1
|
||||||
|
+ mformat -i "$GRUB_DIR/efiboot.img" -F -v "grub_uefi" ::
|
||||||
|
|
||||||
|
GRUB_EFI_TMPDIR="$(mktemp --tmpdir="$BUILDDIR" -dt grub-efi.XXXXX)"
|
||||||
|
- LOOP_DEVICE="$(losetup --show --find "${GRUB_DIR}"/efiboot.img)"
|
||||||
|
- mount -o rw,flush -t vfat "${LOOP_DEVICE}" "${GRUB_EFI_TMPDIR}" >/dev/null 2>&1
|
||||||
|
+ LOOP_DEVICE="$(losetup --show --find "${GRUB_DIR}"/efiboot.img 2>/dev/null)" || LOOP_DEVICE=""
|
||||||
|
+ if [ -n "$LOOP_DEVICE" ]; then
|
||||||
|
+ mount -o rw,flush -t vfat "${LOOP_DEVICE}" "${GRUB_EFI_TMPDIR}" >/dev/null 2>&1
|
||||||
|
+ fi
|
||||||
|
|
||||||
|
build_grub_image() {
|
||||||
|
local GRUB_ARCH="$1" EFI_ARCH="$2"
|
||||||
|
@@ -402,8 +406,7 @@ EOF
|
||||||
|
--output="/tmp/boot${EFI_ARCH,,}.efi" \
|
||||||
|
"boot/grub/grub.cfg"
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
- umount "$GRUB_EFI_TMPDIR"
|
||||||
|
- losetup --detach "${LOOP_DEVICE}"
|
||||||
|
+ [ -n "$LOOP_DEVICE" ] && { umount "$GRUB_EFI_TMPDIR"; losetup --detach "${LOOP_DEVICE}"; }
|
||||||
|
die "Failed to generate EFI loader"
|
||||||
|
fi
|
||||||
|
mkdir -p "${GRUB_EFI_TMPDIR}"/EFI/BOOT
|
||||||
|
@@ -426,8 +429,17 @@ EOF
|
||||||
|
build_grub_image arm64 aa64
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
- umount "$GRUB_EFI_TMPDIR"
|
||||||
|
- losetup --detach "${LOOP_DEVICE}"
|
||||||
|
+ if [ -n "$LOOP_DEVICE" ]; then
|
||||||
|
+ umount "$GRUB_EFI_TMPDIR"
|
||||||
|
+ losetup --detach "${LOOP_DEVICE}"
|
||||||
|
+ else
|
||||||
|
+ (cd "$GRUB_EFI_TMPDIR" && find . -type d | while read -r d; do
|
||||||
|
+ mmd -i "$GRUB_DIR/efiboot.img" "::${d#.}" 2>/dev/null || true
|
||||||
|
+ done)
|
||||||
|
+ (cd "$GRUB_EFI_TMPDIR" && find . -type f | while read -r f; do
|
||||||
|
+ mcopy -i "$GRUB_DIR/efiboot.img" "$f" "::${f#.}"
|
||||||
|
+ done)
|
||||||
|
+ fi
|
||||||
|
rm -rf "$GRUB_EFI_TMPDIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -442,7 +454,7 @@ generate_squashfs() {
|
||||||
|
mkdir -p "$BUILDDIR/tmp-rootfs"
|
||||||
|
mkfs.ext3 -F -m1 "$BUILDDIR/tmp/LiveOS/ext3fs.img" >/dev/null 2>&1
|
||||||
|
mount -o loop "$BUILDDIR/tmp/LiveOS/ext3fs.img" "$BUILDDIR/tmp-rootfs"
|
||||||
|
- cp -a "$ROOTFS"/* "$BUILDDIR"/tmp-rootfs/
|
||||||
|
+ cp -a --one-file-system "$ROOTFS"/* "$BUILDDIR"/tmp-rootfs/
|
||||||
|
umount -f "$BUILDDIR/tmp-rootfs"
|
||||||
|
mkdir -p "$IMAGEDIR/LiveOS"
|
||||||
20
tests/boot-niri-interactive.sh
Executable file
20
tests/boot-niri-interactive.sh
Executable file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Boot the installed niri disk in an interactive QEMU GUI window.
|
||||||
|
set -Eeuo pipefail
|
||||||
|
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
OUT="$PROJECT_DIR/out/niri"
|
||||||
|
DISK="$OUT/niri-disk.img"
|
||||||
|
VARS="$OUT/OVMF_VARS.installed.fd"
|
||||||
|
|
||||||
|
[[ -r "$DISK" ]] || { echo "no $DISK — run tests/run-niri-install.sh first"; exit 1; }
|
||||||
|
cp "$OUT/OVMF_VARS.fd" "$VARS"
|
||||||
|
|
||||||
|
DISPLAY="${DISPLAY:-:0}" exec qemu-system-x86_64 \
|
||||||
|
-name void-niri-installed \
|
||||||
|
-machine q35,accel=kvm:tcg \
|
||||||
|
-cpu max -m 4096 -smp 4 \
|
||||||
|
-drive if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_CODE.fd \
|
||||||
|
-drive "if=pflash,format=raw,file=$VARS" \
|
||||||
|
-drive "if=virtio,file=$DISK,format=raw,cache=none" \
|
||||||
|
-netdev user,id=n0 -device virtio-net-pci,netdev=n0 \
|
||||||
|
-vga virtio -display gtk
|
||||||
34
tests/interactive-qemu.sh
Executable file
34
tests/interactive-qemu.sh
Executable file
@@ -0,0 +1,34 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Launch QEMU with the latest installer ISO for INTERACTIVE testing.
|
||||||
|
# Use this when you want to drive the TUI yourself.
|
||||||
|
set -Eeuo pipefail
|
||||||
|
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
OUT_DIR="$PROJECT_DIR/out"
|
||||||
|
ISO="${ISO:-$(ls -t "$OUT_DIR"/void-install-*.iso 2>/dev/null | grep -v TEST | head -1 || true)}"
|
||||||
|
DISK="${DISK:-$OUT_DIR/test-disk.img}"
|
||||||
|
RAM_MB="${RAM_MB:-4096}"
|
||||||
|
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 — run 'make iso' first"; exit 1; }
|
||||||
|
|
||||||
|
if [[ ! -f "$DISK" ]]; then
|
||||||
|
"$PROJECT_DIR/tests/make-test-disk.sh" "$DISK"
|
||||||
|
fi
|
||||||
|
VARS="$OUT_DIR/OVMF_VARS.interactive.fd"
|
||||||
|
[[ -f "$VARS" ]] || cp "$OVMF_VARS_TPL" "$VARS"
|
||||||
|
|
||||||
|
exec qemu-system-x86_64 \
|
||||||
|
-name void-installer-interactive \
|
||||||
|
-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 \
|
||||||
|
-vga virtio -display gtk
|
||||||
107
tests/lib/make-test-overlay.sh
Executable file
107
tests/lib/make-test-overlay.sh
Executable file
@@ -0,0 +1,107 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Side-channel overlay used ONLY by the QEMU smoke-test harness.
|
||||||
|
# Sourced into the live ISO at /usr/local/share/installer/install.conf.d/
|
||||||
|
# and /etc/profile.d/. Forces unattended + test-mode behaviour and enables
|
||||||
|
# sshd in the installed system so the harness can ssh in.
|
||||||
|
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
DEST="${1:?usage: $0 <out-dir>}"
|
||||||
|
|
||||||
|
mkdir -p "$DEST/etc/profile.d"
|
||||||
|
cat > "$DEST/etc/profile.d/99-void-installer-test.sh" <<EOF
|
||||||
|
# Auto-run installer non-interactively when test ISO boots.
|
||||||
|
export UNATTENDED=1
|
||||||
|
export TEST_MODE=1
|
||||||
|
export PROFILE="${TEST_PROFILE:-stable-cinnamon}"
|
||||||
|
EOF
|
||||||
|
chmod 0644 "$DEST/etc/profile.d/99-void-installer-test.sh"
|
||||||
|
|
||||||
|
mkdir -p "$DEST/usr/local/share/installer/install.conf.d"
|
||||||
|
cat > "$DEST/usr/local/share/installer/install.conf.d/99-test.conf" <<'EOF'
|
||||||
|
# Test overrides (qemu smoke test)
|
||||||
|
SSHD_ENABLE="yes"
|
||||||
|
DEFAULT_DISK="/dev/vda"
|
||||||
|
DEFAULT_ROOT_PART="/dev/vda5"
|
||||||
|
DEFAULT_EFI_PART="/dev/vda1"
|
||||||
|
# Use the local xbps proxy on the host (10.0.2.2 inside QEMU's user net).
|
||||||
|
INSTALL_REPO_URL="http://10.0.2.2:3142/current"
|
||||||
|
# Quieter zram in tiny VM
|
||||||
|
ZRAM_SIZE_PCT="25"
|
||||||
|
EOF
|
||||||
|
chmod 0644 "$DEST/usr/local/share/installer/install.conf.d/99-test.conf"
|
||||||
|
|
||||||
|
# Drop a public key the harness can use to ssh in as moze without a password.
|
||||||
|
# Generated below (or supplied via TEST_PUBKEY env) and added to authorized_keys
|
||||||
|
# under /etc/installer-ssh/ so postinstall.sh copies it into ~/.ssh/.
|
||||||
|
mkdir -p "$DEST/etc/installer-ssh"
|
||||||
|
if [[ -n "${TEST_PUBKEY:-}" ]]; then
|
||||||
|
echo "$TEST_PUBKEY" > "$DEST/etc/installer-ssh/authorized_keys"
|
||||||
|
chmod 0600 "$DEST/etc/installer-ssh/authorized_keys"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Enable an autostart `sshd` already inside the live env so the harness
|
||||||
|
# can ssh into the live ISO too if needed (debug).
|
||||||
|
mkdir -p "$DEST/etc/runit/runsvdir/default"
|
||||||
|
ln -sf /etc/sv/sshd "$DEST/etc/runit/runsvdir/default/sshd" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Autologin root on ttyS0 so the installer's .bash_profile fires under
|
||||||
|
# QEMU's `-display none -serial file:...` (no tty1 attached).
|
||||||
|
mkdir -p "$DEST/etc/sv/agetty-ttyS0-autologin"
|
||||||
|
cat > "$DEST/etc/sv/agetty-ttyS0-autologin/run" <<'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
exec /sbin/agetty --autologin root --noclear -L 115200 ttyS0 vt100
|
||||||
|
EOF
|
||||||
|
chmod 0755 "$DEST/etc/sv/agetty-ttyS0-autologin/run"
|
||||||
|
ln -sf /etc/sv/agetty-ttyS0-autologin "$DEST/etc/runit/runsvdir/default/agetty-ttyS0-autologin"
|
||||||
|
|
||||||
|
# Make .bash_profile fire on ttyS0 AND tty1 (interactive GTK QEMU uses tty1).
|
||||||
|
mkdir -p "$DEST/root"
|
||||||
|
cat > "$DEST/root/.bash_profile" <<'EOF'
|
||||||
|
case "$(tty)" in
|
||||||
|
/dev/ttyS0|/dev/tty1)
|
||||||
|
# Atomic single-instance lock. mkdir is atomic; only ONE tty wins.
|
||||||
|
# Previously the check-then-touch race let both tty1 and ttyS0 enter,
|
||||||
|
# running install-void twice in parallel and corrupting /mnt.
|
||||||
|
if mkdir /tmp/.installer-lock 2>/dev/null; then
|
||||||
|
echo "Void Linux Installer (xps9700) — TEST MODE"
|
||||||
|
# Bring up networking via dhcpcd (live ISO doesn't autostart it).
|
||||||
|
echo " bringing up network..."
|
||||||
|
dhcpcd -b 2>/dev/null || true
|
||||||
|
for i in $(seq 1 20); do
|
||||||
|
if curl -sf --max-time 1 http://10.0.2.2:3142/ >/dev/null 2>&1; then
|
||||||
|
echo " network up after ${i}s"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
sleep 1
|
||||||
|
set -x
|
||||||
|
/usr/local/sbin/install-void 2>&1 | tee /tmp/installer.log
|
||||||
|
rc=${PIPESTATUS[0]}
|
||||||
|
set +x
|
||||||
|
if [ "$rc" -ne 0 ]; then
|
||||||
|
echo "===== INSTALLER FAILED rc=$rc ====="
|
||||||
|
echo "--- ip addr ---"
|
||||||
|
ip addr 2>&1 | head -20
|
||||||
|
echo "--- ip route ---"
|
||||||
|
ip route 2>&1
|
||||||
|
echo "--- /etc/resolv.conf ---"
|
||||||
|
cat /etc/resolv.conf 2>/dev/null
|
||||||
|
echo "--- curl proxy test ---"
|
||||||
|
curl -sv --max-time 3 http://10.0.2.2:3142/ 2>&1 | head -10
|
||||||
|
echo "--- /tmp/installer.log (tail) ---"
|
||||||
|
tail -100 /tmp/installer.log 2>/dev/null
|
||||||
|
echo "===== END FAIL DUMP ====="
|
||||||
|
exec /bin/bash
|
||||||
|
fi
|
||||||
|
echo "Install complete. Powering off."
|
||||||
|
poweroff
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
EOF
|
||||||
|
chmod 0644 "$DEST/root/.bash_profile"
|
||||||
|
|
||||||
|
echo ">>> test overlay staged at $DEST"
|
||||||
53
tests/make-test-disk.sh
Executable file
53
tests/make-test-disk.sh
Executable file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Create a fresh test disk image that mimics the XPS 17 layout:
|
||||||
|
# p1: 150 MiB EFI vfat (with a fake EFI/Microsoft/* tree)
|
||||||
|
# p2: 128 MiB MSR
|
||||||
|
# p3: 4 GiB NTFS placeholder ("Windows", must NOT be touched)
|
||||||
|
# p5: rest btrfs "Mint" (will be wiped by installer)
|
||||||
|
#
|
||||||
|
# No root required: uses sgdisk on the raw file + mtools for FAT32.
|
||||||
|
# The installer reformats p5; p3 carries only a partition-type flag (0700).
|
||||||
|
#
|
||||||
|
# Output: a sparse raw image (QEMU format=raw).
|
||||||
|
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
OUT="${1:-$PROJECT_DIR/out/test-disk.img}"
|
||||||
|
SIZE="${SIZE:-32G}"
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$OUT")"
|
||||||
|
|
||||||
|
command -v sgdisk >/dev/null || { echo "sgdisk missing (gptfdisk)"; exit 1; }
|
||||||
|
command -v mformat >/dev/null || { echo "mformat missing (mtools)"; exit 1; }
|
||||||
|
command -v mmd >/dev/null || { echo "mmd missing (mtools)"; exit 1; }
|
||||||
|
command -v mcopy >/dev/null || { echo "mcopy missing (mtools)"; exit 1; }
|
||||||
|
|
||||||
|
echo ">>> creating sparse raw disk ($SIZE) at $OUT"
|
||||||
|
rm -f "$OUT"
|
||||||
|
truncate -s "$SIZE" "$OUT"
|
||||||
|
|
||||||
|
echo ">>> partitioning"
|
||||||
|
sgdisk -Z "$OUT" >/dev/null 2>&1 || true
|
||||||
|
sgdisk \
|
||||||
|
-n 1:2048:+150M -t 1:ef00 -c 1:"EFI system partition" \
|
||||||
|
-n 2:0:+128M -t 2:0c01 -c 2:"Microsoft reserved partition" \
|
||||||
|
-n 3:0:+4G -t 3:0700 -c 3:"Basic data partition" \
|
||||||
|
-n 5:0:0 -t 5:8300 -c 5:"Mint" \
|
||||||
|
"$OUT" >/dev/null
|
||||||
|
|
||||||
|
# Derive partition-1 byte offset + sector count from GPT metadata.
|
||||||
|
P1_START=$(sgdisk -i 1 "$OUT" | awk '/First sector:/ {print $3}')
|
||||||
|
P1_LAST=$(sgdisk -i 1 "$OUT" | awk '/Last sector:/ {print $3}')
|
||||||
|
P1_SECTORS=$(( P1_LAST - P1_START + 1 ))
|
||||||
|
P1_OFFSET=$(( P1_START * 512 ))
|
||||||
|
|
||||||
|
echo ">>> formatting EFI partition 1 (FAT32) at byte offset $P1_OFFSET"
|
||||||
|
mformat -i "$OUT@@$P1_OFFSET" -F -T "$P1_SECTORS" -v ESP ::
|
||||||
|
|
||||||
|
echo ">>> faking Windows EFI loader on p1"
|
||||||
|
mmd -i "$OUT@@$P1_OFFSET" ::/EFI ::/EFI/Microsoft ::/EFI/Microsoft/Boot
|
||||||
|
printf 'FAKE WINDOWS BOOTMGR\n' \
|
||||||
|
| mcopy -i "$OUT@@$P1_OFFSET" - ::/EFI/Microsoft/Boot/bootmgfw.efi
|
||||||
|
|
||||||
|
echo ">>> done: $OUT"
|
||||||
94
tests/run-niri-install.sh
Executable file
94
tests/run-niri-install.sh
Executable file
@@ -0,0 +1,94 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Build the mainline-niri ISO and install it headlessly into a separate
|
||||||
|
# disk image, isolated from the stable-cinnamon test harness.
|
||||||
|
#
|
||||||
|
# Outputs (under out/niri/):
|
||||||
|
# void-install-niri.iso test ISO with PROFILE=mainline-niri baked in
|
||||||
|
# niri-disk.img 32G raw disk receiving the install
|
||||||
|
# OVMF_VARS.fd per-VM EFI nvram
|
||||||
|
# install.serial.log full installer log
|
||||||
|
#
|
||||||
|
# After a successful install, run:
|
||||||
|
# tests/boot-niri-interactive.sh
|
||||||
|
# to launch the installed niri VM in a GUI window.
|
||||||
|
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
OUT="$PROJECT_DIR/out/niri"
|
||||||
|
mkdir -p "$OUT"
|
||||||
|
|
||||||
|
QEMU="${QEMU:-qemu-system-x86_64}"
|
||||||
|
OVMF_CODE="${OVMF_CODE:-/usr/share/OVMF/OVMF_CODE.fd}"
|
||||||
|
OVMF_VARS_TPL="${OVMF_VARS_TPL:-/usr/share/OVMF/OVMF_VARS.fd}"
|
||||||
|
RAM_MB="${RAM_MB:-4096}"
|
||||||
|
SMP="${SMP:-4}"
|
||||||
|
TIMEOUT_INSTALL="${TIMEOUT_INSTALL:-3600}"
|
||||||
|
|
||||||
|
blue() { printf '\033[34m==> %s\033[0m\n' "$*"; }
|
||||||
|
|
||||||
|
# 1) ensure xbps proxy is up (shared with main harness)
|
||||||
|
"$PROJECT_DIR/tools/start-xbps-proxy.sh"
|
||||||
|
trap '"$PROJECT_DIR/tools/stop-xbps-proxy.sh" 2>/dev/null || true; \
|
||||||
|
pkill -f qemu-system.*void-niri-install 2>/dev/null || true' EXIT
|
||||||
|
|
||||||
|
# 2) build TEST overlay with PROFILE=mainline-niri
|
||||||
|
TEST_OVERLAY="$OUT/test-overlay"
|
||||||
|
TEST_PROFILE=mainline-niri \
|
||||||
|
"$PROJECT_DIR/tests/lib/make-test-overlay.sh" "$TEST_OVERLAY"
|
||||||
|
|
||||||
|
# 3) build ISO
|
||||||
|
ISO="$OUT/void-install-niri.iso"
|
||||||
|
if [[ ! -f "$ISO" || -n "${REBUILD_ISO:-}" ]]; then
|
||||||
|
blue "building niri ISO -> $ISO"
|
||||||
|
EXTRA_INCLUDE_DIR="$TEST_OVERLAY" \
|
||||||
|
OUTPUT_ISO="$ISO" \
|
||||||
|
INSTALL_REPO_URL="http://10.0.2.2:3142/current" \
|
||||||
|
BOOT_CMDLINE="console=tty0 console=ttyS0,115200" \
|
||||||
|
"$PROJECT_DIR/iso/build-iso.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4) fresh disk
|
||||||
|
DISK="$OUT/niri-disk.img"
|
||||||
|
blue "creating fresh disk -> $DISK"
|
||||||
|
"$PROJECT_DIR/tests/make-test-disk.sh" "$DISK"
|
||||||
|
|
||||||
|
# 5) per-VM EFI nvram
|
||||||
|
VARS="$OUT/OVMF_VARS.fd"
|
||||||
|
cp "$OVMF_VARS_TPL" "$VARS"
|
||||||
|
|
||||||
|
# 6) run installer headless
|
||||||
|
SERIAL_LOG="$OUT/install.serial.log"
|
||||||
|
: > "$SERIAL_LOG"
|
||||||
|
blue "boot ISO + run installer (timeout ${TIMEOUT_INSTALL}s)"
|
||||||
|
set +e
|
||||||
|
timeout "$TIMEOUT_INSTALL" "$QEMU" \
|
||||||
|
-name void-niri-install \
|
||||||
|
-machine q35,accel=kvm:tcg \
|
||||||
|
-cpu max -m "$RAM_MB" -smp "$SMP" \
|
||||||
|
-display none -monitor none \
|
||||||
|
-serial "file:$SERIAL_LOG" \
|
||||||
|
-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 order=d,menu=off \
|
||||||
|
-netdev user,id=n0 -device virtio-net-pci,netdev=n0 \
|
||||||
|
-no-reboot
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [[ $rc -ne 0 ]]; then
|
||||||
|
echo "installer QEMU exit $rc — see $SERIAL_LOG"
|
||||||
|
tail -40 "$SERIAL_LOG"
|
||||||
|
exit "$rc"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -q "INSTALLATION COMPLETE\|installation complete\|powering off" "$SERIAL_LOG"; then
|
||||||
|
blue "niri install completed; disk -> $DISK"
|
||||||
|
blue "run: tests/boot-niri-interactive.sh to boot it in a GUI window"
|
||||||
|
else
|
||||||
|
echo "WARNING: no completion marker in $SERIAL_LOG"
|
||||||
|
tail -30 "$SERIAL_LOG"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
242
tests/run-qemu-test.sh
Executable file
242
tests/run-qemu-test.sh
Executable file
@@ -0,0 +1,242 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Automated headless QEMU smoke test for the Void installer.
|
||||||
|
#
|
||||||
|
# Workflow:
|
||||||
|
# 1. Build (or reuse) a TEST variant of the ISO with UNATTENDED=1 +
|
||||||
|
# TEST_MODE=1 and our test pubkey baked in for moze@.
|
||||||
|
# 2. Build a fresh test disk image mimicking the XPS 17 layout.
|
||||||
|
# 3. Boot the ISO under QEMU/OVMF; installer runs unattended; VM powers off.
|
||||||
|
# 4. Boot the installed disk; ssh in as moze; run smoke checks.
|
||||||
|
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
OUT_DIR="$PROJECT_DIR/out"
|
||||||
|
LOG_DIR="$OUT_DIR/qemu-logs"
|
||||||
|
mkdir -p "$OUT_DIR" "$LOG_DIR"
|
||||||
|
|
||||||
|
QEMU="${QEMU:-qemu-system-x86_64}"
|
||||||
|
OVMF_CODE="${OVMF_CODE:-}"
|
||||||
|
OVMF_VARS_TPL="${OVMF_VARS_TPL:-}"
|
||||||
|
RAM_MB="${RAM_MB:-4096}"
|
||||||
|
SMP="${SMP:-4}"
|
||||||
|
SSH_PORT="${SSH_PORT:-2222}"
|
||||||
|
TIMEOUT_INSTALL="${TIMEOUT_INSTALL:-3600}"
|
||||||
|
TIMEOUT_BOOT="${TIMEOUT_BOOT:-300}"
|
||||||
|
ACCEL="${ACCEL:-kvm:tcg}"
|
||||||
|
|
||||||
|
# Auto-detect OVMF paths.
|
||||||
|
for c in /usr/share/OVMF/OVMF_CODE.fd /usr/share/edk2-ovmf/x64/OVMF_CODE.fd \
|
||||||
|
/usr/share/edk2/x64/OVMF_CODE.fd /usr/share/qemu/OVMF_CODE.fd; do
|
||||||
|
[[ -z "$OVMF_CODE" && -r "$c" ]] && OVMF_CODE="$c"
|
||||||
|
done
|
||||||
|
for v in /usr/share/OVMF/OVMF_VARS.fd /usr/share/edk2-ovmf/x64/OVMF_VARS.fd \
|
||||||
|
/usr/share/edk2/x64/OVMF_VARS.fd /usr/share/qemu/OVMF_VARS.fd; do
|
||||||
|
[[ -z "$OVMF_VARS_TPL" && -r "$v" ]] && OVMF_VARS_TPL="$v"
|
||||||
|
done
|
||||||
|
[[ -r "$OVMF_CODE" ]] || { echo "OVMF_CODE not found"; exit 1; }
|
||||||
|
[[ -r "$OVMF_VARS_TPL" ]] || { echo "OVMF_VARS not found"; exit 1; }
|
||||||
|
|
||||||
|
red() { printf '\033[31m%s\033[0m\n' "$*" >&2; }
|
||||||
|
green() { printf '\033[32m%s\033[0m\n' "$*"; }
|
||||||
|
blue() { printf '\033[34m==> %s\033[0m\n' "$*"; }
|
||||||
|
|
||||||
|
# ---------- 0) start xbps caching proxy ----------
|
||||||
|
blue "starting XBPS caching proxy"
|
||||||
|
"$PROJECT_DIR/tools/start-xbps-proxy.sh"
|
||||||
|
PIDFILE="$OUT_DIR/qemu.pid"
|
||||||
|
cleanup_all() {
|
||||||
|
# Kill any QEMU started by this run (pidfile + name fallback).
|
||||||
|
[[ -f "$PIDFILE" ]] && kill "$(cat "$PIDFILE")" 2>/dev/null || true
|
||||||
|
rm -f "$PIDFILE"
|
||||||
|
# Defensive: kill any lingering qemu by our unique -name tags.
|
||||||
|
pkill -f 'qemu-system.*void-install(er-test|ed-test)' 2>/dev/null || true
|
||||||
|
"$PROJECT_DIR/tools/stop-xbps-proxy.sh" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
trap cleanup_all EXIT INT TERM
|
||||||
|
|
||||||
|
# ---------- 1) test ssh keypair ----------
|
||||||
|
TEST_KEY="$OUT_DIR/id_test"
|
||||||
|
if [[ ! -f "$TEST_KEY" ]]; then
|
||||||
|
blue "generating test ssh keypair"
|
||||||
|
ssh-keygen -t ed25519 -N '' -C 'qemu-test' -f "$TEST_KEY" >/dev/null
|
||||||
|
fi
|
||||||
|
TEST_PUBKEY=$(cat "${TEST_KEY}.pub")
|
||||||
|
export TEST_PUBKEY
|
||||||
|
|
||||||
|
# ---------- 2) build TEST ISO ----------
|
||||||
|
TEST_ISO="$OUT_DIR/void-install-TEST.iso"
|
||||||
|
TEST_OVERLAY_DIR="$OUT_DIR/test-overlay"
|
||||||
|
"$PROJECT_DIR/tests/lib/make-test-overlay.sh" "$TEST_OVERLAY_DIR"
|
||||||
|
|
||||||
|
if [[ ! -f "$TEST_ISO" || -n "${REBUILD_ISO:-}" ]]; then
|
||||||
|
blue "building test ISO -> $TEST_ISO"
|
||||||
|
EXTRA_INCLUDE_DIR="$TEST_OVERLAY_DIR" \
|
||||||
|
OUTPUT_ISO="$TEST_ISO" \
|
||||||
|
INSTALL_REPO_URL="http://10.0.2.2:3142/current" \
|
||||||
|
BOOT_CMDLINE="console=tty0 console=ttyS0,115200" \
|
||||||
|
"$PROJECT_DIR/iso/build-iso.sh"
|
||||||
|
else
|
||||||
|
blue "reusing cached test ISO $TEST_ISO (set REBUILD_ISO=1 to rebuild)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------- 3) test disk ----------
|
||||||
|
DISK="$OUT_DIR/test-disk.img"
|
||||||
|
blue "creating fresh test disk -> $DISK"
|
||||||
|
"$PROJECT_DIR/tests/make-test-disk.sh" "$DISK"
|
||||||
|
|
||||||
|
# ---------- 4) OVMF VARS copy ----------
|
||||||
|
VARS="$OUT_DIR/OVMF_VARS.fd"
|
||||||
|
cp "$OVMF_VARS_TPL" "$VARS"
|
||||||
|
|
||||||
|
# ---------- 5) install phase ----------
|
||||||
|
blue "boot ISO + run installer (timeout ${TIMEOUT_INSTALL}s)"
|
||||||
|
SERIAL_LOG="$LOG_DIR/install.serial.log"
|
||||||
|
: > "$SERIAL_LOG"
|
||||||
|
|
||||||
|
set +e
|
||||||
|
timeout "$TIMEOUT_INSTALL" "$QEMU" \
|
||||||
|
-name void-installer-test \
|
||||||
|
-machine q35,accel="$ACCEL" \
|
||||||
|
-cpu max \
|
||||||
|
-m "$RAM_MB" \
|
||||||
|
-smp "$SMP" \
|
||||||
|
-display none \
|
||||||
|
-monitor none \
|
||||||
|
-serial "file:$SERIAL_LOG" \
|
||||||
|
-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 "$TEST_ISO" \
|
||||||
|
-boot order=d,menu=off \
|
||||||
|
-netdev user,id=n0 -device virtio-net-pci,netdev=n0 \
|
||||||
|
-no-reboot
|
||||||
|
RC=$?
|
||||||
|
set -e
|
||||||
|
blue "install QEMU exited rc=$RC"
|
||||||
|
|
||||||
|
if ! grep -q 'Installation complete' "$SERIAL_LOG"; then
|
||||||
|
echo "------ tail of $SERIAL_LOG ------" >&2
|
||||||
|
tail -200 "$SERIAL_LOG" >&2 || true
|
||||||
|
red "installer did not report 'Installation complete'"; exit 1
|
||||||
|
fi
|
||||||
|
green "installer reported success"
|
||||||
|
|
||||||
|
# ---------- 6) boot installed system ----------
|
||||||
|
blue "boot installed system, ssh on 127.0.0.1:$SSH_PORT"
|
||||||
|
BOOT_LOG="$LOG_DIR/boot.serial.log"
|
||||||
|
: > "$BOOT_LOG"
|
||||||
|
|
||||||
|
"$QEMU" \
|
||||||
|
-name void-installed-test \
|
||||||
|
-machine q35,accel="$ACCEL" \
|
||||||
|
-cpu max \
|
||||||
|
-m "$RAM_MB" \
|
||||||
|
-smp "$SMP" \
|
||||||
|
-display none \
|
||||||
|
-serial "file:$BOOT_LOG" \
|
||||||
|
-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" \
|
||||||
|
-netdev "user,id=n0,hostfwd=tcp:127.0.0.1:$SSH_PORT-:22" \
|
||||||
|
-device virtio-net-pci,netdev=n0 \
|
||||||
|
-daemonize \
|
||||||
|
-pidfile "$PIDFILE"
|
||||||
|
|
||||||
|
blue "waiting for sshd"
|
||||||
|
SSH_READY=0
|
||||||
|
for i in $(seq 1 "$TIMEOUT_BOOT"); do
|
||||||
|
if ssh -i "$TEST_KEY" -o StrictHostKeyChecking=no \
|
||||||
|
-o UserKnownHostsFile=/dev/null \
|
||||||
|
-o ConnectTimeout=2 -o BatchMode=yes \
|
||||||
|
-p "$SSH_PORT" moze@127.0.0.1 true 2>/dev/null; then
|
||||||
|
SSH_READY=1
|
||||||
|
green "ssh reachable after ${i}s"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
if [[ $SSH_READY -ne 1 ]]; then
|
||||||
|
tail -200 "$BOOT_LOG" >&2 || true
|
||||||
|
red "sshd never came up"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------- 7) smoke checks ----------
|
||||||
|
blue "running smoke tests"
|
||||||
|
SSHCMD=(ssh -i "$TEST_KEY" -o StrictHostKeyChecking=no
|
||||||
|
-o UserKnownHostsFile=/dev/null -p "$SSH_PORT" moze@127.0.0.1)
|
||||||
|
|
||||||
|
FAILED=0
|
||||||
|
assert() {
|
||||||
|
local desc="$1"; shift
|
||||||
|
if "${SSHCMD[@]}" "$@" >/dev/null 2>&1; then
|
||||||
|
green " ok $desc"
|
||||||
|
else
|
||||||
|
red " FAIL $desc"
|
||||||
|
FAILED=$((FAILED+1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
assert "/ on btrfs subvol=@" "findmnt -no FSTYPE,OPTIONS / | grep -q 'subvol=/@'"
|
||||||
|
assert "user moze in wheel" "id -nG moze | tr ' ' '\\n' | grep -qx wheel"
|
||||||
|
assert "user moze in docker" "id -nG moze | tr ' ' '\\n' | grep -qx docker"
|
||||||
|
assert "sudo via wheel" "sudo -n -l | grep -q 'ALL'"
|
||||||
|
assert "cinnamon-session present" "command -v cinnamon-session"
|
||||||
|
assert "lightdm enabled" "test -L /var/service/lightdm"
|
||||||
|
assert "docker enabled" "test -L /var/service/docker"
|
||||||
|
assert "vscode (code) installed" "command -v code"
|
||||||
|
assert "nvidia kmod metadata" "modinfo nvidia"
|
||||||
|
assert ".ssh dir mode 700" "test \$(stat -c '%a' /home/moze/.ssh) = 700"
|
||||||
|
assert "id_github private key" "test -f /home/moze/.ssh/id_github"
|
||||||
|
assert "ssh config copied" "test -f /home/moze/.ssh/config"
|
||||||
|
assert "EFI vfat mounted" "findmnt -no FSTYPE /boot/efi | grep -q vfat"
|
||||||
|
assert "EFI/Microsoft preserved" "test -f /boot/efi/EFI/Microsoft/Boot/bootmgfw.efi"
|
||||||
|
assert "GRUB has Void entry" "sudo -n grep -q -i 'Void' /boot/grub/grub.cfg"
|
||||||
|
assert "nix-daemon running" "pgrep -x nix-daemon"
|
||||||
|
assert "zram swap active" "swapon --show=NAME --noheadings | grep -q '^/dev/zram'"
|
||||||
|
assert "hostname is xps9700" "test \$(hostname) = xps9700"
|
||||||
|
assert "timezone is Europe/Zurich" "readlink /etc/localtime | grep -q 'Europe/Zurich'"
|
||||||
|
assert "keymap ch-fr_nodeadkeys" "grep -q 'ch-fr_nodeadkeys' /etc/rc.conf"
|
||||||
|
assert "alacritty installed" "command -v alacritty"
|
||||||
|
assert "no firefox installed" "! command -v firefox"
|
||||||
|
assert "gruvbox theme present" "ls -d /usr/share/themes/Gruvbox* >/dev/null 2>&1"
|
||||||
|
assert "gruvbox-dark exact" "test -d /usr/share/themes/Gruvbox-Dark"
|
||||||
|
assert "gruvbox icons present" "ls -d /usr/share/icons/Gruvbox-Plus-Dark >/dev/null 2>&1"
|
||||||
|
assert "bibata cursor present" "test -d /usr/share/icons/Bibata-Modern-Ice"
|
||||||
|
assert "dconf cursor=bibata" "grep -q \"cursor-theme='Bibata-Modern-Ice'\" /etc/dconf/db/local.d/00-cinnamon"
|
||||||
|
assert "dconf gtk=Gruvbox-Dark" "grep -q \"gtk-theme='Gruvbox-Dark'\" /etc/dconf/db/local.d/00-cinnamon"
|
||||||
|
assert "wallpapers deployed" "test -f /usr/share/backgrounds/void-installer/pxfuel.jpg"
|
||||||
|
assert "user .bashrc deployed" "grep -q 'NVM_DIR' /home/moze/.bashrc"
|
||||||
|
assert "vscode settings deployed" "test -f /home/moze/.config/Code/User/settings.json"
|
||||||
|
assert "X11 keymap pinned ch(fr)" "grep -q 'XkbLayout.*ch' /etc/X11/xorg.conf.d/00-keyboard.conf"
|
||||||
|
assert "dconf cinnamon defaults" "test -f /etc/dconf/db/local.d/00-cinnamon"
|
||||||
|
assert "first-login script staged" "test -x /usr/local/libexec/first-login.sh"
|
||||||
|
assert "first-login autostart" "test -f /etc/xdg/autostart/void-installer-first-login.desktop"
|
||||||
|
assert "local-bin profile.d" "test -f /etc/profile.d/local-bin.sh"
|
||||||
|
assert "claude installer used" "grep -q 'claude.ai/install.sh' /usr/local/libexec/first-login.sh"
|
||||||
|
assert "nix unfree allowed" "grep -q 'NIXPKGS_ALLOW_UNFREE' /usr/local/libexec/first-boot-nix.sh"
|
||||||
|
assert "vscode-extensions list" "test -f /etc/installer-vscode-extensions.txt"
|
||||||
|
assert "cups installed" "command -v cupsd"
|
||||||
|
assert "cups service enabled" "test -L /etc/runit/runsvdir/default/cupsd"
|
||||||
|
assert "timeshift installed" "command -v timeshift"
|
||||||
|
assert "flameshot installed" "command -v flameshot"
|
||||||
|
assert "libinput-gestures present" "command -v libinput-gestures"
|
||||||
|
assert "gestures default config" "test -f /etc/skel/.config/libinput-gestures.conf"
|
||||||
|
assert "btrfs snapshot hook" "test -x /usr/local/sbin/xbps-pre-upgrade-snapshot.sh"
|
||||||
|
assert "xbps-install wrapper" "test -x /usr/local/bin/xbps-install"
|
||||||
|
assert "void-upgrade GUI" "test -x /usr/local/bin/void-upgrade-gui"
|
||||||
|
assert "void-upgrade .desktop" "test -f /usr/share/applications/void-upgrade.desktop"
|
||||||
|
assert "Print Screen -> flameshot" "grep -q 'Print' /etc/dconf/db/local.d/00-cinnamon"
|
||||||
|
assert "tray applets configured" "grep -q 'systray@cinnamon.org' /etc/dconf/db/local.d/00-cinnamon"
|
||||||
|
assert "Nemo VS Code action" "test -f /usr/share/nemo/actions/open-vscode.nemo_action"
|
||||||
|
|
||||||
|
cleanup_all
|
||||||
|
echo
|
||||||
|
if [[ $FAILED -eq 0 ]]; then
|
||||||
|
green "ALL SMOKE TESTS PASSED"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
red "$FAILED smoke test(s) failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
35
tools/start-xbps-proxy.sh
Executable file
35
tools/start-xbps-proxy.sh
Executable file
@@ -0,0 +1,35 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Start the XBPS caching proxy in the background.
|
||||||
|
# Idempotent: does nothing if already running.
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
CACHE_DIR="${XBPS_CACHE_DIR:-$PROJECT_DIR/cache/xbps-pkgs}"
|
||||||
|
PORT="${XBPS_PROXY_PORT:-3142}"
|
||||||
|
PIDFILE="${XBPS_PROXY_PIDFILE:-$PROJECT_DIR/out/xbps-proxy.pid}"
|
||||||
|
LOGFILE="${XBPS_PROXY_LOG:-$PROJECT_DIR/out/xbps-proxy.log}"
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$PIDFILE")" "$CACHE_DIR"
|
||||||
|
|
||||||
|
# Already running?
|
||||||
|
if [[ -f "$PIDFILE" ]] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
|
||||||
|
echo ">>> XBPS proxy already running (pid $(cat "$PIDFILE"), port $PORT)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
XBPS_CACHE_DIR="$CACHE_DIR" \
|
||||||
|
XBPS_PROXY_PORT="$PORT" \
|
||||||
|
python3 "$PROJECT_DIR/tools/xbps-proxy.py" > "$LOGFILE" 2>&1 &
|
||||||
|
echo $! > "$PIDFILE"
|
||||||
|
|
||||||
|
# Wait for the socket to open (up to 5 s).
|
||||||
|
for i in $(seq 1 20); do
|
||||||
|
if curl -sf --max-time 1 "http://127.0.0.1:$PORT/" >/dev/null 2>&1; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 0.25
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ">>> XBPS proxy started (pid $(cat "$PIDFILE"), port $PORT)"
|
||||||
|
echo " cache : $CACHE_DIR"
|
||||||
|
echo " log : $LOGFILE"
|
||||||
19
tools/stop-xbps-proxy.sh
Executable file
19
tools/stop-xbps-proxy.sh
Executable file
@@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Stop the XBPS caching proxy.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
PIDFILE="${XBPS_PROXY_PIDFILE:-$PROJECT_DIR/out/xbps-proxy.pid}"
|
||||||
|
|
||||||
|
if [[ -f "$PIDFILE" ]]; then
|
||||||
|
PID="$(cat "$PIDFILE")"
|
||||||
|
if kill -0 "$PID" 2>/dev/null; then
|
||||||
|
kill "$PID"
|
||||||
|
echo ">>> XBPS proxy stopped (pid $PID)"
|
||||||
|
else
|
||||||
|
echo ">>> XBPS proxy not running (stale pidfile)"
|
||||||
|
fi
|
||||||
|
rm -f "$PIDFILE"
|
||||||
|
else
|
||||||
|
echo ">>> no XBPS proxy pidfile found"
|
||||||
|
fi
|
||||||
160
tools/xbps-proxy.py
Normal file
160
tools/xbps-proxy.py
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
XBPS caching HTTP reverse proxy.
|
||||||
|
Cache-first, web fallback. Caches .xbps packages forever; never caches
|
||||||
|
-repodata files (must stay fresh for xbps to see new package versions).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
XBPS_CACHE_DIR=./cache/xbps-pkgs \\
|
||||||
|
XBPS_UPSTREAM=https://repo-default.voidlinux.org \\
|
||||||
|
XBPS_PROXY_PORT=3142 \\
|
||||||
|
python3 tools/xbps-proxy.py
|
||||||
|
"""
|
||||||
|
import http.server
|
||||||
|
import socket
|
||||||
|
import time
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Force IPv4 only — many hosts have no working IPv6 route to voidlinux mirrors,
|
||||||
|
# which surfaces as `[Errno 101] Network is unreachable` mid-download. Pin
|
||||||
|
# socket.getaddrinfo to AF_INET so urllib never tries an AAAA record.
|
||||||
|
_orig_getaddrinfo = socket.getaddrinfo
|
||||||
|
|
||||||
|
|
||||||
|
def _ipv4_only(host, port, family=0, type=0, proto=0, flags=0): # noqa: A002
|
||||||
|
return _orig_getaddrinfo(host, port, socket.AF_INET, type, proto, flags)
|
||||||
|
|
||||||
|
|
||||||
|
socket.getaddrinfo = _ipv4_only
|
||||||
|
|
||||||
|
UPSTREAM = os.environ.get("XBPS_UPSTREAM", "https://repo-default.voidlinux.org")
|
||||||
|
CACHE_DIR = Path(os.environ.get("XBPS_CACHE_DIR", "./cache/xbps-pkgs")).resolve()
|
||||||
|
PORT = int(os.environ.get("XBPS_PROXY_PORT", "3142"))
|
||||||
|
BIND = os.environ.get("XBPS_PROXY_BIND", "0.0.0.0")
|
||||||
|
|
||||||
|
# Suffixes that must always be fetched fresh (small metadata files).
|
||||||
|
ALWAYS_FRESH = ("-repodata",)
|
||||||
|
|
||||||
|
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def should_cache(path: str) -> bool:
|
||||||
|
for suffix in ALWAYS_FRESH:
|
||||||
|
if path.endswith(suffix):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class Handler(http.server.BaseHTTPRequestHandler):
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
self._handle(head_only=False)
|
||||||
|
|
||||||
|
def do_HEAD(self):
|
||||||
|
self._handle(head_only=True)
|
||||||
|
|
||||||
|
def _handle(self, head_only: bool):
|
||||||
|
path = self.path.lstrip("/")
|
||||||
|
if not path:
|
||||||
|
self.send_error(404, "empty path")
|
||||||
|
return
|
||||||
|
fpath = CACHE_DIR / path
|
||||||
|
hit = should_cache(path) and fpath.is_file()
|
||||||
|
|
||||||
|
if hit:
|
||||||
|
self._serve_cached(fpath, head_only)
|
||||||
|
print(f"[HIT ] {path}", flush=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
upstream_url = f"{UPSTREAM}/{path}"
|
||||||
|
print(f"[MISS] {path} <- {upstream_url}", flush=True)
|
||||||
|
last_exc = None
|
||||||
|
for attempt in range(3):
|
||||||
|
try:
|
||||||
|
self._fetch_upstream(path, fpath, upstream_url, head_only)
|
||||||
|
return
|
||||||
|
except urllib.error.HTTPError as exc:
|
||||||
|
print(f"[ERR ] {path} HTTP {exc.code}", flush=True)
|
||||||
|
try:
|
||||||
|
self.send_error(exc.code, exc.reason)
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
except (urllib.error.URLError, OSError) as exc:
|
||||||
|
last_exc = exc
|
||||||
|
if attempt < 2:
|
||||||
|
print(f"[WARN] {path} attempt {attempt + 1} failed: {exc}; retrying", flush=True)
|
||||||
|
time.sleep(1 + attempt)
|
||||||
|
print(f"[ERR ] {path} {last_exc}", flush=True)
|
||||||
|
try:
|
||||||
|
self.send_error(503, str(last_exc))
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _fetch_upstream(self, path, fpath, upstream_url, head_only):
|
||||||
|
req = urllib.request.Request(upstream_url)
|
||||||
|
req.get_method = lambda: "HEAD" if head_only else "GET"
|
||||||
|
with urllib.request.urlopen(req, timeout=120) as resp:
|
||||||
|
ctype = resp.headers.get("Content-Type", "application/octet-stream")
|
||||||
|
clen = resp.headers.get("Content-Length")
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", ctype)
|
||||||
|
if clen:
|
||||||
|
self.send_header("Content-Length", clen)
|
||||||
|
self.end_headers()
|
||||||
|
if head_only:
|
||||||
|
return
|
||||||
|
|
||||||
|
cache_this = should_cache(path)
|
||||||
|
tmp = None
|
||||||
|
tmp_fh = None
|
||||||
|
if cache_this:
|
||||||
|
fpath.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
tmp = fpath.with_suffix(fpath.suffix + ".tmp")
|
||||||
|
tmp_fh = open(tmp, "wb")
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
chunk = resp.read(64 * 1024)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
self.wfile.write(chunk)
|
||||||
|
if tmp_fh is not None:
|
||||||
|
tmp_fh.write(chunk)
|
||||||
|
finally:
|
||||||
|
if tmp_fh is not None:
|
||||||
|
tmp_fh.close()
|
||||||
|
if cache_this and tmp is not None and tmp.exists() and not fpath.is_dir():
|
||||||
|
tmp.rename(fpath)
|
||||||
|
|
||||||
|
def _serve_cached(self, fpath: Path, head_only: bool):
|
||||||
|
size = fpath.stat().st_size
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "application/octet-stream")
|
||||||
|
self.send_header("Content-Length", str(size))
|
||||||
|
self.end_headers()
|
||||||
|
if head_only:
|
||||||
|
return
|
||||||
|
with open(fpath, "rb") as fh:
|
||||||
|
while True:
|
||||||
|
chunk = fh.read(64 * 1024)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
self.wfile.write(chunk)
|
||||||
|
|
||||||
|
# Silence default access log — we do our own.
|
||||||
|
def log_message(self, *_):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
server = http.server.ThreadingHTTPServer((BIND, PORT), Handler)
|
||||||
|
print(f"XBPS proxy : {BIND}:{PORT} -> {UPSTREAM}", flush=True)
|
||||||
|
print(f"Cache dir : {CACHE_DIR}", flush=True)
|
||||||
|
try:
|
||||||
|
server.serve_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nProxy stopped.", flush=True)
|
||||||
Reference in New Issue
Block a user