fix(install): pre-activation, writable home.nix, keyboard/locale selection

- Pre-activation failure ("desktop pre-activation failed"): the in-chroot
  HM build hard-coded `--option substituters ""`, which only works on
  OFFLINE installs (where the ISO store is copied in). Online installs
  (now common — wifi works) had no pins and built from source. Fix: build
  the generation in the live env (its store always has the pins), nix copy
  the closure to the target, chroot only activates. In-chroot build kept
  as fallback. Uses path: (not git+file:) to sidestep libgit2 ownership.
- Read-only home.nix: templates come out of the store mode 0444 and cp
  preserves it — chmod -R u+w after staging so the user can edit it.
- Keyboard + locale selection: gum prompts (localectl layouts/variants +
  curated UTF-8 locale list) and NOMARCHY_LOCALE/KB_LAYOUT/KB_VARIANT for
  unattended. One choice flows everywhere — services.xserver.xkb (source
  of truth) + console.useXkbConfig (tty/greeter) + boot.initrd.systemd
  (so the LUKS passphrase prompt uses the layout; script initrd can't) +
  the generated home.nix's nomarchy.keyboard.* for the Hyprland session.
- test-install.sh: NOMARCHY_OVMF env override for sandboxed OVMF paths.

Verified end-to-end in QEMU: offline LUKS install completes, boots to a
themed desktop (no autogenerated-config banner), Plymouth renders the LUKS
prompt on the theme-tinted background.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Bernardo Magri
2026-06-13 07:38:31 +01:00
parent 465f013155
commit 6763bef73e
3 changed files with 99 additions and 8 deletions

View File

@@ -16,6 +16,7 @@
# NOMARCHY_UNATTENDED=1 NOMARCHY_DISK=/dev/vda NOMARCHY_USERNAME=me \
# NOMARCHY_PASSWORD=secret [NOMARCHY_HOSTNAME=nomarchy] \
# [NOMARCHY_TIMEZONE=UTC] [NOMARCHY_LUKS_PASSPHRASE=...] \
# [NOMARCHY_LOCALE=en_US.UTF-8] [NOMARCHY_KB_LAYOUT=us] [NOMARCHY_KB_VARIANT=] \
# [NOMARCHY_SWAP_GB=N (default: RAM size; 0 = none)] \
# [NOMARCHY_HW="auto"|"none"|"mod1 mod2"] [NOMARCHY_FINISH=none|reboot|poweroff]
# nomarchy-install
@@ -150,6 +151,9 @@ if [[ "$UNATTENDED" == "1" ]]; then
PASSWORD="${NOMARCHY_PASSWORD:?}"
HOSTNAME_="${NOMARCHY_HOSTNAME:-nomarchy}"
TIMEZONE="${NOMARCHY_TIMEZONE:-UTC}"
LOCALE="${NOMARCHY_LOCALE:-en_US.UTF-8}"
KB_LAYOUT="${NOMARCHY_KB_LAYOUT:-us}"
KB_VARIANT="${NOMARCHY_KB_VARIANT:-}"
else
while true; do
USERNAME=$(gum input --placeholder "username (lowercase, e.g. ada)")
@@ -169,6 +173,26 @@ else
warn "Invalid hostname."
done
TIMEZONE=$(timedatectl list-timezones | gum filter --placeholder "timezone (type to search)" || echo UTC)
# Curated UTF-8 locales: the live system only generates en_US, so
# `locale -a` can't enumerate what the TARGET could use.
LOCALE=$(printf '%s\n' \
en_US en_GB de_DE fr_FR es_ES es_MX pt_PT pt_BR it_IT nl_NL \
pl_PL ru_RU uk_UA cs_CZ sk_SK sv_SE nb_NO da_DK fi_FI tr_TR \
el_GR hu_HU ro_RO bg_BG hr_HR sl_SI lt_LT lv_LV et_EE ja_JP \
ko_KR zh_CN zh_TW ar_EG he_IL hi_IN th_TH vi_VN id_ID \
| sed 's/$/.UTF-8/' \
| gum filter --placeholder "language / locale (type to search)" \
|| echo en_US.UTF-8)
KB_LAYOUT=$(localectl list-x11-keymap-layouts 2>/dev/null \
| gum filter --placeholder "keyboard layout (type to search)" \
|| echo us)
KB_VARIANT=""
if [[ "$KB_LAYOUT" != "us" ]] || gum confirm --default=No "Pick a keyboard variant (intl, nodeadkeys, …)?"; then
KB_VARIANT=$( { echo "(none)"; localectl list-x11-keymap-variants "$KB_LAYOUT" 2>/dev/null; } \
| gum filter --placeholder "variant for $KB_LAYOUT (pick '(none)' for the default)" \
|| echo "(none)")
[[ "$KB_VARIANT" == "(none)" ]] && KB_VARIANT=""
fi
fi
[[ "$USERNAME" =~ ^[a-z_][a-z0-9_-]*$ ]] || fail "Invalid username '$USERNAME'."
[[ -f "/usr/share/zoneinfo/$TIMEZONE" || -e "/etc/zoneinfo/$TIMEZONE" ]] \
@@ -177,6 +201,7 @@ fi
HASHED_PASSWORD=$(printf '%s' "$PASSWORD" | mkpasswd -m sha-512 -s)
unset PASSWORD
info "User: $USERNAME @ $HOSTNAME_ ($TIMEZONE)"
info "Locale: $LOCALE · keyboard: $KB_LAYOUT${KB_VARIANT:+ ($KB_VARIANT)}"
# ─── Hardware profile ───────────────────────────────────────────────────
section "Hardware detection"
@@ -320,9 +345,35 @@ nixos-generate-config --root /mnt
mv /mnt/etc/nixos/hardware-configuration.nix "$FLAKE_DIR/"
rm -rf /mnt/etc/nixos
cp "$SHARE/template/home.nix" "$FLAKE_DIR/"
cp "$SHARE/template/theme-state.json" "$FLAKE_DIR/"
# home.nix is generated (not copied from the template) so the chosen
# keyboard layout reaches the Hyprland session — standalone HM cannot
# read system.nix.
cat > "$FLAKE_DIR/home.nix" <<EOF
# Your user environment. The Nomarchy desktop (Hyprland, Waybar,
# Ghostty, theming engine, Stylix) comes from homeModules.nomarchy;
# tune it via the nomarchy.* options, add your own packages and
# programs below.
{ pkgs, ... }:
{
# Keyboard for the desktop session; console + LUKS prompt get the
# same layout from system.nix (xkb + console.useXkbConfig).
nomarchy.keyboard.layout = "$KB_LAYOUT";
nomarchy.keyboard.variant = "$KB_VARIANT";
# Examples:
# nomarchy.terminal = "kitty"; # swap the default terminal
# nomarchy.waybar.enable = false; # bring your own bar
# nomarchy.stylix.enable = false; # opt out of GTK/Qt theming
home.packages = with pkgs; [
# firefox
];
}
EOF
hw_nix=""
for p in "${HW_PROFILES[@]:-}"; do
[[ -n "$p" ]] && hw_nix+=" \"$p\""
@@ -371,7 +422,17 @@ cat > "$FLAKE_DIR/system.nix" <<EOF
networking.hostName = "$HOSTNAME_";
time.timeZone = "$TIMEZONE";
i18n.defaultLocale = "en_US.UTF-8";
i18n.defaultLocale = "$LOCALE";
# One keyboard layout everywhere: xkb is the source of truth, the
# virtual console derives from it (greeter/ttys), and the systemd
# initrd applies it to the LUKS passphrase prompt too (the classic
# script initrd cannot). The Hyprland session reads the same layout
# from nomarchy.keyboard.* in home.nix.
services.xserver.xkb.layout = "$KB_LAYOUT";
services.xserver.xkb.variant = "$KB_VARIANT";
console.useXkbConfig = true;
boot.initrd.systemd.enable = true;
# Your login user — \`username\` flows in from flake.nix automatically.
users.users.\${username} = {
@@ -410,6 +471,9 @@ fi
# outright. The first normal NixOS user is always 1000:users(100); the
# account doesn't exist in the target yet, so numeric ids it is.
chown -R 1000:100 "$FLAKE_DIR"
# The templates come out of the nix store mode 0444 and cp preserves
# that — without this the user owns home.nix but can't edit it.
chmod -R u+w "$FLAKE_DIR"
# /etc/nixos on the installed system points at the user-owned flake.
mkdir -p /mnt/etc
@@ -464,6 +528,25 @@ success "System installed (bootloader in place)"
# fully themed desktop, not bare Hyprland. Best-effort: a failure here
# only costs the user one `home-manager switch` after logging in.
section "Baking the desktop"
# Build the generation HERE in the live environment, not in the chroot:
# the live store always carries the offline pin set, while the target
# store only gets the full ISO store on OFFLINE installs — an online
# install skips that copy, and the old in-chroot build (substituters
# deliberately empty) then tried to compile the world from source.
# Building against the live store is the exact path the live session's
# own theme switching exercises, online or off.
info "Building the desktop generation..."
hm_out=""
if hm_out=$(nix --extra-experimental-features "nix-command flakes" \
build --no-link --print-out-paths \
--option substituters "" \
"path:$FLAKE_DIR#homeConfigurations.$USERNAME.activationPackage"); then
nix --extra-experimental-features "nix-command flakes" \
copy --no-check-sigs --to "local?root=/mnt" "$hm_out" \
|| hm_out=""
fi
[[ -n "$hm_out" ]] || warn "live-side desktop build failed — retrying inside the chroot"
# NOT /mnt/tmp: nixos-enter mounts a fresh tmpfs over /tmp inside the
# chroot, which silently vaporizes any script staged there (cost us a
# full verification round to find). /root persists into the chroot.
@@ -471,10 +554,15 @@ cat > /mnt/root/nomarchy-hm-activate.sh <<EOF
set -ex
exec > /var/log/nomarchy-hm-preactivate.log 2>&1
export PATH=/run/current-system/sw/bin:\$PATH
out=\$(nix --extra-experimental-features "nix-command flakes" \
build --no-link --print-out-paths \
--option substituters "" \
"path:/home/$USERNAME/.nomarchy#homeConfigurations.$USERNAME.activationPackage")
# Normally pre-built in the live env and copied over; the in-chroot
# build (default substituters — the live-side build already proved the
# no-network case) is a last-resort fallback.
out="$hm_out"
if [ -z "\$out" ]; then
out=\$(nix --extra-experimental-features "nix-command flakes" \
build --no-link --print-out-paths \
"path:/home/$USERNAME/.nomarchy#homeConfigurations.$USERNAME.activationPackage")
fi
install -d -o "$USERNAME" -g users /nix/var/nix/profiles/per-user/$USERNAME
install -d -o "$USERNAME" -g users /nix/var/nix/gcroots/per-user/$USERNAME
# activate's profile ops need store access; as the user that means a