Some checks failed
Check / eval (push) Has been cancelled
The machine flake's git-tracked settings file is system state, not "theme" only — rename it to state.json. CLI becomes nomarchy-state-sync with a nomarchy-theme-sync symlink for scripts and muscle memory. Eval (mkFlake, doctor, lifecycle) still accepts theme-state.json; the next write migrates to state.json and removes the legacy file. Documented in MIGRATION.md; drop the CLI alias after release notes.
371 lines
17 KiB
Nix
371 lines
17 KiB
Nix
# Hardware enablement beyond nixos-hardware.
|
||
#
|
||
# The nixos-hardware "common-*" profiles the installer selects cover the
|
||
# BASICS (microcode, the Intel/AMD VA-API media stack, weekly fstrim), and
|
||
# power.nix adds thermald + power-profiles-daemon. This module fills the GAP
|
||
# above those: broadly-beneficial bits that default ON when the installer
|
||
# detects the vendor (opt-OUT), and heavier/experimental bits behind opt-IN
|
||
# toggles. The installer's hardware-db.sh probes what's present and writes the
|
||
# matching nomarchy.hardware.* into the generated system.nix.
|
||
#
|
||
# Audited against the commons so we don't double-set: we add GuC/HuC, the
|
||
# amd-pstate governor, the AMD VA-API env, GPU-compute runtimes, fprintd, and
|
||
# the NPU driver — none of which the commons turn on.
|
||
{ config, lib, pkgs, ... }:
|
||
|
||
let
|
||
cfg = config.nomarchy.hardware;
|
||
# Fingerprint PAM can follow state.json (menu toggle → next
|
||
# sys-rebuild), same bridge as autoTimezone (BACKLOG #55). Missing or
|
||
# invalid JSON fails closed (state-read.nix) instead of a raw stack.
|
||
hwState =
|
||
if config.nomarchy.system.stateFile != null
|
||
then import ../state-read.nix { inherit lib; } config.nomarchy.system.stateFile
|
||
else { };
|
||
pamFromState = (hwState.settings or { }).fingerprint.pam or false;
|
||
|
||
sync = lib.getExe pkgs.nomarchy-state-sync;
|
||
|
||
# The single fingerprint on/off switch (System › Fingerprint). One state key
|
||
# for one user-facing decision — it drives login/sudo PAM here AND the
|
||
# hyprlock unlock in modules/home/idle.nix, which reads the same
|
||
# settings.fingerprint.pam. Two rebuilds, because the two live in different
|
||
# configurations: sudo the system switch (PAM), then a home switch
|
||
# (hyprlock). Same user-owns-the-flake shape as nomarchy-autotimezone.
|
||
#
|
||
# This does NOT decide whether login prompts at all — auto-login skips the
|
||
# greeter entirely, so "fingerprint on" adds the finger to whatever prompts
|
||
# actually happen (sudo, lock screen, and the greeter only when auto-login
|
||
# is off). See nomarchy-autologin in ./greeter.nix.
|
||
nomarchy-fingerprint = pkgs.writeShellScriptBin "nomarchy-fingerprint" ''
|
||
set -e
|
||
if [ "$(id -u)" -eq 0 ]; then
|
||
echo "nomarchy-fingerprint: run as your normal user (it sudos the rebuild itself)" >&2
|
||
exit 1
|
||
fi
|
||
flake="''${NOMARCHY_PATH:-$HOME/.nomarchy}"
|
||
|
||
cur=$(${sync} get settings.fingerprint.pam 2>/dev/null) || cur=false
|
||
case "''${1:-toggle}" in
|
||
on) new=true ;;
|
||
off) new=false ;;
|
||
toggle) case "$cur" in true|True) new=false ;; *) new=true ;; esac ;;
|
||
status) echo "$cur"; exit 0 ;;
|
||
*) echo "usage: nomarchy-fingerprint [toggle|on|off|status]" >&2; exit 64 ;;
|
||
esac
|
||
|
||
# Turning it ON with no enrolled finger would advertise a scan that cannot
|
||
# succeed on every prompt — refuse instead, and say where to go.
|
||
if [ "$new" = true ] \
|
||
&& fprintd-list "$USER" 2>/dev/null | grep -qiE 'no fingers enrolled|No devices available'; then
|
||
notify-send "Fingerprint" "Enroll a finger first (System › Fingerprint › Enroll)." 2>/dev/null || true
|
||
echo "nomarchy-fingerprint: no finger enrolled — run fprintd-enroll first" >&2
|
||
exit 1
|
||
fi
|
||
|
||
${sync} --quiet set settings.fingerprint.pam "$new" --no-switch
|
||
|
||
notify-send "Fingerprint" "Rebuilding…" 2>/dev/null || true
|
||
sudo nixos-rebuild switch --flake "$flake#default"
|
||
home-manager switch --flake "$flake"
|
||
|
||
if [ "$new" = true ]; then
|
||
notify-send "Fingerprint on" "Password or finger — at sudo, the lock screen, and the greeter." 2>/dev/null || true
|
||
else
|
||
notify-send "Fingerprint off" "Password only. Enrolled fingers are kept." 2>/dev/null || true
|
||
fi
|
||
'';
|
||
in
|
||
{
|
||
options.nomarchy.hardware = {
|
||
intel = {
|
||
enable = lib.mkEnableOption ''
|
||
Intel CPU/GPU enablement (the installer turns this on when it detects
|
||
an Intel CPU or GPU). Complements nixos-hardware's common-gpu-intel'';
|
||
|
||
guc = lib.mkOption {
|
||
type = lib.types.bool;
|
||
default = cfg.intel.enable;
|
||
defaultText = lib.literalExpression "config.nomarchy.hardware.intel.enable";
|
||
description = ''
|
||
Load the GPU's GuC/HuC firmware via the i915 param
|
||
(i915.enable_guc=3) — better power management and HuC-accelerated
|
||
media. On by default with intel.enable. NOTE: this is the *i915*
|
||
driver's param; the newer `xe` driver (Lunar Lake / Battlemage /
|
||
Panther Lake and other recent Xe GPUs) enables GuC by default and
|
||
ignores it, so the installer turns this off on xe-driver hardware.
|
||
'';
|
||
};
|
||
|
||
computeRuntime = lib.mkEnableOption ''
|
||
Intel GPU compute: the OpenCL / Level Zero (intel-compute-runtime) and
|
||
oneVPL (vpl-gpu-rt) runtimes for GPU compute and transcode. Opt-in (a
|
||
few hundred MB) — the Intel counterpart to AMD ROCm'';
|
||
};
|
||
|
||
amd = {
|
||
enable = lib.mkEnableOption ''
|
||
AMD CPU/GPU enablement (installer-set on an AMD CPU or GPU).
|
||
Complements nixos-hardware's common-cpu-amd / common-gpu-amd'';
|
||
|
||
pstate = lib.mkOption {
|
||
type = lib.types.bool;
|
||
default = cfg.amd.enable;
|
||
defaultText = lib.literalExpression "config.nomarchy.hardware.amd.enable";
|
||
description = ''
|
||
Use the amd-pstate EPP driver (amd_pstate=active) — the modern Zen
|
||
power/perf governor that power-profiles-daemon drives per profile.
|
||
On by default with amd.enable (broadly beneficial on Zen 2+).
|
||
'';
|
||
};
|
||
|
||
vaapi = lib.mkOption {
|
||
type = lib.types.bool;
|
||
default = cfg.amd.enable;
|
||
defaultText = lib.literalExpression "config.nomarchy.hardware.amd.enable";
|
||
description = ''
|
||
Point VA-API at mesa's radeonsi (LIBVA_DRIVER_NAME=radeonsi) for
|
||
hardware video decode/encode. On by default with amd.enable.
|
||
'';
|
||
};
|
||
|
||
rocm = {
|
||
enable = lib.mkEnableOption ''
|
||
AMD ROCm: the HIP / OpenCL GPU-compute stack (multi-GB closure).
|
||
Opt-in — unlocks GPU PyTorch / Ollama on Radeon'';
|
||
|
||
gfxOverride = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "";
|
||
example = "11.0.0";
|
||
description = ''
|
||
HSA_OVERRIDE_GFX_VERSION for GPUs ROCm doesn't officially list
|
||
(e.g. an RDNA3 780M iGPU, gfx1103, needs "11.0.0"). Empty = no
|
||
override.
|
||
'';
|
||
};
|
||
};
|
||
};
|
||
|
||
fingerprint = {
|
||
enable = lib.mkEnableOption ''
|
||
a fingerprint reader via fprintd (the installer turns this on when it
|
||
detects a known reader). Enroll with `fprintd-enroll`'';
|
||
|
||
pam = lib.mkOption {
|
||
type = lib.types.bool;
|
||
default = pamFromState;
|
||
defaultText = lib.literalExpression
|
||
"(settings.fingerprint.pam from state.json) or false";
|
||
description = ''
|
||
Use the fingerprint for login and sudo (PAM). Opt-in — password-only
|
||
stays the default for the cautious; enroll a finger first. Defaults
|
||
from state.json `settings.fingerprint.pam` (System › Fingerprint
|
||
menu) when set; otherwise false.
|
||
'';
|
||
};
|
||
|
||
parallel = lib.mkOption {
|
||
type = lib.types.bool;
|
||
default = true;
|
||
description = ''
|
||
With fingerprint PAM on, accept the password OR a fingerprint at the
|
||
same prompt (type or touch, whichever comes first) instead of stock
|
||
pam_fprintd's sequential wait-for-the-reader-then-password. Uses the
|
||
pam-fprint-grosshack module (an fprintd fork — source-reviewed; every
|
||
failure path falls through to the normal password rule, so password
|
||
login can never be locked out by it). Set false for the stock
|
||
sequential behavior.
|
||
'';
|
||
};
|
||
};
|
||
|
||
npu.enable = lib.mkEnableOption ''
|
||
the on-die NPU (AI accelerator) kernel driver — amdxdna on AMD (Ryzen
|
||
AI), intel_vpu on Intel (Core Ultra and newer). Opt-in and experimental:
|
||
this loads the in-kernel driver only; the userspace runtime (AMD XRT /
|
||
oneAPI Level Zero NPU) is yours to add. Needs a recent kernel (see
|
||
latestKernel)'';
|
||
|
||
latestKernel = lib.mkEnableOption ''
|
||
the latest mainline kernel (pkgs.linuxPackages_latest) instead of the
|
||
distro default — for very new hardware whose drivers (a fresh NPU, the
|
||
`xe` GPU driver, new-platform enablement) only landed recently. Off by
|
||
default; the default kernel already carries amd-pstate and amdxdna (6.14+)'';
|
||
|
||
camera = {
|
||
hideIrSensor = lib.mkEnableOption ''
|
||
hiding a dual-sensor webcam's IR (face-unlock) node from PipeWire so
|
||
apps only ever see the colour camera. Such modules (common on recent
|
||
ThinkPads) expose the IR sensor as a SECOND, identically-named
|
||
"Integrated Camera"; selecting it gives a dark, 8-bit-greyscale image —
|
||
the classic "my webcam is dark" symptom. The installer turns this on
|
||
when it detects a paired RGB+IR webcam. Only the PipeWire node is
|
||
disabled — the kernel /dev/video* device stays open, so face-unlock
|
||
(Howdy) still works. Acts on the V4L2 path only; the libcamera monitor
|
||
is left untouched, so an external camera you plug in is never affected.
|
||
See irMatch'';
|
||
|
||
irMatch = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "~.*(Integrated I|IR Camera|Infrared).*";
|
||
description = ''
|
||
WirePlumber regex (matched against a V4L2 node's api.v4l2.cap.card)
|
||
selecting the IR sensor to hide. The default catches the common
|
||
dual-sensor naming ("… Integrated I", "IR Camera", "Infrared"); set it
|
||
to your camera's IR card name if it differs — find it with
|
||
`v4l2-ctl --list-devices` or `wpctl inspect`. Only consulted when
|
||
camera.hideIrSensor is on.
|
||
'';
|
||
};
|
||
};
|
||
|
||
i2c = {
|
||
enable = lib.mkEnableOption ''
|
||
I2C devices support. Enables access to /dev/i2c-* (useful for RGB
|
||
controllers, sensors, and DDC/CI monitor control)'';
|
||
|
||
ddcci = lib.mkEnableOption ''
|
||
the ddcci-driver kernel module to expose external monitors as standard
|
||
backlight devices via DDC/CI. This allows brightness keys and swayosd
|
||
to natively control external displays'';
|
||
};
|
||
};
|
||
|
||
config = lib.mkMerge [
|
||
# ── Intel ──────────────────────────────────────────────────────────
|
||
(lib.mkIf cfg.intel.guc {
|
||
boot.kernelParams = [ "i915.enable_guc=3" ];
|
||
})
|
||
(lib.mkIf cfg.intel.computeRuntime {
|
||
hardware.graphics.extraPackages = with pkgs; [ intel-compute-runtime vpl-gpu-rt ];
|
||
})
|
||
|
||
# ── AMD ────────────────────────────────────────────────────────────
|
||
(lib.mkIf cfg.amd.pstate {
|
||
boot.kernelParams = [ "amd_pstate=active" ];
|
||
})
|
||
(lib.mkIf cfg.amd.vaapi {
|
||
# radeonsi itself comes from mesa (via common-gpu-amd); this just steers
|
||
# libva at it. mkDefault so a hand-set value or another module wins.
|
||
environment.sessionVariables.LIBVA_DRIVER_NAME = lib.mkDefault "radeonsi";
|
||
hardware.graphics.extraPackages = [ pkgs.libva ];
|
||
})
|
||
(lib.mkIf cfg.amd.rocm.enable {
|
||
hardware.graphics.extraPackages = [ pkgs.rocmPackages.clr pkgs.rocmPackages.clr.icd ];
|
||
environment.sessionVariables = lib.optionalAttrs (cfg.amd.rocm.gfxOverride != "") {
|
||
HSA_OVERRIDE_GFX_VERSION = cfg.amd.rocm.gfxOverride;
|
||
};
|
||
})
|
||
|
||
# ── Fingerprint ────────────────────────────────────────────────────
|
||
# NixOS defaults security.pam.services.*.fprintAuth to
|
||
# services.fprintd.enable — so turning on fprintd alone would enable
|
||
# finger auth for login/sudo/greetd/passwd/… even when the user (or
|
||
# the migration template) left fingerprint.pam commented off. Force
|
||
# every interactive service we care about to follow our opt-in flag.
|
||
(lib.mkIf cfg.fingerprint.enable {
|
||
services.fprintd.enable = true;
|
||
# Ships whenever a reader exists, regardless of the pam flag: the
|
||
# toggle's whole job is to turn the flag back on while it's off.
|
||
environment.systemPackages = [ nomarchy-fingerprint ];
|
||
security.pam.services = lib.genAttrs [
|
||
"login" "sudo" "su" "greetd" "hyprlock" "sshd"
|
||
"passwd" "chsh" "chfn" "chpasswd"
|
||
"polkit-1" "swaylock"
|
||
"groupadd" "groupdel" "groupmod" "groupmems"
|
||
] (_: {
|
||
fprintAuth = cfg.fingerprint.pam;
|
||
} // lib.optionalAttrs (cfg.fingerprint.pam && cfg.fingerprint.parallel) {
|
||
# Parallel mode: same rule slot as stock fprintd (so ordering —
|
||
# sufficient, before pam_unix — is inherited), different module.
|
||
# grosshack prompts for the password itself while polling the
|
||
# reader; whichever lands first wins. A typed password makes the
|
||
# rule FAIL with the token stored, and the stock
|
||
# `auth sufficient pam_unix.so … try_first_pass` right after it
|
||
# does the actual validation — password stays sufficient on its
|
||
# own, so a broken reader/fprintd can never lock login out.
|
||
rules.auth.fprintd.modulePath = lib.mkForce
|
||
"${pkgs.pam-fprint-grosshack}/lib/security/pam_fprintd_grosshack.so";
|
||
});
|
||
})
|
||
|
||
# ── Newest kernel for very-new hardware (opt-in escape hatch) ──────
|
||
(lib.mkIf cfg.latestKernel {
|
||
boot.kernelPackages = lib.mkDefault pkgs.linuxPackages_latest;
|
||
})
|
||
|
||
# ── NPU (in-kernel driver only; userspace runtime is BYO) ──────────
|
||
# The driver has to actually be in the running kernel — warn (don't fail)
|
||
# when it predates the shipped one, pointing at latestKernel.
|
||
(lib.mkIf (cfg.npu.enable && cfg.amd.enable) {
|
||
boot.kernelModules = [ "amdxdna" ];
|
||
warnings = lib.optional
|
||
(!lib.versionAtLeast config.boot.kernelPackages.kernel.version "6.14")
|
||
''
|
||
nomarchy.hardware.npu: the amdxdna driver needs kernel >= 6.14, but
|
||
this config ships ${config.boot.kernelPackages.kernel.version}. Set
|
||
nomarchy.hardware.latestKernel = true.'';
|
||
})
|
||
(lib.mkIf (cfg.npu.enable && cfg.intel.enable) {
|
||
boot.kernelModules = [ "intel_vpu" ];
|
||
warnings = lib.optional
|
||
(!lib.versionAtLeast config.boot.kernelPackages.kernel.version "6.11")
|
||
''
|
||
nomarchy.hardware.npu: the intel_vpu driver (especially for newer
|
||
NPUs) wants a recent kernel, but this config ships
|
||
${config.boot.kernelPackages.kernel.version}. Consider
|
||
nomarchy.hardware.latestKernel = true.'';
|
||
})
|
||
|
||
# ── Webcam: hide a dual-sensor module's IR node ────────────────────
|
||
# A built-in RGB+IR webcam exposes its IR (face-unlock) sensor as a second,
|
||
# identically-named camera; an app that picks it gets a dark greyscale
|
||
# image. Disable that node on the V4L2 PipeWire path so only the colour
|
||
# camera is offered. Matched by card name (irMatch). libcamera is left
|
||
# alone on purpose — an external camera may rely on it, and surgical
|
||
# internal-only libcamera scoping isn't possible (the distinguishing
|
||
# device props bind after the monitor rule runs). The kernel /dev/video*
|
||
# stays open, so Howdy face-unlock still reads the IR sensor directly.
|
||
(lib.mkIf (cfg.camera.hideIrSensor && config.services.pipewire.wireplumber.enable) {
|
||
services.pipewire.wireplumber.extraConfig."90-nomarchy-hide-ir-camera" = {
|
||
"monitor.v4l2.rules" = [
|
||
{
|
||
matches = [ { "api.v4l2.cap.card" = cfg.camera.irMatch; } ];
|
||
actions."update-props"."node.disabled" = true;
|
||
}
|
||
];
|
||
};
|
||
})
|
||
|
||
# ── I2C / DDC/CI ───────────────────────────────────────────────────
|
||
(lib.mkIf cfg.i2c.enable {
|
||
hardware.i2c.enable = true;
|
||
})
|
||
(lib.mkIf cfg.i2c.ddcci {
|
||
# The driver needs I2C underneath it
|
||
hardware.i2c.enable = true;
|
||
boot.extraModulePackages = [ config.boot.kernelPackages.ddcci-driver ];
|
||
boot.kernelModules = [ "ddcci_backlight" ];
|
||
})
|
||
|
||
# ── Sanity ─────────────────────────────────────────────────────────
|
||
{
|
||
assertions = [
|
||
{
|
||
assertion = cfg.amd.rocm.enable -> cfg.amd.enable;
|
||
message = "nomarchy.hardware.amd.rocm.enable needs nomarchy.hardware.amd.enable.";
|
||
}
|
||
{
|
||
assertion = cfg.intel.computeRuntime -> cfg.intel.enable;
|
||
message = "nomarchy.hardware.intel.computeRuntime needs nomarchy.hardware.intel.enable.";
|
||
}
|
||
{
|
||
assertion = cfg.npu.enable -> (cfg.amd.enable || cfg.intel.enable);
|
||
message = "nomarchy.hardware.npu.enable needs a detected Intel or AMD platform.";
|
||
}
|
||
];
|
||
}
|
||
];
|
||
}
|