# Filtering Source Trees with Nix and Nixpkgs

**URL:** https://discourse.nixos.org/t/filtering-source-trees-with-nix-and-nixpkgs/19148
**Category:** Guides
**Created:** [May 14, 2022, 7:02pm UTC](https://discourse.nixos.org/t/filtering-source-trees-with-nix-and-nixpkgs/19148 "2022-05-14T19:02:16Z")
**Posts on this page:** 6
**Page:** 1

<div class="post-metadata">

### Author: ![Growpotkin](https://discourse.nixos.org/user_avatar/discourse.nixos.org/growpotkin/32/343_2.png) [@Growpotkin](https://discourse.nixos.org/u/Growpotkin)
#### Post date: [May 14, 2022, 7:02pm UTC](https://discourse.nixos.org/t/filtering-source-trees-with-nix-and-nixpkgs/19148/1 "2022-05-14T19:02:16Z")

</div>

I’m preparing some articles for my coworkers who are learning Nix for the first time, and after wrapping this one up I thought it might be useful to share. For context, the code-base we work in is a monstrous mono-repo where you will get atrocious cache performance if you aren’t really explicit with filters. Caching aside, just copying sub-trees of the workspace unnecessarily can become really costly, making it especially important to filter “purely in Nix” up-front using prim-ops to avoid spurious file copies in an intermediate derivation.

I plan to extend this soon with examples of dead simple source tree construction with `runCommandNoCC` + `cp ${./foo} .` style cherry picking, and likely a proper tree-walker.

I hope y’all find it helpful, and feel free to share any critiques/additions.

# Source Filtering

Many Nix expresssions, particularly those which produce “derivations”  
( build recipes ) carry an attribute named `src` which is used to create your  
build area.

In most cases `src`, and all of its contents, effect the hash of your derivation  
such that any file modifications in that tree will trigger a cache miss ( rebuild ).

With that in mind, filtering a source tree down to the minimal list of files that  
really effect a build’s outputs can make a night and day difference in caching  
performance; this is especially true in large source trees such as a mono-repo.

Nix carries a simple builtin command `builtins.filterSource` which allows us to  
perform rudimentary filtering, and Nixpkgs carries a collection of more robust  
filters which you will want to become familiar with.

## Source Filters Overview

- `builtins.filterSource` : simple builtin filter routine.

- `lib.cleanSource` : sane default filter which removes temporary and VC files.

- `lib.cleanSourceWith` : applies custom filters and composes them.

- `lib.sources.trace` : traces source filters for debugging.

- `lib.sources.sourceByRegex` : produces a filter from regex expressions.

- `lib.sources.sourceFilesBySuffices` : produces a filter from a list of suffices.

- `nix-gitignore.gitignoreSource[Pure]` : filters using `.gitignore` syntax.

## Basic Filtering Examples

### Filtering a local project directory

```
{ pkgs ? import <nixpkgs> {}
, stdenv ? pkgs.stdenv
, lib ? pkgs.lib
}:
stdenv.mkDerivation {
  pname = "my-project";
  version = "0.0.1";
  src = lib.cleanSource ./.;
}

```

### Nesting Filters

Discouraged, but fine for small codebases.

```
{ pkgs ? import <nixpkgs> {}
, stdenv ? pkgs.stdenv
, lib ? pkgs.lib
, nix-gitignore ? pkgs.nix-gitignore
}:
stdenv.mkDerivation {
  pname = "my-project";
  version = "0.0.1";
  # A bit more efficient than `lib.sources.sourceByRegex' because
  # these directories are pruned immediately, and aren't traversed.
  src = nix-gitignore.gitignoreFilterPure [
    "node_modules/"
    ".yarn/cache/"
  ] ( lib.cleanSource ./. );
}

```

### Ignore Nix files ( Composed )

```
{ pkgs ? import <nixpkgs> {}
, stdenv ? pkgs.stdenv
, lib ? pkgs.lib
}:
stdenv.mkDerivation {
  pname = "my-project";
  version = "0.0.1";
  # `sourceFilesBySuffices' uses, `cleanSourceWith' internally, so these will
  # compose properly.
  src = lib.sources.sourceFilesBySuffices [".nix"] ( lib.cleanSource ./. );
}

