website/content/posts/2023-03-24-managing-mutable-files-in-nixos/index.adoc
Gabriel Arazas 2d33d4f481
Add missing doccontentref to older posts
I thought I committed them already. This is why you should really pay
attention to the build logs.
2023-11-10 18:13:34 +08:00

14 KiB
Raw Blame History


title: "Managing mutable files in NixOS" date: 2023-03-24T00:00:00+00:00

tags: - Nix ---

Managing mutable files in NixOS

Gabriel Arazas <foodogsquared@foodogsquared.one> v1.0.2, 2023-04-07: Update quote description

Some weeks ago, Ive made a post describing how to combine traditional dotfiles and NixOS config with Nix flakes. That seems all well and good but it comes with a few problems.

  • Changes from the dotfiles have to be pushed first to the remote repo.

  • Then, the dotfile flake input have to be updated. After pushing changes from the dotfiles, you have to update it as depicted from Workflows and its caveats. While managing with flakes is less tedious than managing it with fetchers, youre essentially just reducing the source of updates from two to one.

While it is reproducible, its a tedious process especially if you need immediate changes from your dotfiles.

One of the responses I got from the aforementioned post is an alternative solution for managing dotfiles.

Im more likely to use home-managers mkOutOfStoreSymlink if I find myself in situations where I need non-generational or mutable file content. I have a trivial personal HM module that git-clones things to a desired path as part of the activation script if theyre absent and then manage them out of band from then on.

— /u/mtndewforbreakfast

Long story short, I tried this approach and I found it to be a better solution overall. It is more flexible and lends itself as a great solution for managing mutable files — files that are not meant to be managed by Nix. This also reduces the things I need to do post-installation which is already contained in a script so thats a nice benefit. Anyways, heres my take on the posted solution.

A better way to manage traditional dotfiles in home-manager

As hinted from the quoted statement, github:nix-community/home-manager[mkOutOfStoreSymlink, rev=bb4b25b302dbf0f527f190461b080b5262871756, path=modules/files.nix] is a home-manager function that accepts a path string and returns a derivation. This derivation contains a builder script that links the given path to a store path.

Using it should be simple. The following listing simply links my dotfiles located on /home/foo-dogsquared/dotfiles and creates a path on the Nix store.

An example of using the function
mkOutOfStoreSymlink "/home/foo-dogsquared/dotfiles"

This pretty much allows us to interact with various options from home-manager that normally accepts a store path. In my case, I mainly use it for linking various files with home.file.<name>.source, xdg.configFile.<name>.source, and so forth. To give some more context, heres an example usage of the function with my use case.

What is dotfiles yet again?

Just to continue the tradition from the last post, dotfiles is now a derivation from mkOutOfStoreSymlink. The very same type as to what fetchFromGitHub returns. Since it is a derivation, it will evaluate to the output path if coerced into a string which should be a store path that is symlinked to the dotfiles. This is why the code works still unchanged for the most part.

Compared to the approach of making the dotfiles as a flake input, this reduces the reproducibility of our home-manager configuration a little bit. Instead of fully including the dotfiles, we only assume we have the dotfiles at the given location. However, this does remove the workflow of managing the flake input and its caveats altogether.

You dont have to do nix flake update or anything else in your NixOS config and manage them separately. Were compromising reproducibility with this but it is worth it considering I want the changes immediately.

Adding a declarative interface for fetching mutable files

Take note with the above method, we did reduce from fully including the dotfiles to only assuming we have the dotfiles. I still want to include the dotfiles declared somewhere in the configuration. The closest well ever get is to create a module that accepts a list of files to download and put it in the filesystem which is exactly what I did. Anyhoo, heres how I would use the imaginary module.

{
  home.mutableFile = {
    "${config.xdg.userDirs.documents}/dotfiles" = {
      url = "https://github.com/foo-dogsquared/dotfiles.git";
      type = "git";
    };

    "${config.xdg.userDirs.documents}/top-secret" = {
      url = "https://example.com/file.zip";
      type = "fetch";
    };
  };
}

This module is meant to be used for fetching mutable files. It would have different methods for fetching the file indicated by the type attribute. For the initial version of this module, well consider two use cases: cloning the Git repos and downloading the file. Lets first create the skeleton for the module.

modules/home-manager/fetch-mutable-files.nix
link:git:refs/heads/content/posts/2023-03-24-managing-mutable-files-in-nixos~3[role=include]

We have yet to define certain parts including what each attribute could contain. Each of the attribute in the home.mutableFile.<name> expects at least two attributes: the URL to be downloaded and the download method. The file should only be downloaded if the path doesnt exist.

Note

At this point, updates to the code are shown as diffs. It is meant to be used with git apply and similar tools.

git apply file.patch
link:git:refs/heads/content/posts/2023-03-24-managing-mutable-files-in-nixos~2[role=include]

Take note we also added the path attribute that comes with a function to handle the path. Its for cleanly passing absolute paths and relative paths when it needs to.

{
  # Absolute paths should be acceptable.
  home.mutableFile."${config.xdg.userDirs.documents}/top-secret" = { };
  home.mutableFile."${config.xdg.configHome}/doom" = { };
  home.mutableFile."${config.home.homeDirectory}/hello" = { };
  home.mutableFile."/home/foo-dogsquared/writings" = { };

  # So does relative paths...
  home.mutableFile."dotfiles" = { };
  home.mutableFile."library" = { };
}

