feature | start-date | author | co-authors | shepherd-team | shepherd-leader | related-issues |
---|---|---|---|---|---|---|
flakes |
2019-07-09 |
Eelco Dolstra |
TBD |
Domen Kožar, Alyssa Ross, Shea Levy, John Ericson |
Domen Kožar |
(will contain links to implementation PRs) |
This RFC proposes a mechanism to package Nix expressions into composable entities called "flakes". Flakes allow hermetic, reproducible evaluation of multi-repository Nix projects; impose a discoverable, standard structure on Nix projects; and replace previous mechanisms such as Nix channels and the Nix search path.
Flakes are motivated by a number of serious shortcomings in Nix:
-
While Nix pioneered reproducible builds, sadly, Nix expressions are not nearly as reproducible as Nix builds. Nix expressions can access arbitrary files (such as
~/.config/nixpkgs/config.nix
), environment variables, and Git repositories. This means for instance that it is hard to ensure reproducible evaluation of NixOS or NixOps configurations. -
Nix projects lack discoverability and a standard structure. For example, it's just convention that a repository has a
release.nix
for Hydra jobs and adefault.nix
for packages. -
There is no standard way to compose Nix projects. Typical ways are to rely on the Nix search path (e.g.
import <nixpkgs>
) or to usefetchGit
orfetchTarball
. The former has poor reproducibility, while the latter is bad UX because of the need to manually update Git hashes to update dependencies. -
nix-channel
needs a replacement: channels are hard to create, users cannot easily pin specific versions of channels, channels interact in ad hoc ways with the Nix search path, and so on.
The flakes mechanism seeks to address all these problems. This RFC,
however, only describes the format and semantics of flakes; it doesn't
describe changes to the nix
command to support flakes.
A flake is a directory that contains a file named flake.nix
in the
root directory. flake.nix
specifies some metadata about the flake
such as dependencies (called inputs), as well as its outputs (the
Nix values such as packages or NixOS modules provided by the flake).
As an example, below is the flake.nix
of
dwarffs
(a FUSE filesystem
for automatically fetching DWARF debug symbols by ELF build ID). It
depends on the Nixpkgs flake and provides a package (i.e. an
installable derivation) and a NixOS module.
{
description = "A filesystem that fetches DWARF debug info from the Internet on demand";
outputs = { self, nixpkgs }: rec {
packages.x86_64-linux.dwarffs =
with nixpkgs.packages.x86_64-linux;
with nixpkgs.builders;
with nixpkgs.lib;
stdenv.mkDerivation {
name = "dwarffs-0.1.${substring 0 8 self.lastModifiedDate}";
buildInputs = [ fuse nix nlohmann_json boost ];
NIX_CFLAGS_COMPILE = "-I ${nix.dev}/include/nix -include ${nix.dev}/include/nix/config.h -D_FILE_OFFSET_BITS=64";
src = self;
installPhase =
''
mkdir -p $out/bin $out/lib/systemd/system
cp dwarffs $out/bin/
ln -s dwarffs $out/bin/mount.fuse.dwarffs
cp ${./run-dwarffs.mount} $out/lib/systemd/system/run-dwarffs.mount
cp ${./run-dwarffs.automount} $out/lib/systemd/system/run-dwarffs.automount
'';
};
nixosModules.dwarffs = ...;
defaultPackage.x86_64-linux = packages.x86_64-linux.dwarffs;
checks.build = packages.x86_64-linux.dwarffs;
};
}
A flake has the following attributes:
-
description
: A short description of the flake. -
inputs
: An attrset specifying the dependencies of the flake (described below). -
outputs
: A function that, given an attribute set containing the outputs of each of the input flakes keyed by their identifier, yields the Nix values provided by this flake. Thus, in the example above,inputs.nixpkgs
contains the result of the call to theoutputs
function of thenixpkgs
flake, andinputs.nixpkgs.packages.fuse
refers to thepackages.fuse
output attribute ofnixpkgs
.In addition to the outputs of each input, each input in
inputs
also contains some metadata about the inputs. These are:-
outPath
: The path in the Nix store of the flake's source tree. This means that you could import Nixpkgs in a more legacy-ish way by writingwith import inputs.nixpkgs { system = "x86_64-linux"; };
since
nixpkgs
still contains a/default.nix
. In this case we bypass its outputs entirely and only use the flake mechanism to get its source tree. -
rev
: The commit hash of the flake's repository, if applicable. -
revCount
: The number of ancestors of the revisionrev
. This is not available forgithub
repositories (see below), since they're fetched as tarballs rather than as Git repositories. -
lastModifiedDate
: The commit time of the revisionrev
, in the format%Y%m%d%H%M%S
(e.g.20181231100934
). UnlikerevCount
, this is available for both Git and GitHub repositories, so it's useful for generating (hopefully) monotonically increasing version strings. -
lastModified
: The commit time of the revisionrev
as an integer denoting the number of seconds since 1970. -
narHash
: The SHA-256 (in SRI format) of the NAR serialization of the flake's source tree.
The value returned by the
outputs
function must be an attribute set. The attributes can have arbitrary values; however, some tools may require specific attributes to have a specific value (e.g. thenix
command may expect the value ofpackages.x86_64-linux
to be an attribute set of derivations built for thex86_64-linux
platform). -
The attribute inputs
specifies the dependencies of a flake. These
specify the location of the dependency, or a symbolic flake identifier
that is looked up in a registry or in a command-line flag. For
example, the following specifies a dependency on the Nixpkgs and import-cargo
repositories:
# A GitHub repository.
inputs.import-cargo = {
type = "github";
owner = "edolstra";
repo = "import-cargo";
};
# An indirection through the flake registry.
inputs.nixpkgs = {
type = "indirect";
id = "nixpkgs";
};
Each input is fetched, evaluated and passed to the outputs
function
as a set of attributes with the same name as the corresponding
input. The special input named self
refers to the outputs and source
tree of this flake. Thus, a typical outputs
function looks like
this:
outputs = { self, nixpkgs, import-cargo }: {
... outputs ...
};
It is also possible to omit inputs entirely and only list them as
expected function arguments in outputs
. Thus,
outputs = { self, nixpkgs }: ...;
without an inputs.nixpkgs
attribute will simply look up nixpkgs
in
the flake registry.
Repositories that don't contain a flake.nix
can also be used as
inputs, by setting the input's flake
attribute to false
:
inputs.grcov = {
type = "github";
owner = "mozilla";
repo = "grcov";
flake = false;
};
outputs = { self, nixpkgs, grcov }: {
packages.x86_64-linux.grcov = stdenv.mkDerivation {
src = grcov;
...
};
};
The following input types are specified at present:
-
git
: A Git repository or dirty local working tree. -
github
: A more efficient scheme to fetch repositories from GitHub as tarballs. These have slightly different semantics fromgit
(in particular, therevCount
attribute is not available). -
gitlab
: Likegithub
, but for Git repositories hosted on GitLab. -
tarball
: A.tar.{gz,xz,bz2}
file. -
path
: A directory in the file system. This generally should be avoided in favor ofgit
inputs, sincepath
inputs have no concept of revisions (only a content hash) or tracked files (anything in the source directory is copied). -
hg
: A Mercurial repository.
Transitive inputs can be overriden from a flake.nix
file. For
example, the following overrides the nixpkgs
input of the nixops
input:
inputs.nixops.inputs.nixpkgs = {
type = "github";
owner = "my-org";
repo = "nixpkgs";
};
It is also possible to "inherit" an input from another input. This is
useful to minimize flake dependencies. For example, the following sets
the nixpkgs
input of the top-level flake to be equal to the
nixpkgs
input of the dwarffs
input of the top-level flake:
inputs.nixops.follows = "dwarffs/nixpkgs";
The value of the follows
attribute is a /
-separated sequence of
input names denoting the path of inputs to be followed from the root
flake.
Overrides and follows
can be combined, e.g.
inputs.nixops.inputs.nixpkgs.follows = "dwarffs/nixpkgs";
sets the nixpkgs
input of nixops
to be the same as the nixpkgs
input of dwarffs
. It is worth noting, however, that it is generally
not useful to eliminate transitive nixpkgs
flake inputs in this
way. Most flakes provide their functionality through Nixpkgs overlays
or NixOS modules, which are composed into the top-level flake's
nixpkgs
input; so their own nixpkgs
input is usually irrelevant.
Inputs specified in flake.nix
are typically "unlocked" in that they
don't specify an exact revision. To ensure reproducibility, Nix will
automatically generate and use a lock file called flake.lock
in
the flake's directory. The lock file contains a graph structure
isomorphic to the graph of dependencies of the root flake. Each node
in the graph (except the root node) maps the (usually) unlocked input
specifications in flake.nix
to locked input specifications. Each
node also contains some metadata, such as the dependencies (outgoing
edges) of the node.
For example, if flake.nix
has the inputs in the example above, then
the resulting lock file might be:
{
"version": 6,
"root": "n1",
"nodes": {
"n1": {
"inputs": {
"nixpkgs": "n2",
"import-cargo": "n3",
"grcov": "n4"
}
},
"n2": {
"inputs": {},
"locked": {
"owner": "edolstra",
"repo": "nixpkgs",
"rev": "7f8d4b088e2df7fdb6b513bc2d6941f1d422a013",
"type": "github",
"lastModified": 1580555482,
"narHash": "sha256-OnpEWzNxF/AU4KlqBXM2s5PWvfI5/BS6xQrPvkF5tO8="
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"n3": {
"inputs": {},
"locked": {
"owner": "edolstra",
"repo": "import-cargo",
"rev": "8abf7b3a8cbe1c8a885391f826357a74d382a422",
"type": "github",
"lastModified": 1567183309,
"narHash": "sha256-wIXWOpX9rRjK5NDsL6WzuuBJl2R0kUCnlpZUrASykSc="
},
"original": {
"owner": "edolstra",
"repo": "import-cargo",
"type": "github"
}
},
"n4": {
"inputs": {},
"locked": {
"owner": "mozilla",
"repo": "grcov",
"rev": "989a84bb29e95e392589c4e73c29189fd69a1d4e",
"type": "github",
"lastModified": 1580729070,
"narHash": "sha256-235uMxYlHxJ5y92EXZWAYEsEb6mm+b069GAd+BOIOxI="
},
"original": {
"owner": "mozilla",
"repo": "grcov",
"type": "github"
},
"flake": false
}
}
}
This graph has 4 nodes: the root flake, and its 3 dependencies. The
nodes have arbitrary labels (e.g. n1
). The label of the root node of
the graph is specified by the root
attribute. Nodes contain the
following fields:
-
inputs
: The dependencies of this node, as a mapping from input names (e.g.nixpkgs
) to node labels (e.g.n2
). -
original
: The original input specification fromflake.lock
, as a set ofbuiltins.fetchTree
arguments. -
locked
: The locked input specification, as a set ofbuiltins.fetchTree
arguments. Thus, in the example above, when we build this flake, the inputnixpkgs
is mapped to revision7f8d4b088e2df7fdb6b513bc2d6941f1d422a013
of theedolstra/nixpkgs
repository on GitHub.It also includes the attribute
narHash
, specifying the expected contents of the tree in the Nix store (as computed bynix hash-path
), and may include input-type-specific attributes such as thelastModified
orrevCount
. The main reason for these attributes is to allow flake inputs to be substituted from a binary cache:narHash
allows the store path to be computed, while the other attributes are necessary because they provide information not stored in the store path. -
flake
: A Boolean denoting whether this is a flake or non-flake dependency. Corresponds to theflake
attribute in theinputs
attribute inflake.nix
.
The original
and locked
attributes are omitted for the root
node. This is because we cannot record the commit hash or content hash
of the root flake, since modifying flake.lock
will invalidate these.
The graph representation of lock files allows circular dependencies between flakes. For example, here are two flakes that reference each other:
{
inputs.b = ... location of flake B ...;
# Tell the 'b' flake not to fetch 'a' again, to ensure its 'a' is
# *this* 'a'.
inputs.b.inputs.a.follows = "";
outputs = { self, b }: {
foo = 123 + b.bar;
xyzzy = 1000;
};
}
and
{
inputs.a = ... location of flake A ...;
inputs.a.inputs.b.follows = "";
outputs = { self, a }: {
bar = 456 + a.xyzzy;
};
}
Lock files transitively lock direct as well as indirect dependencies. That is, if a lock file exists and is up to date, Nix will not look at the lock files of dependencies. However, lock file generation itself does use the lock files of dependencies by default.
Lock files are not sufficient by themselves to ensure reproducible evaluation. We also need to disallow certain impurities that the Nix language previously allowed. In particular, the following are disallowed in a flake:
-
Access to files outside of the top-level flake or its inputs, as well as paths fetched using
fetchTarball
,fetchGit
and so on without a commit hash or content hash. In particular this means that Nixpkgs will not be able to use~/.config/nixpkgs
anymore. -
Access to the environment. This means that
builtins.getEnv "<var>"
always returns an empty string. -
Access to the system type (
builtins.currentSystem
). -
Access to the current time (
builtins.currentTime
). -
Use of the Nix search path (
<...>
); composition must be done through flake inputs orfetchX
builtins.
Pure evaluation breaks certain workflows. In particular, it breaks the
use of the Nixpkgs configuration file. Similarly, there are people who
rely on $NIX_PATH
to pass configuration data to NixOps
configurations.
For composition of multi-repository projects, the main alternative is
to continue on with explicit fetchGit
/ fetchTarball
calls to pull
in other repositories. However, since there is no explicit listing of
dependencies, this does not provide automatic updating.
Instead of a flake.nix
, flakes could store their metadata in a
simpler format such as JSON or TOML. This avoids the Turing tarpit
where getting flake metadata requires the execution of an arbitrarily
complex, possibly non-terminating program.
Flakes could be implemented as an external tool on top of Nix. Indeed,
there is nothing that flakes allow you to do that couldn't previously
be done using fetchGit
, the --pure-eval
flag and some shell
scripting. However, implementing flake-like functionality in an
external tool would defeat the goals of this RFC. First, it probably
wouldn't lead to a standard way to structure and compose Nix projects,
since we might well end up with numerous competing
"standards". Second, it would degrade rather than improve the Nix UX,
since users would now have to deal with Nix and the flake-like tool
on top of it.
-
Should flakes have arguments (like "system type")? This must be done in a way that maintains hermetic evaluation and evaluation caching.
-
Currently, if flake dependencies (repositories or branches) get deleted upstream, rebuilding the flake may fail. (This is similar to
fetchurl
referencing a stale URL.) We need a command to gather all flake dependencies and copy them somewhere else (possibly vendor them into the repository of the calling flake). -
Maybe flake metadata should be stored in a
flake.json
orflake.toml
file. This would prevent ambiguities when the Nix language changes in a future edition.
- Currently flake outputs are untyped; we only have some conventions
about what they should be (e.g.
packages
should be an attribute set of derivations). For discoverability, it would be nice if outputs were typed. Maybe this could be done via the Nix configurations concept (https://gist.github.com/edolstra/29ce9d8ea399b703a7023073b0dbc00d).
Funding for the development of the flakes prototype was provided by
Target Corporation. The flakes project was
inspired/motivated by Shea Levy's work on
require.nix
and
ensuing discussions at NixCon 2018.