```

### Composing filters

```
{ pkgs ? import <nixpkgs> {}
, stdenv ? pkgs.stdenv
, lib ? pkgs.lib
}:
let
  inherit (lib) cleanSource cleanSourceWith;
  # Remove `*/.yarn/cache' directories.
  cleanYarnCacheFilter = name: type:
    ! ( ( type == "directory" ) &&
        ( ( baseNameOf name ) == "cache" ) &&
        ( ( baseNameOf ( dirOf name ) ) == ".yarn" ) # `dirOf' is like `dirname'
      );

  # Remove `*/node_modules' directories
  cleanNodeModulesFilter = name: type:
    ! ( ( type == "directory" ) && ( ( baseNameOf name ) == "node_modules" ) );

  # Clean cached Node.js artifacts, and apply Nix's default clean routine.
  cleanNodeSource = src:
    cleanSourceWith {
      filter = cleanYarnCacheFilter;
      src = cleanSourceWith {
        filter = cleanNodeModulesFilter;
        src = cleanSource src;
      };
    };
in stdenv.mkDerivation {
  pname = "my-project";
  version = "0.0.1";
  src = cleanNodeSource ./.;
}

```

## Writing filters

Both `builtins.filterSource` and `lib.sources.cleanSourceWith` accept functions with  
the prototype `filter :: Name (Path/File Name) -> Type (Filetype) -> Boolean`  
where `Type ::= "directory" | "regular" | "symlink" | "unknown"`, “unknown”  
being Sockets and other oddball filesystem node types.  
A filter function is expected to return `true` if a file should be preserved,  
and `false` if a file should be removed.

An example filter which unconditionally preserves all files would be:

```
let myFilter = name: type: true;
in builtins.filterSource myFilter ./.
# Or equivalently ( since we never reference `name' or `type' ) :
builtins.filterSource ( _: _: true ) ./.

```

The filter above could also be used with Nixpkgs `lib.cleanSourceWith`:

```
lib.cleanSourceWith { filter = _: _: true; src = ./.; }

