From d4437daf6fa0c0e8142ef51d16d5bf04d6cd15db Mon Sep 17 00:00:00 2001 From: Tom van der Lee Date: Fri, 18 Sep 2026 15:35:22 +0200 Subject: Skip platform-irrelevant dotfiles packages during stow iterm2 was being stowed on Linux and herbstluftwm/xorg on Darwin. Add a built-in Darwin/Linux exclude default plus an excludePackages override option in modules/home/dotfiles.nix, superseding the earlier "accept harmless unused symlinks" decision. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01BBSd4VBfoKHjE1HR3P8kmn --- modules/home/dotfiles.nix | 27 +++++++++++++++ .../filter-dotfiles-by-platform/.openspec.yaml | 2 ++ .../changes/filter-dotfiles-by-platform/design.md | 38 ++++++++++++++++++++++ .../filter-dotfiles-by-platform/proposal.md | 21 ++++++++++++ .../specs/dotfiles-management/spec.md | 16 +++++++++ .../changes/filter-dotfiles-by-platform/tasks.md | 13 ++++++++ 6 files changed, 117 insertions(+) create mode 100644 openspec/changes/filter-dotfiles-by-platform/.openspec.yaml create mode 100644 openspec/changes/filter-dotfiles-by-platform/design.md create mode 100644 openspec/changes/filter-dotfiles-by-platform/proposal.md create mode 100644 openspec/changes/filter-dotfiles-by-platform/specs/dotfiles-management/spec.md create mode 100644 openspec/changes/filter-dotfiles-by-platform/tasks.md diff --git a/modules/home/dotfiles.nix b/modules/home/dotfiles.nix index 3df944c..21afb5e 100644 --- a/modules/home/dotfiles.nix +++ b/modules/home/dotfiles.nix @@ -2,6 +2,15 @@ let cfg = config.home.dotfiles; dotfilesDir = "${config.home.homeDirectory}/dotfiles"; + + # Packages in the external dotfiles repo that are only relevant on one + # platform. Kept minimal and overridable via excludePackages rather than + # a full per-platform mapping - see design.md for alternatives considered. + platformDefaultExcludes = + lib.optionals (!pkgs.stdenv.isDarwin) [ "iterm2" ] + ++ lib.optionals (!pkgs.stdenv.isLinux) [ "herbstluftwm" "xorg" ]; + + excludePackages = platformDefaultExcludes ++ cfg.excludePackages; in { options.home.dotfiles.repoUrl = lib.mkOption { @@ -10,11 +19,18 @@ in description = "Git URL of the personal dotfiles repo to clone and stow into $HOME. Leave empty to disable."; }; + options.home.dotfiles.excludePackages = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Names of dotfiles stow packages to always skip, in addition to built-in platform defaults."; + }; + config = lib.mkIf (cfg.repoUrl != "") { home.packages = [ pkgs.git pkgs.stow ]; home.activation.dotfiles = lib.hm.dag.entryAfter [ "writeBoundary" ] '' DOTFILES_DIR=${lib.escapeShellArg dotfilesDir} + EXCLUDE_PACKAGES=(${lib.escapeShellArgs excludePackages}) if [ ! -d "$DOTFILES_DIR/.git" ]; then $DRY_RUN_CMD ${pkgs.git}/bin/git clone --recurse-submodules \ ${lib.escapeShellArg cfg.repoUrl} "$DOTFILES_DIR" @@ -23,6 +39,17 @@ in for pkg in "$DOTFILES_DIR"/*/; do pkg_name="$(basename "$pkg")" case "$pkg_name" in .*) continue ;; esac + skip=false + for excluded in "''${EXCLUDE_PACKAGES[@]}"; do + if [ "$pkg_name" = "$excluded" ]; then + skip=true + break + fi + done + if [ "$skip" = true ]; then + echo "Skipping dotfiles package '$pkg_name' — not relevant on this platform." >&2 + continue + fi $DRY_RUN_CMD ${pkgs.stow}/bin/stow -d "$DOTFILES_DIR" -t ${lib.escapeShellArg config.home.homeDirectory} "$pkg_name" \ || echo "Warning: stow failed for package '$pkg_name' — skipped." >&2 done diff --git a/openspec/changes/filter-dotfiles-by-platform/.openspec.yaml b/openspec/changes/filter-dotfiles-by-platform/.openspec.yaml new file mode 100644 index 0000000..f2cbbe6 --- /dev/null +++ b/openspec/changes/filter-dotfiles-by-platform/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-18 diff --git a/openspec/changes/filter-dotfiles-by-platform/design.md b/openspec/changes/filter-dotfiles-by-platform/design.md new file mode 100644 index 0000000..60cff21 --- /dev/null +++ b/openspec/changes/filter-dotfiles-by-platform/design.md @@ -0,0 +1,38 @@ +## Context + +See proposal.md - Why. `modules/home/dotfiles.nix` currently loops over every non-hidden top-level directory in the cloned external dotfiles repo and stows all of them unconditionally (lines 22-28). The only existing platform-branching precedent in this codebase is the simple `if pkgs.stdenv.isDarwin then ... else ...` in `users/tom.nix:6`. Only two real `homeConfigurations` exist today (`tom@linux`, `tom@darwin` in `flake.nix:44-53`); no NixOS hosts are defined yet (`hosts/` has only `template`). The known current package inventory in the external repo includes `iterm2` (Darwin-only), and `herbstluftwm` + `xorg` (Linux-only). + +## Goals / Non-Goals + +**Goals:** +- Skip stowing packages that are known to be irrelevant to the current platform, using a minimal, easily-extended mechanism. +- Preserve an escape hatch so a host can exclude additional packages without further code changes if the external repo's contents change. +- Keep all changes scoped to `modules/home/dotfiles.nix`; no changes to the external dotfiles repo, `flake.nix`, or host definitions. + +**Non-Goals:** +- Building a full per-host "which programs/WMs does this host use" model. The current problem is fully explained by OS (Darwin vs. Linux), so no new host-level option or metadata is introduced. +- Handling a future NixOS host that runs a desktop environment (e.g. GNOME, per `add-gnome-desktop`) but would still be Linux and thus still match the Linux exclude branch for `herbstluftwm`/`xorg` as "relevant." No such host exists yet; if it becomes a real problem later, the `excludePackages` escape hatch this change adds already covers it per-host. + +## Decisions + +**Decision: a small built-in platform-keyed exclude list, plus an `excludePackages` override option, computed with `pkgs.stdenv.isDarwin`/`isLinux`.** + +Inside `modules/home/dotfiles.nix`, add: +```nix +options.home.dotfiles.excludePackages = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Names of dotfiles stow packages to always skip, in addition to built-in platform defaults."; +}; +``` +and a `let`-bound list combining a small built-in default (`iterm2` when not Darwin; `herbstluftwm`, `xorg` when not Linux) with `cfg.excludePackages`. Pass the combined list into the activation script via `lib.escapeShellArgs` as a bash array, and check membership in the existing `for pkg in ...` loop before calling `stow`, skipping with a warning message (mirroring the existing stow-failure warning style at line 27) instead of silently stowing. + +Alternatives considered: +- **Marker-file convention in the external dotfiles repo** (e.g. each stow package declares its own target platform via a `.nix-platform` file that `dotfiles.nix` reads). This would keep nix-config fully generic and free of package-name knowledge, matching the original design philosophy most closely. Rejected because the dotfiles repo lives outside this git repository and is not something this change can edit or verify; it would also require the mechanism to handle a missing/malformed marker file gracefully, adding complexity for no immediate benefit over a 3-name list. +- **Fully generic attrs-based platform-mapping option** (e.g. `home.dotfiles.packagePlatforms = { darwin = [...]; linux = [...]; }`), configured per-host. Rejected as overengineering relative to the current need (only two platforms, three known mismatched packages); the simpler list-plus-override captures the same override capability with far less surface area. +- **Do nothing (status quo)**: the previously accepted "harmless unused symlinks" trade-off. Rejected per the user's explicit request; this change supersedes that decision. + +## Risks / Trade-offs + +- [Reintroducing external-repo package-name knowledge into nix-config, which the original design explicitly avoided] → Mitigation: kept minimal (3 names in one `let` binding) and overridable via `excludePackages`, so it degrades gracefully if the external repo's package list changes rather than silently breaking. +- [A future Linux host that both is Linux and doesn't want `herbstluftwm`/`xorg` (e.g. a GNOME NixOS host) would still get them stowed, since the built-in default only branches on Darwin vs. Linux, not on WM in use] → Mitigation: no such host exists yet; the `excludePackages` option already provides a per-host override once one does. diff --git a/openspec/changes/filter-dotfiles-by-platform/proposal.md b/openspec/changes/filter-dotfiles-by-platform/proposal.md new file mode 100644 index 0000000..6cf7af4 --- /dev/null +++ b/openspec/changes/filter-dotfiles-by-platform/proposal.md @@ -0,0 +1,21 @@ +## Why + +The dotfiles activation in `modules/home/dotfiles.nix` stows every top-level package directory from the external personal dotfiles repo into `$HOME` unconditionally, with no platform filtering. In practice this means macOS-only config (`iterm2`, for iTerm2) gets stowed on the Linux home-manager target, and Linux X11-only config (`herbstluftwm`, `xorg`) gets stowed on the Darwin machine — producing dead, irrelevant symlinks. This was previously an explicitly accepted trade-off (see the archived `add-dotfiles-stow-bootstrap` change's design non-goals, reaffirmed by `add-gnome-desktop`'s out-of-scope note), but the user now wants platform-irrelevant packages skipped, so this change supersedes that earlier "accept the risk" decision. + +## What Changes + +- `modules/home/dotfiles.nix` gains a new `home.dotfiles.excludePackages` option (list of package names to skip regardless of platform), as a manual per-host escape hatch. +- The module computes a small built-in default exclude set from the current known platform mismatches, using the same `pkgs.stdenv.isDarwin`/`isLinux` pattern already used in `users/tom.nix`: `iterm2` is skipped when not on Darwin; `herbstluftwm` and `xorg` are skipped when not on Linux. +- The stow activation loop skips any package in the combined exclude set (built-in + `excludePackages`), printing a visible "skipped — not relevant on this platform" message instead of silently stowing it. Packages not in the exclude set continue to stow exactly as before. + +## Capabilities + +### Modified Capabilities +- `dotfiles-management`: adds a requirement that platform-irrelevant dotfiles packages (built-in defaults or explicitly configured via `excludePackages`) are skipped during stow, rather than always stowing every top-level package directory. + +## Impact + +- **Code**: `modules/home/dotfiles.nix` (new option, exclude-set computation, activation script loop). +- **Specs**: `openspec/specs/dotfiles-management/spec.md` gains a new requirement; existing requirements (opt-in via repo URL, no re-clone, package-to-symlink mapping, per-package fault isolation) are unaffected. +- **Prior decisions superseded**: the "no filtering, accept harmless unused symlinks" non-goal recorded in the archived `add-dotfiles-stow-bootstrap` design and reaffirmed in `add-gnome-desktop` no longer holds; this change intentionally reverses it. +- No changes to `flake.nix`, host definitions, or the external dotfiles repo itself. diff --git a/openspec/changes/filter-dotfiles-by-platform/specs/dotfiles-management/spec.md b/openspec/changes/filter-dotfiles-by-platform/specs/dotfiles-management/spec.md new file mode 100644 index 0000000..e49bac6 --- /dev/null +++ b/openspec/changes/filter-dotfiles-by-platform/specs/dotfiles-management/spec.md @@ -0,0 +1,16 @@ +## ADDED Requirements + +### Requirement: Platform-irrelevant packages are skipped +The system SHALL skip stowing a dotfiles package when that package is platform-irrelevant, either because it matches a built-in platform default (based on the current OS) or because it has been explicitly listed for exclusion, rather than always stowing every top-level package directory. + +#### Scenario: A package is excluded by built-in platform default +- **WHEN** the dotfiles repository contains a top-level package directory that is known to be relevant only to a different platform than the one currently running (for example, a macOS-only package on a Linux system, or a Linux-only package on a macOS system) +- **THEN** that package is not symlinked, and a visible message indicates it was skipped as not relevant on this platform + +#### Scenario: A package is excluded via explicit configuration +- **WHEN** a package name has been explicitly configured for exclusion, regardless of the built-in platform defaults +- **THEN** that package is not symlinked, and a visible message indicates it was skipped + +#### Scenario: A non-excluded package is still stowed +- **WHEN** a top-level package directory is neither a built-in platform-mismatched package nor explicitly excluded +- **THEN** that package is symlinked into the home directory as described by the existing package-symlinking requirement diff --git a/openspec/changes/filter-dotfiles-by-platform/tasks.md b/openspec/changes/filter-dotfiles-by-platform/tasks.md new file mode 100644 index 0000000..f2a93d7 --- /dev/null +++ b/openspec/changes/filter-dotfiles-by-platform/tasks.md @@ -0,0 +1,13 @@ +## 1. Exclude-list option and platform defaults + +- [x] 1.1 In `modules/home/dotfiles.nix`, add the `home.dotfiles.excludePackages` option (`lib.types.listOf lib.types.str`, default `[ ]`) and verify `nix flake check` still evaluates the module without errors +- [x] 1.2 Add a `let`-bound built-in exclude list computed from `pkgs.stdenv.isDarwin`/`isLinux` (`iterm2` excluded when not Darwin; `herbstluftwm` and `xorg` excluded when not Linux), combined with `cfg.excludePackages`, and verify by inspecting the evaluated module (e.g. `nix eval` or a scratch `home-manager build`) that the combined list contains the expected names for each platform + +## 2. Skip excluded packages during stow + +- [x] 2.1 Pass the combined exclude list into the `home.activation.dotfiles` script via `lib.escapeShellArgs` and extend the `for pkg in ...` loop (currently `modules/home/dotfiles.nix:22-28`) to skip any package name present in that list, printing a message that it was skipped as not relevant on this platform, and verify by rendering the activation script (`home-manager build --flake .#tom@darwin` or `.#tom@linux`, or `nix eval .#homeConfigurations.\"tom@darwin\".activationPackage` and inspecting the generated script) that the skip branch is present +- [x] 2.2 Verify non-excluded packages are unaffected: confirm the rendered activation script still calls `stow` for packages outside the exclude list (e.g. `git`, `neovim`, `tmux`) + +## 3. Validation + +- [x] 3.1 Run `openspec validate filter-dotfiles-by-platform --strict` and confirm it passes -- cgit v1.3