How to make `src = ./.` in a flake.nix not change a lot?

Say I have a flake.nix with a derivation built from ./.. I would like to understand why it gets a different src when I only add a comment in flake.nix, and I filterSource a lot of files with:

        let
          ignoreFiles = [
            # Nix files
            "flake.nix" "flake.lock" "default.nix" "shell.nix"
            # git commit and editing format files
            ".commitlintrc.yml" "package.json" ".husky" ".editorconfig"
            # CI files
            ".cirrus.yml" "action.yml" "azure-pipelines.yml" "Dockerfile"
            # Git files
            ".github" ".gitattributes" ".gitignore" ".git"
          ];
        in
          # If the `path` checked is found in ignoreFiles, don't add it to the source
          if pkgs.lib.lists.any (p: p == (baseNameOf path)) ignoreFiles then
            false
          else
            true
        ) ./.;

I checked with ls -la in a preConfigure hook that the files are filtered, and what’s peculiar is that although flake.nix is filtered, adding a comment to it, that doesn’t change evaluation, changes the hash of the source:

$ nix build -L
sile> unpacking sources
sile> unpacking source archive /nix/store/laq2809ncq4ci664y35pgxgs5asmryri-xky96ibnmyvyz09v1piag4wgc167
frv-source
sile> source root is xky96ibnmyvyz09v1piag4wgc1673frv-source
$ # Add a comment to `flake.nix`
$ nix build -L
sile> unpacking sources
sile> unpacking source archive /nix/store/rinc2yd7a5nrll1wcw2y820pziygpvqz-lw9bn2iyhm1knrmy7yad1nyygqqvs
59i-source
sile> source root is lw9bn2iyhm1knrmy7yad1nyygqqvs59i-source

Any ideas what causes the hash of the source to change? Full flake.nix is available here:

I usually am quite happy with src = builtins.fetchGit ./.; (in non-flake-mode though - haven’t explored flakes yet)

This is happening because of 2 things:

  1. filterSource uses the name of the directory the filter is applied to as the store path name (the part after the hash) for the one it creates. It is not possible to specify the name yourself using filterSource.
  2. Nix moves all the files tracked by the version control inside the self repository (i.e. directory the flake belongs to) to the Nix store. This happens even before the evaluation of the flake starts. So the flake evaluates inside a directory named <hash>-source in the Nix store, where <hash> depends on all the files tracked by the versions control.

During the evaluation, filterSource will create a directory named <hash2>-<hash>-source that includes the files that passed the filter you specified. Since <hash2> also depends on <hash>-source, we get different hashes even if the change is in a file that didn’t pass the filter.

I think this means builtins.filterSource is useless inside a flake.

Using builtins.path, which allows specifying the store path name, like below should fix the problem.

src = builtins.path {
  path = ./.;
  name = "sile";
  filter = path: type:
    let
      ignoreFiles = [
        # Nix files
        "flake.nix"
        "flake.lock"
        "default.nix"
        "shell.nix"
        # git commit and editing format files
        ".commitlintrc.yml"
        "package.json"
        ".husky"
        ".editorconfig"
        # CI files
        ".cirrus.yml"
        "action.yml"
        "azure-pipelines.yml"
        "Dockerfile"
        # Git files
        ".github"
        ".gitattributes"
        ".gitignore"
        ".git"
      ];
    in
    # If the `path` checked is found in ignoreFiles, don't add it to the source
    if pkgs.lib.lists.any (p: p == (baseNameOf path)) ignoreFiles then
      false
    else
      true
    ;
};

Also I suggest nix-filter rather than creating the filtering boilerplate yourself in every project.

correction

I realized my previous answer was not entirely correct. It included this paragraph:

Source directories without an explicit name are not reproducible. The default name uses the basename of the current directory. Without flakes, this doesn’t change as long as you don’t rename the project directory. So it might be harder for you to discover the reproducibility issue without flakes. Also see Best practices — nix.dev documentation

The mentioned impurity is only valid for non-flake evaluations, because flake uses source as the store path name, regardless of the directory name. So saying it might be harder to discover without flakes was wrong.

4 Likes

another good idea is to define a function called projectPath like this:

this way projectPath "/my/folder" only depends on the NAR serialization of /my/folder and not of flake’s self (which depend on the entire repo and therefore makes the cache fragile)

we use this extensively on Makes to achieve incremental builds on big monorepos, where changing a code comment MUST only trigger the build of such code comment and leave intact everything else

Just using ./my/folder as the src value should have the same effect. It shouldn’t rebuild if a file outside that directory changes.

Just using ./my/folder as the src value should have the same effect. It shouldn’t rebuild if a file outside that directory changes.

it works like that on nix stable,

it does not work like that on flakes,

on flakes the git repo (src = ./.) also known as. self.sourceInfo is always copied into the store for hermeticity.
On flakes ./my/folder is equivalent to: "${self.sourceInfo}/my/folder".
Given self.sourceInfo changes on any code change anywhere in the repository (because it represents the entire repository!), then ./my/folder ("${self.sourceInfo}/my/folder") also changes because of it’s invisible dependency on the entire repository (self.sourceInfo)

with the mentioned projectPath function you break that implicit dependency on self.sourceInfo

It works like that both within a flake and outside one. There is no reason to use projectPath to avoid unnecessary rebuilds because there will be none without it. Out of directory changes won’t trigger a rebuild as long as you specify a relative path (nix type) for the src value like, like ./my/folder.

I know that self repository is moved to Nix store before the evaluation, but I think this is irrelevant in this case.

This is not true. "${toString ./my/folder}"[1] is equivalent to
"${self.sourceInfo}/my/folder". ./my/folder will result in nix moving that directory to a separate store path with the name <hash>-folder[2].

[1] toString prevents nix moving the directory to the store and let’s you reference the absolute path of the current directory (in a flake it’s a nix store anyway).
[2] The basename of the path is used in the store path name.

I am pretty sure changes in ./a/c don’t trigger a rebuild when you use ./a/b as the src. I created a repository to confirm this. As long as you don’t change anything inside foo/src or bar/src you won’t trigger a rebuild for foo or bar derivations, respectively. I also made them exactly the same derivation to show that, parent directory name (foo and bar) doesn’t affect the output derivation. This is true both for flake and non-flake evaluation.

Input sources of derivation foo can be checked with nix show-derivation '.#foo'| jq '.[] | .inputSrcs' for the flake evaluation and with nix show-derivation -f ./derivations/foo.nix | jq '.[] | .inputSrcs' for the non-flake evaluation.

Note that I didn’t test projectPath myself, so it is possible it has other advantages (one of them is using src as the name of the project root directory for non-flake evaluations, which removes the impurity described in here) but preventing rebuilds is not one of them.

1 Like

This is a simple thing I noted: bar references foo

But anyway I can reproduce what you say, and it’s correct, thanks for teaching me this!

knowledge.nix += 1

1 Like