```

### A more useful filter: Optimized `node2nix` ignores.

A common Nix utility `node2nix` uses `nix-gitignore.gitignoreSourcePure`  
to pluck just the `package.json` and `package-lock.json` files from a  
source tree.

In this case, `nix-gitignore.gitignoreSourcePure` is somewhat unnecessary,  
since the underlying predicate “only keep these two files” is dead simple.  
Really, when we are building a single package, you should really use  
`builtins.pathExists` with `srcs = [...]`; but because `node2nix` is  
designed to possibly build groups of packages in a tree, it needs to  
preserve `package[-lock].json` files recursively.  
However, we can still improve the existing implementation by writing a  
filter directly, which will save us from evaluating an unnecessarily  
expensive routine.

In summary, our goal is simply: filter a source tree, preserving only  
`package.json` and `package-lock.json` files.

1. Rough Draft

2. A Working Filter

3. Diving Off the Deep End

4. Recommended Solution

---

<div class="post-metadata">

### Author: ![Ten](https://discourse.nixos.org/letter_avatar_proxy/v4/letter/t/3da27b/32.png) [@Ten](https://discourse.nixos.org/u/Ten)
#### Post date: [May 1, 2024, 7:28pm UTC](https://discourse.nixos.org/t/filtering-source-trees-with-nix-and-nixpkgs/19148/2 "2024-05-01T19:28:54Z")

</div>

> [@Growpotkin](#):
>
> and would still get the hash we care about  
> for our “real build” - which is what we actually care about

I’m struggling to understand how fullyClean will actually have a hash that does not depend on the empty directories of mostlyClean, since I don’t see anything that makes it not-source-addressed. (I’m trying to prevent recompilations that may be caused by adding and removing directories in an otherwise largely filtered tree)

---

<div class="post-metadata">

### Author: ![Infinisil](https://discourse.nixos.org/user_avatar/discourse.nixos.org/infinisil/32/26523_2.png) [@Infinisil](https://discourse.nixos.org/u/Infinisil)
#### Post date: [May 1, 2024, 7:46pm UTC](https://discourse.nixos.org/t/filtering-source-trees-with-nix-and-nixpkgs/19148/3 "2024-05-01T19:46:28Z")

</div>

Oh you might be interested in checking out [`lib.fileset`](https://nixos.org/manual/nixpkgs/unstable/#sec-functions-library-fileset), which hasn’t existed when the original post was published 🙂

---

<div class="post-metadata">

### Author: ![Ten](https://discourse.nixos.org/letter_avatar_proxy/v4/letter/t/3da27b/32.png) [@Ten](https://discourse.nixos.org/u/Ten)
#### Post date: [May 1, 2024, 7:56pm UTC](https://discourse.nixos.org/t/filtering-source-trees-with-nix-and-nixpkgs/19148/4 "2024-05-01T19:56:14Z")

</div>

Thanks! I was just looking at it but I still couldn’t find a way in there to do the regular directory traversal that filterSource does, but only creating a directory once we’re about to put an actual (not filtered-out) file inside, in order to not update hashes and cause recompilations based on unrelated directories.  
It does look like something that I’m very likely not the first to need and should consequently already exist in Nix…  
Is there such a thing?

EDIT: Ah [this post](https://discourse.nixos.org/t/easy-source-filtering-with-file-sets/29117) seems to suggest that maybe lib.fileset by default does filter empty directories? Does this mean then that [#8820](https://github.com/NixOS/nix/issues/8820) (that you opened!) is out of date?

---

<div class="post-metadata">

### Author: ![Ten](https://discourse.nixos.org/letter_avatar_proxy/v4/letter/t/3da27b/32.png) [@Ten](https://discourse.nixos.org/u/Ten)
#### Post date: [May 1, 2024, 8:45pm UTC](https://discourse.nixos.org/t/filtering-source-trees-with-nix-and-nixpkgs/19148/5 "2024-05-01T20:45:21Z")

</div>

So I’m trying to use [`fileset.fileFilter`](https://nixos.org/manual/nixpkgs/stable/#function-library-lib.fileset.fileFilter) and the documentation specifies that `type` may not be `directory`, unlike with `cleanSourceWith`.  
Does that mean that `fileset` will always traverse the entire file tree recursively?  
(I would like to avoid e.g. `target` and `node_modules` because those tend to be huge so slow to traverse and I know nothing interesting is in there)

EDIT: looks like combing it with [`lib.fileset.fromSource`](https://nixos.org/manual/nixpkgs/stable/#function-library-lib.fileset.fromSource) may be a fix…  
EDIT2: Yeah doing `fromSource` of a filtered source then `toSource` again works.

---

<div class="post-metadata">

### Author: ![Infinisil](https://discourse.nixos.org/user_avatar/discourse.nixos.org/infinisil/32/26523_2.png) [@Infinisil](https://discourse.nixos.org/u/Infinisil)
#### Post date: [May 1, 2024, 9:53pm UTC](https://discourse.nixos.org/t/filtering-source-trees-with-nix-and-nixpkgs/19148/6 "2024-05-01T21:53:56Z")

</div>

> [@Ten](#):
>
> EDIT: Ah [this post](https://discourse.nixos.org/t/easy-source-filtering-with-file-sets/29117) seems to suggest that maybe lib.fileset by default does filter empty directories? Does this mean then that [#8820](https://github.com/NixOS/nix/issues/8820) (that you opened!) is out of date?

Indeed, `lib.fileset` cannot even at all express the notion of “empty directories” because it (conceptually) has no notion of directories at all (it’s just a set of files)! ~~So that’s also why the issue there is still up-to-date, because there _might_ be use cases where you do need empty directories, so when you convert to a store path, you’d need some non-fileset argument to specify that.~~ The issue there however is just for _efficiency_ reasons. Having that feature would allow `lib.fileset` to be faster 🙂

> [@Ten](#):
>
> Does that mean that `fileset` will always traverse the entire file tree recursively?

`fileset.fileFilter` _does_ recurse all the way if you use it as-is, but because the combinators are lazy, you can prevent that from happening:

```nix
# All rust files in the current directory that are not in ./target
# This prevents recursing into ./target!
difference
  (fileFilter (file: file.hasExt "rs") ./.)
  ./target

```

As you discovered, you can also convert from `lib.sources`-based values, though I’d avoid that since you lose some laziness properties that way.

Note that for the use-case of filtering out directories if you just know their name, but not their path, there’s a recent discussion in [`lib.fileset` should have a way to filter directories · Issue #271307 · NixOS/nixpkgs · GitHub](https://github.com/NixOS/nixpkgs/issues/271307) 🙂
