Skip to content

support user-specific paths with the respective user as owner of non-persisted intermediate/parent paths - #16

Merged
WilliButz merged 7 commits into
mainfrom
default-user-ownership
Sep 7, 2025
Merged

support user-specific paths with the respective user as owner of non-persisted intermediate/parent paths#16
WilliButz merged 7 commits into
mainfrom
default-user-ownership

Conversation

@WilliButz

@WilliButz WilliButz commented Jul 1, 2025

Copy link
Copy Markdown
Member

Flake lock file updates:

• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/9d3ae807ebd2981d593cddd0080856873139aa40' (2025-01-29)
  → 'github:nixos/nixpkgs/3016b4b15d13f3089db8a41ef937b13a9e33a8df' (2025-06-30)
@Sporif

Sporif commented Jul 1, 2025

Copy link
Copy Markdown

Seems the home directory is added twice to intermediate file paths.

@WilliButz

Copy link
Copy Markdown
Member Author

Seems the home directory is added twice to intermediate file paths.

Oh thanks, no idea how I managed to overlooked that

@WilliButz
WilliButz force-pushed the default-user-ownership branch from 5006055 to ba42a25 Compare July 2, 2025 08:00
@Sporif

Sporif commented Jul 2, 2025

Copy link
Copy Markdown

Great, this now produces almost the same paths as my implementation. The only thing missing is the home directory on persistent storage, which needs its permissions set to config.users.users.${name}.homeMode.

@WilliButz
WilliButz force-pushed the default-user-ownership branch from ba42a25 to 3ca70b0 Compare July 6, 2025 10:40
@Sporif

Sporif commented Jul 6, 2025

Copy link
Copy Markdown

There's no differences now, it all looks good to me. Thanks!

@mnixry

mnixry commented Jul 16, 2025

Copy link
Copy Markdown

Thanks for your amazing work @WilliButz!

I'm encountering a conflict problem when experimenting with this branch. I'm trying to define multiple directories with different mode under the same parent directory, but it's causing a merge conflict:

