Demystifying Nix: declaring build sources

Published: 2026-10-14

Nix is an excellent framework for building software. There’s a lot to be gained by programmatically defining how your software is built, but the language does suffer from somewhat esoteric documentation. This is my first article in what I hope becomes a series sharing and dissecting specific parts of a Nix build in an effort to demystify the process.

Declaring source files for the build context

Much like authoring a container with Docker or Podman, a proper build context needs to be defined. Nix builds are self-contained and hermetically sealed processes that operate on a language specific datatype called a source, a collected set of files that exist in the /nix/store.

So how does one define a source within the Nix? Thankfully, Nix provides a standard library of functions and other types designed to do just that!


lib.fileset

The Nix standard library, accessed via the lib input, exposes a few modules and functions to help define source files for the build. The best place to start is defining a source from a local repository. In Nix, collections of local file paths have their own datatype called a fileset, and lib.filesets is module designed to work with and manipulate filesets.

Useful functions for working with filesets include:

  • lib.fileset.toSource: Tries to convert a fileset into source.
  • lib.fileset.union: Takes two filesets and merges them together.
  • lib.fileset.unions: Takes a list of filesets and merges them together.
  • lib.fileset.fileFilter: Takes a predicate and a fileset, and filters out files from the fileset.
# Note that the lib input is included to access the module
{lib, ...}: let
  # You can use the "with" keyword to alias the module namespace.
  # Any declaration without a full path in the function
  # will use the lib.fileset path
  goSource = with lib.fileset; toSource {
    # Creating a source from local files requires a root directory
    # This root directory represents the boundaries of the build context
    root = ./.;
    fileset = unions [
      ./go.mod
      ./go.sum
      ./main.go
      # Example excluding files without a ".go" extension
      fileFilter (file: file.hasExt "go") ./cmd
    ];
  };
in {...}

As shown in the above example, combining these functions together can offer a very robust means of including and excluding files from your context. As with image authoring, any change to the context can trigger rebuilds, so it’s recommended to keep your context as lean as possible to prevent unwanted invalidation of the cache.

builtins.fetchGit

While defining sources from local files can be helpful to build you own projects, Nix also offers a means to define sources from remote repositories. builtins.fetchGit is function that allows you to fetch source files from a git forge.

  • This is part of the builtins functions, and can be called without specifying its input in the top level function signature.
  • Calling this function returns a Nix source, meaning it can be immediately be passed to a build function without further manipulation.
# Example fetching via SSH
{...}: let
  source = builtins.fetchGit {
    url = "[email protected]:my-secret/repository.git";
    ref = "master";
    rev = "adab8b916a45068c044658c4158d81878f9ed1c3";
  };
in {...}

lib.sources

Wile fetching from a remote repository is helpful, it doesn’t offer the same amount of control the lib.filesets module has for manipulating what gets added to the build context. Fetching returns a source which means that it already exists in the /nix/store. To further curate what gets added to the build context requires using the lib.sources module.

Useful functions in this module include:

  • lib.sources.cleanSource: Filters a source and removes any version control directories.
  • lib.sources.cleanSourceWith: Passes in a filter function and additionally filters out any version control files.
  • lib.sources.sourceByGlob: Constructs a new source using glob patterns.

Since lib.sources returns and operates on sources, it’s important to note that any function call from this module will produce a new source in the /nix/store. Too many calls to lib.sources can unintentionally explode the amount of files in the /nix/store. If needed, lib.sources.cleanSourceWith allows you to chain multiple filter functions together without producing intermediate source directories in the /nix/store.

Personally, I find just using lib.sources.sourceByGlob ergonomic enough to filter in only what I need from the repository.

{lib, ...}: let
  repo = builtins.fetchGit {
    url = "[email protected]:my-secret/repository.git";
    ref = "master";
    rev = "adab8b916a45068c044658c4158d81878f9ed1c3";
  };
  # Note: the glob patterns are strings not filesets and need to be quoted
  rustSource = with lib.sources.sourceByGlob repo [
    "Cargo.toml"
    "Cargo.lock"
    "src/**/*.rs"
  ];
in { ... }

Final thoughts

Et voilà! With the information in this article, the essentials for declaring the source for a Nix build has been covered. lib.filesets lets you define sources using local files, and lib.sources lets you manipulate sources fetched remotely.

There’s more than just sources, however, to declaring a Nix build context. In the next article, I’ll be going over Nix’s concept of build inputs, what they are, how they affect the build, and how to declare them.

Do you have a cool idea? I love helping bring complex visions to life.

Say hi or follow me here: