diff --git a/core/system/default.nix b/core/system/default.nix index 12805e3..eedc3be 100644 --- a/core/system/default.nix +++ b/core/system/default.nix @@ -4,6 +4,7 @@ imports = [ ./options.nix ./state.nix + ./branding.nix ./graphics.nix ./nix.nix ./scripts.nix diff --git a/flake.nix b/flake.nix index 7e1433c..d2ccdcf 100644 --- a/flake.nix +++ b/flake.nix @@ -38,13 +38,14 @@ system = "x86_64-linux"; lib = nixpkgs.lib; - # Overlays - overlays = [ - (final: prev: { - makima-bin = prev.makima; - nomarchy-system-scripts = final.callPackage ./core/system/scripts-derivation.nix { }; - }) - ]; + # Overlays — the function form lives here so both Nomarchy itself and + # any downstream flake (see `overlays.default` in the outputs) can layer + # the same Nomarchy packages onto their nixpkgs. + nomarchyOverlay = final: prev: { + makima-bin = prev.makima; + nomarchy-system-scripts = final.callPackage ./core/system/scripts-derivation.nix { }; + }; + overlays = [ nomarchyOverlay ]; pkgs = import nixpkgs { inherit system overlays; @@ -90,6 +91,12 @@ # Expose inputs for downstream use inherit inputs; + # Downstream flakes consume this to layer Nomarchy's packages onto their + # own nixpkgs. Used by the installer-generated flake so both the + # nixosSystem and the standalone homeManagerConfiguration share one + # `pkgs` with the overlay applied. + overlays.default = nomarchyOverlay; + nixosModules = { system = import ./core; home = import ./features; diff --git a/installer/disko-golden.nix b/installer/disko-golden.nix index c2d630a..56d1c10 100644 --- a/installer/disko-golden.nix +++ b/installer/disko-golden.nix @@ -17,12 +17,13 @@ content = { type = "gpt"; partitions = { - # EFI System Partition + # EFI System Partition. 1 GiB leaves room for several kernel + # generations + initrd + Plymouth assets without filling up. ESP = { priority = 1; name = "ESP"; start = "1M"; - end = "512M"; + end = "1G"; type = "EF00"; content = { type = "filesystem"; @@ -32,16 +33,17 @@ }; }; - # LUKS-encrypted root partition + # LUKS-encrypted root partition. The installer writes the + # passphrase to an in-memory tmpfs (/dev/shm/nomarchy-luks.key) + # rather than the spinning /tmp so the secret never touches disk. luks = { size = "100%"; content = { type = "luks"; name = "crypted"; - # Password will be provided via /tmp/secret.key settings = { allowDiscards = true; # Enable TRIM for SSDs - passwordFile = "/tmp/secret.key"; + passwordFile = "/dev/shm/nomarchy-luks.key"; }; content = { type = "btrfs"; @@ -76,6 +78,13 @@ mountpoint = "/var/log"; mountOptions = [ "compress=zstd" "noatime" ]; }; + + # Snapshots — kept off the rolled-back root so tools like + # snapper / btrbk / nomarchy-rollback have a stable home. + "@snapshots" = { + mountpoint = "/.snapshots"; + mountOptions = [ "compress=zstd" "noatime" ]; + }; }; # Create a read-only snapshot of root for impermanence rollback diff --git a/installer/install.sh b/installer/install.sh index d0fe29b..5f348be 100755 --- a/installer/install.sh +++ b/installer/install.sh @@ -8,6 +8,13 @@ set -e # For a customized installation, manually set up your disk and use the generated # flake configuration as a starting point. +# Load the hardware-detection database — resolved relative to this script so it +# works whether we're invoked from /etc/install.sh on the live ISO or straight +# from a checkout. +_NOMARCHY_INSTALL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=hardware-db.sh +source "$_NOMARCHY_INSTALL_DIR/hardware-db.sh" + # Colors and styling RED='\033[0;31m' GREEN='\033[0;32m' @@ -19,6 +26,8 @@ BOLD='\033[1m' # Installer state NOMARCHY_REPO="" +NOMARCHY_REV="" +HOSTNAME="" TARGET_DRIVE="" USERNAME="" LUKS_PASSWORD="" @@ -93,6 +102,18 @@ check_environment() { fi success "Found Nomarchy at $NOMARCHY_REPO" + # Capture the exact commit we're installing from. The generated flake + # pins `nomarchy.url` to this revision so the installed system can't + # silently drift onto a newer (possibly breaking) main. + if command -v git >/dev/null 2>&1 && [[ -d "$NOMARCHY_REPO/.git" ]]; then + NOMARCHY_REV=$(git -C "$NOMARCHY_REPO" rev-parse HEAD 2>/dev/null || echo "") + fi + if [[ -n "$NOMARCHY_REV" ]]; then + success "Pinning Nomarchy to $NOMARCHY_REV" + else + info "Could not determine Nomarchy revision; downstream flake will track main." + fi + # Check internet gum spin --spinner dot --title "Checking internet connection..." -- sleep 1 while ! ping -c 1 -W 2 1.1.1.1 &>/dev/null; do @@ -184,6 +205,13 @@ configure_user() { success "Username: $USERNAME" + HOSTNAME=$(nrun gum input --value "nomarchy" --placeholder "Hostname for this machine") + if [[ -z "$HOSTNAME" ]] || [[ ! "$HOSTNAME" =~ ^[a-z0-9][a-z0-9-]*$ ]]; then + error "Invalid hostname (use lowercase letters, digits, and hyphens only)" + exit 1 + fi + success "Hostname: $HOSTNAME" + # User password (can be same as LUKS or different) info "Set a password for your user account" local pass1 pass2 @@ -226,66 +254,147 @@ select_timezone() { select_hardware() { section "Hardware Configuration" - local product_name cpu_vendor - product_name=$(cat /sys/class/dmi/id/product_name 2>/dev/null || echo "Unknown") - cpu_vendor=$(lscpu 2>/dev/null | grep "Vendor ID" | awk '{print $3}' || echo "Unknown") - - info "Detected: $product_name" - info "CPU: $cpu_vendor" + local dmi_vendor dmi_product detect_output + dmi_vendor=$(cat /sys/class/dmi/id/sys_vendor 2>/dev/null || echo "Unknown") + dmi_product=$(cat /sys/class/dmi/id/product_name 2>/dev/null || echo "Unknown") + info "DMI: $dmi_vendor / $dmi_product" echo "" - # Set CPU-specific module - if [[ "$cpu_vendor" == "AuthenticAMD" ]]; then - HARDWARE_MODULES="inputs.nixos-hardware.nixosModules.common-cpu-amd" - elif [[ "$cpu_vendor" == "GenuineIntel" ]]; then - HARDWARE_MODULES="inputs.nixos-hardware.nixosModules.common-cpu-intel" - fi + # Auto-detect CPU, GPU, chassis, and known model from hardware-db.sh. + detect_output=$(nomarchy_detect_hw || true) - local vendor - vendor=$(gum choose --header "Select hardware vendor" \ - "Generic (Auto-detect)" \ - "Framework" \ - "Dell" \ - "Lenovo" \ - "Apple (T2 Mac)" \ - "Microsoft Surface" \ - "Other...") + echo "Auto-detected:" + nomarchy_hw_summary <<< "$detect_output" + echo "" - case "$vendor" in - *Framework*) - local model - model=$(gum choose "16-7040-amd" "13-7040-amd" "13-intel-13th-gen" "13-intel-12th-gen") - HARDWARE_MODULES="$HARDWARE_MODULES\n inputs.nixos-hardware.nixosModules.framework-$model" - NOMARCHY_HW_OPTS="nomarchy.hardware.isFramework = true;" + # Collect modules + nomarchy options from the detector output. + local modules=() hw_opts=() + while IFS= read -r line; do + case "$line" in + "MODULE "*) modules+=("${line#MODULE }") ;; + "OPT "*) hw_opts+=("${line#OPT }") ;; + esac + done <<< "$detect_output" + + # Let the user accept, extend, or replace the detection. + local choice + choice=$(nrun gum choose --header "Hardware configuration" \ + "Accept detected modules" \ + "Add an extra nixos-hardware module" \ + "Pick from the manual list (override)") + + case "$choice" in + "Add an extra nixos-hardware module") + local extra + extra=$(nrun gum input --placeholder "e.g. asus-zephyrus-ga401 (no 'nixos-hardware.' prefix)") + [[ -n "$extra" ]] && modules+=("$extra") ;; - *Dell*) - local model - model=$(gum choose "xps-15-9500" "xps-15-9510" "xps-13-9310" "xps-13-9380" "precision-5530") - HARDWARE_MODULES="$HARDWARE_MODULES\n inputs.nixos-hardware.nixosModules.dell-$model" - [[ "$model" == *"xps"* ]] && NOMARCHY_HW_OPTS="nomarchy.hardware.isXPS = true;" - ;; - *Lenovo*) - local model - model=$(gum choose "thinkpad-x1-carbon-gen10" "thinkpad-t14-amd" "thinkpad-t480" "thinkpad-x1-extreme") - HARDWARE_MODULES="$HARDWARE_MODULES\n inputs.nixos-hardware.nixosModules.lenovo-$model" - ;; - *Apple*) - NOMARCHY_HW_OPTS="nomarchy.hardware.isT2Mac = true;" - ;; - *Surface*) - local model - model=$(gum choose "surface-pro-9" "surface-pro-8" "surface-laptop-4") - HARDWARE_MODULES="$HARDWARE_MODULES\n inputs.nixos-hardware.nixosModules.microsoft-$model" - ;; - *Other*) - info "Enter nixos-hardware module path (or leave empty):" - local custom_mod - custom_mod=$(gum input --placeholder "e.g., inputs.nixos-hardware.nixosModules.asus-zephyrus-ga401") - [[ -n "$custom_mod" ]] && HARDWARE_MODULES="$HARDWARE_MODULES\n $custom_mod" + "Pick from the manual list (override)") + modules=() + hw_opts=() + _select_hardware_manual modules hw_opts ;; esac - success "Hardware configuration set" + # De-duplicate while preserving order. + local seen="" uniq_mods=() m + for m in "${modules[@]}"; do + if [[ ":$seen:" != *":$m:"* ]]; then + uniq_mods+=("$m") + seen="$seen:$m" + fi + done + + # Emit a list the heredoc in generate_flake_config splats into + # hardware-selection.nix's imports. The heredoc already indents the first + # line by 4 spaces — we add real newlines + 4 spaces (via $'\n ') for + # subsequent lines so every entry lines up. + HARDWARE_MODULES="" + for m in "${uniq_mods[@]}"; do + [[ -z "$HARDWARE_MODULES" ]] || HARDWARE_MODULES+=$'\n ' + HARDWARE_MODULES+="inputs.nixos-hardware.nixosModules.${m}" + done + + # Same treatment for nomarchy.hardware.* toggles. + NOMARCHY_HW_OPTS="" + local o + for o in "${hw_opts[@]}"; do + # opt is e.g. `isFramework=true` → `nomarchy.hardware.isFramework = true;` + local key="${o%%=*}" val="${o#*=}" + NOMARCHY_HW_OPTS+="nomarchy.hardware.${key} = ${val};"$'\n ' + done + + success "Hardware configuration set (${#uniq_mods[@]} module$([[ ${#uniq_mods[@]} -eq 1 ]] || echo s))" +} + +# Manual fallback menu, kept for odd hardware the DB doesn't recognise yet. +# Writes into the two arrays named by its arguments (bash 4.3+ nameref). +_select_hardware_manual() { + local -n _mods_ref="$1" + local -n _opts_ref="$2" + + local vendor + vendor=$(nrun gum choose --header "Pick vendor" \ + "Framework" "Dell" "Lenovo" "Apple (T2 Mac)" "Microsoft Surface" "ASUS" "System76" "Other...") + + case "$vendor" in + Framework) + local model + model=$(nrun gum choose \ + "framework-16-7040-amd" \ + "framework-13-amd-ai-300-series" \ + "framework-13-7040-amd" \ + "framework-13-13th-gen-intel" \ + "framework-13-12th-gen-intel" \ + "framework-13-11th-gen-intel") + _mods_ref+=("$model") + _opts_ref+=("isFramework=true") + ;; + Dell) + local model + model=$(nrun gum choose \ + "dell-xps-15-9500" "dell-xps-15-9510" "dell-xps-15-9520" \ + "dell-xps-13-9310" "dell-xps-13-9370" "dell-xps-13-9380" \ + "dell-precision-5530" "dell-latitude-7480") + _mods_ref+=("$model") + [[ "$model" == *xps* ]] && _opts_ref+=("isXPS=true") + ;; + Lenovo) + local model + model=$(nrun gum choose \ + "lenovo-thinkpad-x1-carbon-gen11" "lenovo-thinkpad-x1-carbon-gen10" \ + "lenovo-thinkpad-x1-carbon-gen9" "lenovo-thinkpad-x1-extreme" \ + "lenovo-thinkpad-t14-amd-gen3" "lenovo-thinkpad-t14-amd-gen2" \ + "lenovo-thinkpad-t480" "lenovo-thinkpad-l13") + _mods_ref+=("$model") + ;; + "Apple (T2 Mac)") + _mods_ref+=("apple-t2") + _opts_ref+=("isT2Mac=true") + ;; + "Microsoft Surface") + local model + model=$(nrun gum choose \ + "microsoft-surface-pro-9" "microsoft-surface-pro-8" "microsoft-surface-pro-7" \ + "microsoft-surface-laptop-5" "microsoft-surface-laptop-4" "microsoft-surface-laptop-3") + _mods_ref+=("$model") + ;; + ASUS) + local model + model=$(nrun gum choose \ + "asus-zephyrus-ga403" "asus-zephyrus-ga402" "asus-zephyrus-ga401" \ + "asus-zephyrus-ga503" "asus-rog-strix-g513" "asus-zenbook-ux") + _mods_ref+=("$model") + ;; + System76) + _mods_ref+=("system76") + ;; + "Other...") + local custom + custom=$(nrun gum input --placeholder "e.g. asus-zephyrus-ga401 (no 'nixos-hardware.' prefix)") + [[ -n "$custom" ]] && _mods_ref+=("$custom") + ;; + esac } # ============================================================================ @@ -316,9 +425,11 @@ review_configuration() { section "Review Configuration" echo " Drive: $TARGET_DRIVE (BTRFS + LUKS2)" + echo " Hostname: $HOSTNAME" echo " Username: $USERNAME" echo " Timezone: $TIMEZONE" echo " Impermanence: $ENABLE_IMPERMANENCE" + echo " Nomarchy rev: ${NOMARCHY_REV:-main (unpinned)}" echo "" nrun gum style --foreground 9 "This will DESTROY all data on $TARGET_DRIVE" @@ -345,10 +456,16 @@ execute_installation() { sed "s|@TARGET_DRIVE@|${TARGET_DRIVE}|g" "$disko_file" > "$tmp_disko" - # Provide the LUKS passphrase via a temporary file for disk encryption - echo -n "$LUKS_PASSWORD" > /tmp/secret.key + # Provide the LUKS passphrase via tmpfs so the secret never touches a + # spinning disk. /dev/shm is tmpfs on the live ISO. We restrict perms + # to root and shred the file (overwrite) on the way out, even though + # it's already in RAM — defense in depth. + local luks_key="/dev/shm/nomarchy-luks.key" + install -m 600 /dev/null "$luks_key" + printf '%s' "$LUKS_PASSWORD" > "$luks_key" disko --mode disko "$tmp_disko" - rm /tmp/secret.key + shred -u "$luks_key" 2>/dev/null || rm -f "$luks_key" + unset LUKS_PASSWORD success "Disk partitioned" # 9.2 Generate hardware config @@ -362,7 +479,16 @@ execute_installation() { generate_flake_config success "Configuration generated" - # 9.4 Initialize git repo + # 9.4 Resolve inputs once, here, and lock them. First boot then consumes + # the same flake.lock and doesn't re-resolve a newer upstream. + info "Resolving flake inputs (this pins nomarchy, nixpkgs, etc.)..." + ( + cd /mnt/etc/nixos + nix --extra-experimental-features "nix-command flakes" flake lock >/dev/null + ) + success "flake.lock written" + + # 9.5 Initialize git repo so `nix` treats /etc/nixos as a flake worktree. info "Initializing git repository..." ( cd /mnt/etc/nixos @@ -374,7 +500,7 @@ execute_installation() { ) success "Git repository initialized" - # 9.5 Handle impermanence + # 9.6 Handle impermanence if [[ "$ENABLE_IMPERMANENCE" == "true" ]]; then info "Setting up impermanence..." mkdir -p /mnt/persist/etc @@ -384,9 +510,30 @@ execute_installation() { success "Impermanence configured" fi - # 9.6 Install + # 9.7 Install the NixOS system from the freshly-generated flake. info "Running nixos-install (this will take a while)..." - nixos-install --flake /mnt/etc/nixos#default --no-root-passwd + nixos-install --flake "/mnt/etc/nixos#$HOSTNAME" --no-root-passwd + success "NixOS installed" + + # 9.8 Activate Home Manager for $USERNAME inside the new system so the + # user's first login already has Nomarchy's dotfiles. `home-manager + # switch` must run as the target user with a real $HOME, so we use + # `runuser` (sudo -u keeps the caller's HOME → files land in /root). + info "Activating Home Manager for $USERNAME..." + if nixos-enter --root /mnt -- bash -c " + set -e + install -d -o '$USERNAME' -g users -m 0755 '/home/$USERNAME' + runuser -u '$USERNAME' -- env HOME='/home/$USERNAME' \ + nix --extra-experimental-features 'nix-command flakes' \ + run 'home-manager/release-25.11' -- switch \ + --flake '/etc/nixos#$USERNAME' --impure + "; then + success "Home Manager activated" + else + error "Home Manager activation failed (non-fatal)." + info "Run \`nomarchy-env-update\` after the first login to retry." + fi + success "Installation complete!" } @@ -398,14 +545,25 @@ generate_flake_config() { local impermanence_opt="" [[ "$ENABLE_IMPERMANENCE" == "true" ]] && impermanence_opt="nomarchy.system.impermanence.enable = true;" - # flake.nix - cat > /mnt/etc/nixos/flake.nix << 'FLAKE_EOF' + # Pin the upstream Nomarchy flake to the exact commit we're installing + # from so the first post-reboot `nixos-rebuild` doesn't silently pull a + # newer main. Fall back to tracking main if we couldn't resolve a SHA. + local nomarchy_url + if [[ -n "$NOMARCHY_REV" ]]; then + nomarchy_url="github:bemagri/nomarchy/$NOMARCHY_REV" + else + nomarchy_url="github:bemagri/nomarchy" + fi + + # flake.nix — the generator uses a non-quoted heredoc so $HOSTNAME, + # $USERNAME, and $nomarchy_url expand inline. + cat > /mnt/etc/nixos/flake.nix < /mnt/etc/nixos/hardware-selection.nix << EOF @@ -455,10 +630,11 @@ FLAKE_EOF EOF # system.nix — curated system-level options. Uncomment what you want and - # run \`sudo nixos-rebuild switch --flake /etc/nixos#default\` to apply. + # run \`sudo nixos-rebuild switch --flake /etc/nixos#$HOSTNAME\` to apply. cat > /mnt/etc/nixos/system.nix << EOF { pkgs, ... }: { + networking.hostName = "$HOSTNAME"; time.timeZone = "$TIMEZONE"; $impermanence_opt @@ -609,8 +785,12 @@ finish() { echo "Next steps:" echo " 1. Remove the installation media" echo " 2. Reboot your computer" - echo " 3. Log in with username: $USERNAME" - echo " 4. Your configuration is at /etc/nixos/" + echo " 3. Log in as: $USERNAME (host: $HOSTNAME)" + echo " 4. Your configuration lives at /etc/nixos/" + echo "" + echo "Rebuild commands:" + echo " • System: sudo nixos-rebuild switch --flake /etc/nixos#$HOSTNAME" + echo " • Dotfiles: nomarchy-env-update (runs home-manager switch)" echo "" echo "Tip: run 'nomarchy-themes-prebuild' once to pre-cache every theme" echo " variant. Theme switches after that are instant (no rebuild)." @@ -641,7 +821,3 @@ main() { } main "$@" -finish -} - -main "$@"