With the interface done, we can then proceed with the implementation which is just a shell script managed by systemd. Lets first build the systemd service before we proceed with the shell script.

Tip

Remember, were using systemd to manage the service. The service is run in a completely new environment and isnt in a shell with programming features like Bash and zsh. This means you cannot run the following command on Service.ExecStart directive like how one would expect on the shell.

curl "https://example.org" || echo "ERROR"
link:git:refs/heads/content/posts/2023-03-24-managing-mutable-files-in-nixos~1[role=include]

Creating the shell script should be trivial. We could generate the entire script by iterating each of the file from home.mutableFile.<name> and mapping the methods from it. Heres one way to let Nix generate our shell script featuring writeShellScript builder.

link:git:refs/heads/content/posts/2023-03-24-managing-mutable-files-in-nixos[role=include]
Note

For those who want a complete version of the module directly without applying all of the above patches, you can see it with this link.

With the module being complete for the most part, we just have to include it to our home-manager configuration…

flake.nix
{
  outputs = { nixpkgs, home-manager, ... }@inputs: {
    homeConfigurations.foodogsquared = home-manager.lib.homeManagerConfiguration {
      modules = [
        ./modules/home-manager/fetch-mutable-files.nix
        ./home.nix
      ];
    };
  };
}

and finally use it. Like I said previously, the nice thing with this module for me is allowing me beyond fetching my dotfiles. I could, for example, fetch Doom Emacs alongside my home-manager configuration. Very nice!

home.nix
{ config, lib, pkgs, ... }:

let
  dotfiles = config.lib.file.mkOutOfStoreSymlink config.home.mutableFile."dotfiles".path;
in
{
  home.mutableFile = {
    "dotfiles" = {
      url = "https://github.com/foo-dogsquared/dotfiles.git";
      type = "git";
    };

    "${config.xdg.configHome}/emacs" = {
      url = "https://github.com/doomemacs/doomemacs";
      type = "git";
    };
  };

  # Putting the dotfiles in their rightful place.
  xdg.configFile = {
    doom.source = "${dotfiles}/emacs";
    wezterm.source = "${dotfiles}/wezterm";
    nvim.source = "${dotfiles}/nvim";
  };
}
Some room for improvement

The given code from this post is just one minimal starting point for this module. In my NixOS configuration, github:foo-dogsquared/nixos-config[my version of the module, rev=c5bf67553c84efb1e0d3f1859f2d98736d66f616, path=modules/home-manager/files/mutable-files.nix] has expanded with the ability to declare archived files which is already extracted in the filesystem. I use it for fetching several things including Doom Emacs (even automatically installing it).

Starting from the version of the module featured here, there are room for improvement. You could implement the following suggestions as an exercise.

  • Add a type for fetching archived files. The archive should already be extracted into the path. Additionally, you could add an option for extracting a single file or directory.

  • Each resource to be fetched may require different tweaks. For example, you may want to shallow clone Doom Emacs since the repo history is too large. You might want to add an option (e.g., home.mutableFile.<name>.extraArgs) to set extra arguments to each file.

  • Add the option to allow changing the package to be used for the shell script. This would also require restructuring (and possibly renaming) of the module though.

  • Add an attribute that links to a store path (e.g., home.mutableFile.<name>.outPath) for each of the given URL. You could also add an attribute (e.g., home.mutableFile.<name>.dontLinkToStore) that either opts-in/-out of including the file in the store directory. Take note this value should be automatically applied and shouldnt be set by the user.

Mutable files in NixOS

So far, we only manage them mutable files in home-manager. I cannot find an equivalent in NixOS but it should be pretty trivial to recreate it especially that the things that made mkOutOfStoreSymlink possible is readily available in nixpkgs. All we have to do is to recreate them.

For this case, well use the runCommandLocal builder typically used for cheap commands. This also what mkOutOfStoreSymlink uses in the source code.

let
  path = lib.escapeShellArg "/etc/dotfiles";
in
pkgs.runCommandLocal "nixos-mutable-file-${builtins.baseNameOf path}" { } ''ln -s ${path} $out''

Similarly, this can be used for various NixOS options that normally accepts a store path (e.g., environment.etc.<name>.source). For example, if you have a minimal i3 setup that you want to link from a non-NixOS-managed folder, all you have to do is to link it from the dotfiles.

Unresolved directive in <stdin> - include::./assets/runcommandlocal.nix[]
Implementing a similar module for NixOS

If you want to create a declarative interface similar to the featured module for NixOS, you can create your implementation based from that. However, theres more moving parts that you have to worry about since were using NixOS where the scope includes the whole system unlike with home-manager where it focuses on the home environment. Here are some things you may need to pay attention to.

  • Since were using system-level services with systemd, the fetched files will be owned the user of the fetching service (which is root by default). You may want to add an option for the group and owner for each files.

  • Reject (or at least discourage) relative paths since it will be confusing to use. Its best if the module encourages the user to use absolute paths instead.

  • Making sure the fetching service does not modify the generated files from NixOS. Even though this is already handled by the previous module, it is something to keep in mind now that were modifying the whole operating system.