error:
       … while calling the 'head' builtin
         at /nix/store/n8gf8yr0571s1ld4nr6dmk143iv3rvmh-source/lib/attrsets.nix:1534:13:
         1533|           if length values == 1 || pred here (elemAt values 1) (head values) then
         1534|             head values
             |             ^
         1535|           else

       … while evaluating the attribute 'value'
         at /nix/store/n8gf8yr0571s1ld4nr6dmk143iv3rvmh-source/lib/modules.nix:1086:7:
         1085|     // {
         1086|       value = addErrorContext "while evaluating the option `${showOption loc}':" value;
             |       ^
         1087|       inherit (res.defsFinal') highestPrio;

       … while evaluating the option `system.build.toplevel':

       … while evaluating definitions from `/nix/store/n8gf8yr0571s1ld4nr6dmk143iv3rvmh-source/nixos/modules/system/activation/top-level.nix':

       … while evaluating the option `system.systemBuilderArgs':

       … while evaluating definitions from `/nix/store/n8gf8yr0571s1ld4nr6dmk143iv3rvmh-source/nixos/modules/system/activation/activatable-system.nix':

       … while evaluating the option `system.activationScripts.etc.text':

       … while evaluating definitions from `/nix/store/n8gf8yr0571s1ld4nr6dmk143iv3rvmh-source/nixos/modules/system/etc/etc-activation.nix':

       … while evaluating definitions from `/nix/store/n8gf8yr0571s1ld4nr6dmk143iv3rvmh-source/nixos/modules/system/etc/etc.nix':

       … while evaluating the option `environment.etc."systemd/system".source':

       … while evaluating the option `systemd.units."systemd-tmpfiles-resetup.service".text':

       … while evaluating definitions from `/nix/store/n8gf8yr0571s1ld4nr6dmk143iv3rvmh-source/nixos/modules/system/boot/systemd.nix':

       … while evaluating the option `systemd.services.systemd-tmpfiles-resetup.unitConfig':

       … while evaluating definitions from `/nix/store/n8gf8yr0571s1ld4nr6dmk143iv3rvmh-source/nixos/modules/system/boot/systemd.nix':

       … while evaluating the option `systemd.services.systemd-tmpfiles-resetup.restartTriggers':

       … while evaluating definitions from `/nix/store/n8gf8yr0571s1ld4nr6dmk143iv3rvmh-source/nixos/modules/system/boot/systemd/tmpfiles.nix':

       … while evaluating the option `environment.etc."tmpfiles.d".source':

       … while evaluating definitions from `/nix/store/n8gf8yr0571s1ld4nr6dmk143iv3rvmh-source/nixos/modules/system/boot/systemd/tmpfiles.nix':

       … while evaluating the option `systemd.tmpfiles.settings.preservation."/persistent/home/mix".d.mode':

       (stack trace truncated; use '--show-trace' to show the full, detailed trace)

       error: The option `systemd.tmpfiles.settings.preservation."/persistent/home/mix".d.mode' has conflicting definition values:
       - In `/nix/store/6yc9c7fr2nrg6yiwa28y27cv6j5a4g2v-source/preservation.nix': "0755"
       - In `/nix/store/6yc9c7fr2nrg6yiwa28y27cv6j5a4g2v-source/preservation.nix': "700"
       Use `lib.mkForce value` or `lib.mkDefault value` to change the priority on any of these definitions.

Here is the relevant portion of my NixOS configuration:

Details
{
  pkgs,
  inputs,
  vars,
  ...
}:

{
  imports = [
    inputs.preservation.nixosModules.default
  ];

  preservation.enable = true;
  # pverservation required initrd using systemd.
  boot.initrd.systemd.enable = true;

  environment.systemPackages = [
    # `sudo ncdu -x /`
    pkgs.ncdu
  ];

  # There are two ways to clear the root filesystem on every boot:
  ##  1. use tmpfs for /
  ##  2. (btrfs/zfs only)take a blank snapshot of the root filesystem and revert to it on every boot via:
  ##     boot.initrd.postDeviceCommands = ''
  ##       mkdir -p /run/mymount
  ##       mount -o subvol=/ /dev/disk/by-uuid/UUID /run/mymount
  ##       btrfs subvolume delete /run/mymount
  ##       btrfs subvolume snapshot / /run/mymount
  ##     '';
  #
  #  See also https://grahamc.com/blog/erase-your-darlings/

  # NOTE: preservation only mounts the directory/file list below to /persistent
  # If the directory/file already exists in the root filesystem you should
  # move those files/directories to /persistent first!
  preservation.preserveAt."/persistent" = {
    directories = [
      "/etc/NetworkManager/system-connections"
      "/etc/ssh"
      "/etc/nix/inputs"
      #       "/etc/nixos"

      # my secrets
      "/etc/agenix/"

      "/var/log"

      # system-core
      "/var/lib/nixos"
      "/var/lib/systemd"
      {
        directory = "/var/lib/private";
        mode = "0700";
      }
      # lanzaboote - secure boot
      "/var/lib/sbctl"
      # fprintd fingerprint info
      {
        directory = "/var/lib/fprint/";
        mode = "0700";
      }

      # containers
      "/var/lib/docker"
      "/var/lib/cni"
      "/var/lib/containers"

      # other data
      "/var/lib/flatpak"

      # virtualisation
      "/var/lib/libvirt"
      "/var/lib/lxc"
      "/var/lib/lxd"
      "/var/lib/qemu"
      # "/var/lib/waydroid"

      # network
      "/var/lib/tailscale"
      "/var/lib/bluetooth"
      "/var/lib/NetworkManager"
    ];

    files = [
      # auto-generated machine ID
      {
        file = "/etc/machine-id";
        inInitrd = true;
      }
      {
        file = "/etc/dae/config.dae";
        mode = "0600";
      }
    ];

    users."${vars.user.name}" = {
      commonMountOptions = [
        "x-gvfs-hide"
      ];
      directories = [
        # ======================================
        # XDG Directories
        # ======================================

        "Downloads"
        "Music"
        "Pictures"
        "Documents"
        "Videos"

        # ======================================
        # Nix / Home Manager Profiles
        # ======================================

        ".local/state/home-manager"
        ".local/state/nix/profiles"
        ".local/share/nix"

        # ======================================
        # IDE / Editors
        # ======================================

        # neovim plugins(wakatime & copilot)
        ".wakatime"
        ".config/github-copilot"

        # vscode
        ".vscode"
        ".config/Code"
        ".vscode-insiders"
        ".config/Code - Insiders"

        # cursor ai editor
        ".cursor"
        ".config/Cursor"

        # zed editor
        ".config/zed"
        ".local/share/zed"

        # nvim
        ".local/share/nvim"
        ".local/state/nvim"

        # doom-emacs
        # "org" # org files
        # ".config/emacs"
        # ".local/share/doom"
        # ".local/share/emacs"

        # Joplin
        ".config/joplin" # tui client
        ".config/Joplin" # joplin-desktop

        # ".local/share/jupyter"

        # ======================================
        # Cloud Native
        # ======================================
        {
          # pulumi - infrastructure as code
          directory = ".pulumi";
          mode = "0700";
        }
        {
          directory = ".aws";
          mode = "0700";
        }
        {
          directory = ".docker";
          mode = "0700";
        }

        # ======================================
        # language package managers
        # ======================================
        ".local/share/pnpm/store" # PNPM Store
        #         ".npm" # typsescript/javascript
        #         "go"
        #         ".cargo" # rust
        #         ".m2" # java maven
        #         ".gradle" # java gradle
        #         ".conda" # python generated by `conda-shell`

        # ======================================
        # Security
        # ======================================

        {
          directory = ".gnupg";
          mode = "0700";
        }
        {
          directory = ".ssh";
          mode = "0700";
        }
        {
          directory = ".pki";
          mode = "0700";
        }
        {
          directory = ".local/share/kwalletd";
          mode = "0700";
        }

        ".local/share/password-store"
        # gnmome keyrings
        ".local/share/keyrings"

        # ======================================
        # Games / Media
        # ======================================

        ".steam"
        ".config/blender"
        ".config/LDtk"

        ".local/share/Steam"
        ".local/share/PrismLauncher"

        ".local/share/tiled"
        ".local/share/GOG.com"
        ".local/share/StardewValley"
        ".local/share/feral-interactive"

        # ======================================
        # Instant Messaging
        # ======================================
        ".config/QQ"

        ".local/share/TelegramDesktop"

        # ======================================
        # Meeting / Remote Desktop
        # ======================================
        ".config/remmina"
        ".config/freerdp"
        ".zoom"

        ".local/share/remmina"

        # ======================================
        # browsers
        # ======================================
        ".mozilla"
        ".config/google-chrome"

        # ======================================
        # CLI data
        # ======================================
        ".local/share/atuin"
        ".local/share/zoxide"
        ".local/share/direnv"
        ".local/share/k9s"

        # ======================================
        # Containers
        # ======================================
        ".local/share/containers"
        ".local/share/flatpak"
        # flatpak app's data
        ".var"

        # ======================================
        # Misc
        # ======================================

        # Audio
        ".config/pulse"
        ".local/state/wireplumber"

        # Digital Painting
        ".local/share/krita"
      ];
      files = [
        {
          file = ".wakatime.cfg";
          how = "symlink";
        }
        {
          file = ".local/share/fish/fish_history";
          how = "symlink";
          # create parent directory automatically
          configureParent = true;
        }
        {
          file = ".config/zoomus.conf";
          how = "symlink";
        }
        {
          file = ".config/zoom.conf";
          how = "symlink";
        }
      ];
    };
  };

  # systemd-machine-id-commit.service would fail but it is not relevant
  # in this specific setup for a persistent machine-id so we disable it
  #
  # see the firstboot example below for an alternative approach
  systemd.suppressedSystemUnits = [ "systemd-machine-id-commit.service" ];

  # let the service commit the transient ID to the persistent volume
  systemd.services.systemd-machine-id-commit = {
    unitConfig.ConditionPathIsMountPoint = [
      ""
      "/persistent/etc/machine-id"
    ];
    serviceConfig.ExecStart = [
      ""
      "systemd-machine-id-setup --commit --root /persistent"
    ];
  };
}

Is this conflict expected by design? If so, what’s the recommended way to resolve conflicting permission definitions for the same target directory?

Since I’m very new to NixOS, so any guidance would be appreciated.

@WilliButz

Copy link
Copy Markdown
Member Author

@mnixry I'll see that I look into this later today. It is definitely not expected.
The error message indicates two conflicting mode definitions for the user home on the persistent partition. "700" should be the expected value, as this is the default value of users.users.<name>.homeMode. I'll have too take a closer look to see where "0755" is coming from

@WilliButz
WilliButz force-pushed the default-user-ownership branch from 3ca70b0 to 22889ae Compare July 17, 2025 20:52
@WilliButz

Copy link
Copy Markdown
Member Author

@mnixry so the issue was caused by the following part in the config:

preservation.preserveAt."/persistent".users."${vars.user.name}".files = [
  {
    file = ".wakatime.cfg"; how = "symlink";
  }
];

Previously, the default value of configureParentDirectory for this file entry was true, causing the creation of a conflicting tmpfiles rule for the parent directory of the symlink, which is the user home.

Now this should no longer be the case.

@WilliButz
WilliButz force-pushed the default-user-ownership branch from 22889ae to f011789 Compare July 17, 2025 20:58
@mnixry

mnixry commented Jul 24, 2025

Copy link
Copy Markdown

Previously, the default value of configureParentDirectory for this file entry was true, causing the creation of a conflicting tmpfiles rule for the parent directory of the symlink, which is the user home.

Now this should no longer be the case.

@WilliButz Thanks for pushing the fix. I can confirm the config conflict is resolved.

@SamLukeYes

Copy link
Copy Markdown

I tried the latest default-user-ownership branch just now, and my configuration failed to evaluate.

error:
       … while calling the 'head' builtin
         at /nix/store/nzkmbbqv7r856kckd7v8ywz23lkmgh0w-source/lib/attrsets.nix:1534:13:
         1533|           if length values == 1 || pred here (elemAt values 1) (head values) then
         1534|             head values
             |             ^
         1535|           else

       … while evaluating the attribute 'value'
         at /nix/store/nzkmbbqv7r856kckd7v8ywz23lkmgh0w-source/lib/modules.nix:1085:7:
         1084|     // {
         1085|       value = addErrorContext "while evaluating the option `${showOption loc}':" value;
             |       ^
         1086|       inherit (res.defsFinal') highestPrio;

       … while evaluating the option `system.build.toplevel':

       … while evaluating definitions from `/nix/store/nzkmbbqv7r856kckd7v8ywz23lkmgh0w-source/nixos/modules/system/activation/top-level.nix':

       … while evaluating the option `warnings':

       … while evaluating definitions from `/nix/store/nzkmbbqv7r856kckd7v8ywz23lkmgh0w-source/nixos/modules/system/boot/systemd/tmpfiles.nix':

       … while evaluating the option `systemd.tmpfiles.settings':

       … while evaluating definitions from `/nix/store/nzkmbbqv7r856kckd7v8ywz23lkmgh0w-source/flake.nix':

       … while evaluating the option `preservation.preserveAt."/persistent".users.yes.directories."[definition 1-entry 15]".configureParent':

       (stack trace truncated; use '--show-trace' to show the full, detailed trace)

       error: attribute 'file' missing
       at /nix/store/7vhdv1517mr37wl617kbp0kv6mid7vqw-source/options.nix:88:72:
           87|               isUserSymlink = attrs.config.how == "symlink" && attrs.config.user != "root";
           88|               notOnTopLevel = !builtins.isNull (builtins.match ".+/.*" attrs.config.file);
             |                                                                        ^
           89|             in

nix repl results:

nix-repl> d = nixosConfigurations.absolute.config.preservation.preserveAt."/persistent".users.yes.directories

nix-repl> builtins.elemAt d 14
{
  configureParent = «error: attribute 'file' missing»;
  createLinkTarget = false;
  directory = "/home/yes/.local/share/fastfetch";
  group = "yes";
  how = "symlink";
  inInitrd = false;
  mode = "0755";
  mountOptions = [ ... ];
  parent = { ... };
  user = "yes";
}

nix-repl> (builtins.elemAt d 14).configureParent
error:
       … while evaluating the attribute 'value'
         at /nix/store/nzkmbbqv7r856kckd7v8ywz23lkmgh0w-source/lib/modules.nix:1085:7:
         1084|     // {
         1085|       value = addErrorContext "while evaluating the option `${showOption loc}':" value;
             |       ^
         1086|       inherit (res.defsFinal') highestPrio;

       … while evaluating the option `preservation.preserveAt."/persistent".users.yes.directories."[definition 1-entry 15]".configureParent':

       … while evaluating the attribute 'mergedValue'
         at /nix/store/nzkmbbqv7r856kckd7v8ywz23lkmgh0w-source/lib/modules.nix:1132:5:
         1131|     # Type-check the remaining definitions, and merge them. Or throw if no definitions.
         1132|     mergedValue =
             |     ^
         1133|       if isDefined then

       (stack trace truncated; use '--show-trace' to show the full, detailed trace)

       error: attribute 'file' missing
       at /nix/store/7vhdv1517mr37wl617kbp0kv6mid7vqw-source/options.nix:88:72:
           87|               isUserSymlink = attrs.config.how == "symlink" && attrs.config.user != "root";
           88|               notOnTopLevel = !builtins.isNull (builtins.match ".+/.*" attrs.config.file);
             |                                                                        ^
           89|             in

nix-repl> 

@WilliButz

Copy link
Copy Markdown
Member Author

@SamLukeYes thanks for the report, I'll look into it later this week

@WilliButz
WilliButz force-pushed the default-user-ownership branch from f011789 to e9376d0 Compare August 9, 2025 16:10
@WilliButz

Copy link
Copy Markdown
Member Author

@SamLukeYes there was a mix up with attributes. that's now fixed and also covered by the "basic" test

@WilliButz
WilliButz merged commit 2ef52b3 into main Sep 7, 2025
2 checks passed
@WilliButz
WilliButz deleted the default-user-ownership branch September 7, 2025 14:31
@Vonfry

Vonfry commented Sep 14, 2025

Copy link
Copy Markdown

The mode confliction is also existing.

It raises

       error: The option `systemd.tmpfiles.settings.preservation."/home/vonfry/.ssh".d.mode' has conflicting definition values:
       - In `/nix/store/1xq4n97cms68kqxiww3i0hczajfnc0ys-source/module.nix': "0700"
       - In `/nix/store/1xq4n97cms68kqxiww3i0hczajfnc0ys-source/module.nix': "0755"
       Use `lib.mkForce value` or `lib.mkDefault value` to change the priority on any of these definitions.

with the following config:

    files = [
      {
        file = ".ssh/known_hosts";
        mode = "0600";
        parent = { mode = "0700"; };
        configureParent = true;
      }
    ];

I have to write something like this to fix it:

    systemd.tmpfiles.settings.preservation = {
      "${homeDir}/${gpgbase}".d = mkForce ...;
      "${homeDir}/.ssh".d = mkForce ...;
    };

Vonfry added a commit to Vonfry/dotfiles that referenced this pull request Sep 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants