From 0c3de27f411c25be6a57596e676f95789be2ee02 Mon Sep 17 00:00:00 2001 From: Bad3r <25513724+Bad3r@users.noreply.github.com> Date: Sun, 19 Oct 2025 17:21:42 +0300 Subject: [PATCH 01/20] fix(meta): fail fast on missing app and role modules --- modules/meta/nixos-app-helpers.nix | 18 ++++++++++++++---- modules/roles/desktop.nix | 19 +++++++++++++------ modules/roles/xserver.nix | 20 ++------------------ modules/system76/imports.nix | 14 +++++++++----- modules/workstation.nix | 14 +++++++------- 5 files changed, 45 insertions(+), 40 deletions(-) diff --git a/modules/meta/nixos-app-helpers.nix b/modules/meta/nixos-app-helpers.nix index b6e3aec30..1fb27eb7c 100644 --- a/modules/meta/nixos-app-helpers.nix +++ b/modules/meta/nixos-app-helpers.nix @@ -87,7 +87,12 @@ let (lib.nameValuePair appName (lib.getAttrFromPath path imported)) ] else - acc + throw ( + "nixos-app-helpers: expected " + + toString filePath + + " to export flake.nixosModules.apps." + + appName + ) else acc ) { } appFiles; @@ -120,16 +125,21 @@ let if lib.hasAttrByPath path imported then lib.getAttrFromPath path imported else - lib.trace "nixos-app-helpers: app module missing path ${builtins.concatStringsSep "." path}" null + throw ( + "nixos-app-helpers: app module missing path " + + builtins.concatStringsSep "." path + ) else - lib.trace "nixos-app-helpers: missing app file ${toString maybeFile}" null; + throw ( + "nixos-app-helpers: missing app file " + toString maybeFile + ); previewList = lib.take 20 appKeys; preview = lib.concatStringsSep ", " previewList; ellipsis = if lib.length appKeys > 20 then ", …" else ""; suggestion = if appKeys == [ ] then "" else " Known keys (partial): ${preview}${ellipsis}"; in if fallbackModule != null then - lib.trace "nixos-app-helpers: fallback loaded ${name}" fallbackModule + fallbackModule else throw ("Unknown NixOS app '" + name + "'" + suggestion); diff --git a/modules/roles/desktop.nix b/modules/roles/desktop.nix index 43a99c8e2..c8e60ae2c 100644 --- a/modules/roles/desktop.nix +++ b/modules/roles/desktop.nix @@ -11,8 +11,18 @@ let getApp = rawHelpers.getApp or fallbackGetApp; getApps = rawHelpers.getApps or (names: map getApp names); - xserverRole = lib.attrByPath [ "roles" "xserver" ] config.flake.nixosModules null; - i3Module = lib.attrByPath [ "window-manager" "i3" ] config.flake.nixosModules null; + xserverRolePath = [ "roles" "xserver" ]; + xserverRole = + if lib.hasAttrByPath xserverRolePath config.flake.nixosModules then + lib.getAttrFromPath xserverRolePath config.flake.nixosModules + else + throw "Desktop role requires flake.nixosModules.roles.xserver"; + i3ModulePath = [ "window-manager" "i3" ]; + i3Module = + if lib.hasAttrByPath i3ModulePath config.flake.nixosModules then + lib.getAttrFromPath i3ModulePath config.flake.nixosModules + else + throw "Desktop role requires flake.nixosModules.window-manager.i3"; desktopApps = [ "blueberry" @@ -29,10 +39,7 @@ let "udiskie" ]; - roleImports = - lib.optionals (xserverRole != null) [ xserverRole ] - ++ lib.optionals (i3Module != null) [ i3Module ] - ++ getApps desktopApps; + roleImports = [ xserverRole i3Module ] ++ getApps desktopApps; in { flake.nixosModules.roles.desktop.imports = roleImports; diff --git a/modules/roles/xserver.nix b/modules/roles/xserver.nix index 2a8432999..ad5eecc64 100644 --- a/modules/roles/xserver.nix +++ b/modules/roles/xserver.nix @@ -23,26 +23,10 @@ let throw ("Unknown NixOS app '" + name + "' (role xserver)"); getApp = name: - let - tryRaw = - if rawHelpers ? getApp then - builtins.tryEval (rawHelpers.getApp name) - else - { - success = false; - value = null; - }; - in - if tryRaw.success then tryRaw.value else fallbackGetApp name; + if rawHelpers ? getApp then rawHelpers.getApp name else fallbackGetApp name; getApps = names: - if rawHelpers ? getApps then - let - attempt = builtins.tryEval (rawHelpers.getApps names); - in - if attempt.success then attempt.value else map getApp names - else - map getApp names; + if rawHelpers ? getApps then rawHelpers.getApps names else map getApp names; i3SessionModule = { pkgs, lib, ... }: { diff --git a/modules/system76/imports.nix b/modules/system76/imports.nix index 61043ae36..915463cb4 100644 --- a/modules/system76/imports.nix +++ b/modules/system76/imports.nix @@ -15,15 +15,19 @@ let name: let getRole = roleHelpers.getRole or (_: null); - attempt = builtins.tryEval (getRole name); + roleValue = getRole name; in - if attempt.success && attempt.value != null then - attempt.value + if roleValue != null then + roleValue else if lib.hasAttrByPath [ "roles" name ] nixosModules then lib.getAttrFromPath [ "roles" name ] nixosModules else - null; - roleModules = lib.filter (module: module != null) (map getRoleModule roleNames); + throw ( + "system76 host requires role " + + name + + " but it was not found in helpers or flake.nixosModules" + ); + roleModules = map getRoleModule roleNames; getVirtualizationModule = name: if lib.hasAttrByPath [ "virtualization" name ] nixosModules then diff --git a/modules/workstation.nix b/modules/workstation.nix index bf66fc018..9a70e4902 100644 --- a/modules/workstation.nix +++ b/modules/workstation.nix @@ -11,8 +11,7 @@ let resolveRoleOption = name: let - candidateEval = builtins.tryEval (rawResolveRole name); - candidate = if candidateEval.success then candidateEval.value else null; + candidate = rawResolveRole name; namePath = lib.splitString "." name; rolePath = [ "roles" ] ++ namePath; @@ -20,9 +19,9 @@ let attrs: if lib.hasAttrByPath rolePath attrs then let - valueEval = builtins.tryEval (lib.getAttrFromPath rolePath attrs); + value = lib.getAttrFromPath rolePath attrs; in - if valueEval.success then valueEval.value else null + value else null; @@ -144,9 +143,10 @@ in if missingRoleNames == [ ] then importsValue else - lib.warn ( - "Skipping unavailable workstation roles: " + lib.concatStringsSep ", " missingRoleNames - ) importsValue; + throw ( + "workstation bundle requires roles that failed to resolve: " + + lib.concatStringsSep ", " missingRoleNames + ); config = lib.mkIf (hmGuiModule != null) ( let extraNames = lib.attrByPath [ "home-manager" "extraAppImports" ] [ ] config; From e66663a4f96d1e5145983c68a4739fc15b1d58e4 Mon Sep 17 00:00:00 2001 From: Bad3r <25513724+Bad3r@users.noreply.github.com> Date: Sun, 19 Oct 2025 17:33:06 +0300 Subject: [PATCH 02/20] fix(runtime): surface hidden activation errors --- modules/hardware/monitors/lenovo-y27q-20.nix | 34 ++++++++++++----- modules/home/pass-secret-service.nix | 4 +- modules/services/duplicati-r2.nix | 40 ++++++++++++++++---- modules/system76/usbguard.nix | 12 +++++- 4 files changed, 69 insertions(+), 21 deletions(-) diff --git a/modules/hardware/monitors/lenovo-y27q-20.nix b/modules/hardware/monitors/lenovo-y27q-20.nix index 48e4ffd53..1fe94cd3d 100644 --- a/modules/hardware/monitors/lenovo-y27q-20.nix +++ b/modules/hardware/monitors/lenovo-y27q-20.nix @@ -49,7 +49,10 @@ let # Prefer EDID-based match so connectors (HDMI, DP, USB-C) do not matter while IFS= read -r dev; do [ -z "$dev" ] && continue - info="$($COLORMGR get-device "$dev" 2>/dev/null || true)" + if ! info="$($COLORMGR get-device "$dev" 2>/dev/null)"; then + echo "lenovo-y27q-20: failed to query device $dev" >&2 + exit 1 + fi vendor=$(trim "$(printf '%s\n' "$info" | sed -n 's/^\s*Vendor:\s*//p' | head -n1)") model=$(trim "$(printf '%s\n' "$info" | sed -n 's/^\s*Model:\s*//p' | head -n1)") serial=$(trim "$(printf '%s\n' "$info" | sed -n 's/^\s*Serial:\s*//p' | head -n1)") @@ -70,11 +73,11 @@ let device_path="$dev" break - done < <($COLORMGR get-devices-by-kind display 2>/dev/null || true) + done < <($COLORMGR get-devices-by-kind display 2>/dev/null) if [ -z "$device_path" ]; then for candidate in ${lib.concatStringsSep " " (map lib.escapeShellArg candidateIds)}; do - if path="$($COLORMGR find-device "$candidate" 2>/dev/null)"; then + if path="$($COLORMGR find-device $candidate 2>/dev/null)"; then device_path="$path" break fi @@ -83,13 +86,20 @@ let if [ -z "$device_path" ]; then echo "lenovo-y27q-20: no matching colord display device found" >&2 - exit 0 + exit 1 fi - profile_id="$($COLORMGR find-profile-by-filename "$PROFILE_PATH" 2>/dev/null || true)" + if ! profile_id="$($COLORMGR find-profile-by-filename "$PROFILE_PATH" 2>/dev/null)"; then + profile_id="" + fi if [ -z "$profile_id" ]; then - $COLORMGR import-profile "$PROFILE_PATH" >/dev/null 2>&1 || true - profile_id="$($COLORMGR find-profile-by-filename "$PROFILE_PATH" 2>/dev/null || true)" + if ! $COLORMGR import-profile "$PROFILE_PATH" >/dev/null 2>&1; then + echo "lenovo-y27q-20: failed to import profile $PROFILE_PATH" >&2 + exit 1 + fi + if ! profile_id="$($COLORMGR find-profile-by-filename "$PROFILE_PATH" 2>/dev/null)"; then + profile_id="" + fi fi if [ -z "$profile_id" ]; then @@ -98,8 +108,14 @@ let fi if ! $COLORMGR device-get-default-profile "$device_path" | grep -F "$profile_id" >/dev/null 2>&1; then - $COLORMGR device-add-profile "$device_path" "$profile_id" >/dev/null 2>&1 || true - $COLORMGR device-make-profile-default "$device_path" "$profile_id" >/dev/null 2>&1 || true + if ! $COLORMGR device-add-profile "$device_path" "$profile_id" >/dev/null 2>&1; then + echo "lenovo-y27q-20: failed to add profile $PROFILE_PATH to $device_path" >&2 + exit 1 + fi + if ! $COLORMGR device-make-profile-default "$device_path" "$profile_id" >/dev/null 2>&1; then + echo "lenovo-y27q-20: failed to set $PROFILE_PATH as default for $device_path" >&2 + exit 1 + fi fi ''; in diff --git a/modules/home/pass-secret-service.nix b/modules/home/pass-secret-service.nix index 24a85fc0c..300c4dae5 100644 --- a/modules/home/pass-secret-service.nix +++ b/modules/home/pass-secret-service.nix @@ -24,7 +24,7 @@ if [ -r "$key_file" ]; then if ! gpg --batch --list-secret-keys ${keyFingerprint} >/dev/null 2>&1; then gpg --batch --yes --import "$key_file" - echo "5\ny\n" | gpg --batch --yes --command-fd 0 --edit-key ${keyFingerprint} trust quit >/dev/null 2>&1 || true + echo "5\ny\n" | gpg --batch --yes --command-fd 0 --edit-key ${keyFingerprint} trust quit >/dev/null 2>&1 fi fi '' @@ -33,7 +33,7 @@ activation.initPassStore = lib.mkIf haveKeyPath ( lib.hm.dag.entryAfter [ "importPassGpgKey" ] '' if [ ! -d "$HOME/.password-store" ]; then - pass init --quiet ${keyFingerprint} || true + pass init --quiet ${keyFingerprint} fi '' ); diff --git a/modules/services/duplicati-r2.nix b/modules/services/duplicati-r2.nix index 6b9dd0f1c..597810dbc 100644 --- a/modules/services/duplicati-r2.nix +++ b/modules/services/duplicati-r2.nix @@ -465,11 +465,23 @@ let local base base="$(basename "$unit")" if [[ "$base" == *.timer ]]; then - systemctl stop "$base" 2>/dev/null || true - systemctl disable --runtime "$base" 2>/dev/null || true + if ! systemctl stop "$base" 2>/dev/null; then + echo "duplicati-r2 generator: failed to stop $base" >&2 + exit 1 + fi + if ! systemctl disable --runtime "$base" 2>/dev/null; then + echo "duplicati-r2 generator: failed to disable $base" >&2 + exit 1 + fi elif [[ "$base" == *.service ]]; then - systemctl stop "$base" 2>/dev/null || true - systemctl disable --runtime "$base" 2>/dev/null || true + if ! systemctl stop "$base" 2>/dev/null; then + echo "duplicati-r2 generator: failed to stop $base" >&2 + exit 1 + fi + if ! systemctl disable --runtime "$base" 2>/dev/null; then + echo "duplicati-r2 generator: failed to disable $base" >&2 + exit 1 + fi fi rm -f "$unit" fi @@ -619,13 +631,25 @@ let systemctl daemon-reload for timer in "''${backup_timers[@]}"; do - systemctl enable --runtime "$timer" >/dev/null 2>&1 || true - systemctl start "$timer" >/dev/null 2>&1 || true + if ! systemctl enable --runtime "$timer" >/dev/null 2>&1; then + echo "duplicati-r2 generator: failed to enable $timer" >&2 + exit 1 + fi + if ! systemctl start "$timer" >/dev/null 2>&1; then + echo "duplicati-r2 generator: failed to start $timer" >&2 + exit 1 + fi done for timer in "''${verify_timers[@]}"; do - systemctl enable --runtime "$timer" >/dev/null 2>&1 || true - systemctl start "$timer" >/dev/null 2>&1 || true + if ! systemctl enable --runtime "$timer" >/dev/null 2>&1; then + echo "duplicati-r2 generator: failed to enable $timer" >&2 + exit 1 + fi + if ! systemctl start "$timer" >/dev/null 2>&1; then + echo "duplicati-r2 generator: failed to start $timer" >&2 + exit 1 + fi done ''; }; diff --git a/modules/system76/usbguard.nix b/modules/system76/usbguard.nix index 1a655d383..ac2ada7f5 100644 --- a/modules/system76/usbguard.nix +++ b/modules/system76/usbguard.nix @@ -11,8 +11,16 @@ let flakeLib = libAttrs.flake or { }; securityLib = (flakeLib.lib or { }).security or { }; in - securityLib.usbguard or { }; - baseRules = lib.strings.trim (usbguardLib.baseRules or ""); + if securityLib ? usbguard then + securityLib.usbguard + else + throw "system76/usbguard: securityLib.usbguard missing; ensure usbguard-lib exports the helper"; + baseRulesRaw = usbguardLib.baseRules or null; + baseRules = + if baseRulesRaw == null || lib.strings.trim baseRulesRaw == "" then + throw "system76/usbguard: baseRules is empty; populate usbguardLib.baseRules" + else + lib.strings.trim baseRulesRaw; baseRulesFile = pkgs.writeText "usbguard-base.rules" baseRules; defaultsModule = usbguardLib.defaultsModule or null; moduleImports = lib.optional (defaultsModule != null) defaultsModule; From 29e2f175e1d136053f4be9438201d8b8353284e0 Mon Sep 17 00:00:00 2001 From: Bad3r <25513724+Bad3r@users.noreply.github.com> Date: Sun, 19 Oct 2025 18:11:07 +0300 Subject: [PATCH 03/20] fix: incorrect naming for modules/apps --- modules/apps/kiro-fhs.nix | 2 +- modules/apps/vscode-fhs.nix | 2 +- modules/apps/wappalyzer-next.nix | 22 +++++++++++++++------- modules/roles/dev.nix | 4 ++-- modules/roles/warp-client.nix | 2 +- 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/modules/apps/kiro-fhs.nix b/modules/apps/kiro-fhs.nix index a49de467b..abfb31f80 100644 --- a/modules/apps/kiro-fhs.nix +++ b/modules/apps/kiro-fhs.nix @@ -19,7 +19,7 @@ { # App module that installs Kiro (FHS) when imported - flake.nixosModules.apps.kiroFhs = + flake.nixosModules.apps."kiro-fhs" = { pkgs, ... }: { # Allow unfree if required by kiro-fhs packaging diff --git a/modules/apps/vscode-fhs.nix b/modules/apps/vscode-fhs.nix index 7071c08a0..f33be6830 100644 --- a/modules/apps/vscode-fhs.nix +++ b/modules/apps/vscode-fhs.nix @@ -23,7 +23,7 @@ { # App module that installs VS Code (FHS) when imported - flake.nixosModules.apps.vscodeFhs = + flake.nixosModules.apps."vscode-fhs" = { pkgs, ... }: { nixpkgs.allowedUnfreePackages = [ diff --git a/modules/apps/wappalyzer-next.nix b/modules/apps/wappalyzer-next.nix index 8336c0dc7..cb44d6f69 100644 --- a/modules/apps/wappalyzer-next.nix +++ b/modules/apps/wappalyzer-next.nix @@ -12,11 +12,19 @@ */ _: { - flake.homeManagerModules.apps."wappalyzer-next" = - { pkgs, config, ... }: - { - home.packages = [ - config.flake.packages.${pkgs.system}.wappalyzer-next - ]; - }; + flake = { + nixosModules.apps."wappalyzer-next" = + { pkgs, ... }: + { + environment.systemPackages = [ pkgs.wappalyzer-next ]; + }; + + homeManagerModules.apps."wappalyzer-next" = + { pkgs, config, ... }: + { + home.packages = [ + config.flake.packages.${pkgs.system}.wappalyzer-next + ]; + }; + }; } diff --git a/modules/roles/dev.nix b/modules/roles/dev.nix index 2ae5649e5..7cbb93252 100644 --- a/modules/roles/dev.nix +++ b/modules/roles/dev.nix @@ -44,8 +44,8 @@ let "yarn" "nrm" # FHS-based dev tools - "vscodeFhs" - "kiroFhs" + "vscode-fhs" + "kiro-fhs" ]; roleImports = getApps devApps; in diff --git a/modules/roles/warp-client.nix b/modules/roles/warp-client.nix index e239fafe8..25c402e3a 100644 --- a/modules/roles/warp-client.nix +++ b/modules/roles/warp-client.nix @@ -8,7 +8,7 @@ # Docs: # - WARP on Linux: https://developers.cloudflare.com/warp-client/get-started/linux/ # - Firewall ports: https://developers.cloudflare.com/cloudflare-one/connections/connect-devices/warp/deployment/firewall/ - flake.nixosModules.warp-client = _: { + flake.nixosModules.roles."warp-client" = _: { services.cloudflare-warp = { enable = true; openFirewall = true; # opens UDP 2408 From 14f68ec06c509510b2f6e9fa1a2990cdf6156db4 Mon Sep 17 00:00:00 2001 From: Bad3r <25513724+Bad3r@users.noreply.github.com> Date: Sun, 19 Oct 2025 18:11:32 +0300 Subject: [PATCH 04/20] docs: add Nix debug manual --- docs/nix-debugging-manual.md | 109 +++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 docs/nix-debugging-manual.md diff --git a/docs/nix-debugging-manual.md b/docs/nix-debugging-manual.md new file mode 100644 index 000000000..5755850e6 --- /dev/null +++ b/docs/nix-debugging-manual.md @@ -0,0 +1,109 @@ +# Nix Ecosystem Debugging Manual + +## 1. Introduction +Debugging in the Nix ecosystem spans expression evaluation, reproducible builds, and declarative user environments. This manual summarizes practical techniques drawn from official manuals, community knowledge, and established tooling so you can triage issues systematically across Nix, NixOS, and Home Manager.citeturn15search0 + +--- + +## 2. Debugging Nix Language Expressions + +### 2.1 Using the Nix REPL (`nix repl`) +- Launch `nix repl` with flake or file inputs and inspect bindings via `:?`, `:doc`, and `:type` for quick reference while iterating on expressions.citeturn6search1 +- Toggle stack traces interactively with `:show-trace` or collect build logs with `:log ` to pinpoint failing derivations without leaving the REPL.citeturn6search1 +- Use `:b drv` or `:bl` to build derivations in place, creating GC roots that preserve artifacts for deeper inspection.citeturn6search1 +- Combine `--expr` or `--file` with `--extra-experimental-features 'flakes repl-flake'` when testing flake-based inputs so the REPL mirrors production evaluation settings.citeturn6search1 + +```bash +nix repl --expr 'import {}' +nix-repl> :doc builtins.map +nix-repl> :show-trace +``` + +### 2.2 Tracing Evaluation (`builtins.trace`) +- `builtins.trace "message" value` emits diagnostics while returning `value`, and `builtins.traceVerbose` obeys the `trace-verbose` setting to silence noisy traces in normal runs.citeturn16search2 +- Enable `builtins.break` with `--debugger` and `:bt`/`:st` commands to step through evaluation paths when the standard trace output is insufficient.citeturn16search2 +- Prefer library helpers such as `lib.debug.traceVal`, `traceValFn`, and `traceSeq` to force deeper evaluation or format values without rewriting existing expressions.citeturn15search0 +- For performance investigations, set `trace-function-calls = true` in `nix.conf` to emit function timings, or `trace-import-from-derivation = true` to spot unintentional IFD usage during evaluation.citeturn16search3turn16search4 + +```nix +{ lib, ... }: + +let + traced = lib.debug.traceValFn (v: "expanding ${v}") (builtins.trace "enter" 42); +in traced +``` + +### 2.3 Common Errors and Pitfalls +- Infinite recursion typically arises when option definitions depend on themselves; convert conditionals into `lib.mkIf` or restructure module arguments to break cycles.citeturn1search0 +- Missing attributes and type mismatches surface clearer diagnostics when rerun with `--show-trace`, which expands stack frames to the originating file and option definition.citeturn16search2 +- Avoid accessing `pkgs.lib` inside module arguments—import `lib` explicitly to prevent recursion through package set initialization.citeturn1search0 + +### 2.4 Language-Level Debugging Tools (e.g., `nix-debug`, `nix-tree`) +- Use `nix eval --show-trace --expr ''` or `nix-instantiate --eval --strict` to surface evaluation errors without building derivations.citeturn6search2 +- Explore dependency graphs with `nix-tree ` or `nix path-info --recursive --closure-size` to identify unexpectedly large closures and hidden references.citeturn19view0turn7search0 +- Compare two derivations using `nix store diff-closures /path/or/flake1 /path/or/flake2` to detect runtime-impacting differences between generations.citeturn7search2 +- Analyze why a package depends on another via `nix why-depends` to trace a single chain of references through the store.citeturn7search1 +- For ad hoc profiling, combine `nix build --keep-failed` with standard Unix tools inside the retained build directory, then refresh caches using `nix log` to review build output.citeturn6search0 + +--- + +## 3. Debugging NixOS Systems + +### 3.1 Analyzing Build Failures +- When `nixos-rebuild` aborts, inspect failures immediately with `nix log `; add `--print-build-logs` or `-L` to stream logs during the rebuild.citeturn6search0 +- Preserve failed build environments with `nix build --keep-failed` or `nix-build --keep-failed` so you can drop into `result-tmp` directories for manual investigation.citeturn6search0 +- Detect nondeterministic outputs by pairing `--check` with `nix store diff-closures` or the `.check` outputs generated after a mismatch.citeturn7search2 +- Use `nix build --keep-going` during large upgrades to aggregate all failing derivations instead of halting at the first error.citeturn6search0 + +### 3.2 Debugging Systemd Services +- Query unit health with `systemctl status ` and follow live logs via `journalctl -fu ` or boot-scoped summaries with `journalctl -b`.citeturn2search0 +- On configuration switches, enable verbose activation by exporting `STC_DEBUG=1` before running `sudo nixos-rebuild switch` to view each action performed by `switch-to-configuration`.citeturn2search0 +- For crash loops, combine `systemctl reset-failed ` with targeted restarts so journal output reflects a single activation attempt without historic noise.citeturn2search0 +- Use `nixos-option services..