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 afilesetintosource.lib.fileset.union: Takes twofilesetsand merges them together.lib.fileset.unions: Takes a list offilesetsand merges them together.lib.fileset.fileFilter: Takes a predicate and afileset, and filters out files from thefileset.
# 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
builtinsfunctions, 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.