flakey flakey
This commit is contained in:
parent
884b39cd61
commit
d192288b75
1 changed files with 664 additions and 0 deletions
664
content/posts/flakes-arent-real.md
Normal file
664
content/posts/flakes-arent-real.md
Normal file
|
|
@ -0,0 +1,664 @@
|
||||||
|
+++
|
||||||
|
date = "2023-11-25"
|
||||||
|
draft = true
|
||||||
|
path = "/blog/flakes-arent-real"
|
||||||
|
tags = ["nix"]
|
||||||
|
title = "Flakes aren't real and cannot hurt you: a guide to using Nix flakes the non-flake way"
|
||||||
|
+++
|
||||||
|
|
||||||
|
Inflammatory title out of the way, let's go.
|
||||||
|
|
||||||
|
I think that Nix flakes have some considerable benefits, such as:
|
||||||
|
|
||||||
|
* Convenient pinning of evaluation-time dependencies
|
||||||
|
* Eliminating pointless rebuilds of code by only including tracked files in
|
||||||
|
builds
|
||||||
|
* Making Nix code, on average, much more reproducible by pervasive pinning
|
||||||
|
* Allegedly caching evaluation
|
||||||
|
* Possibly making Nix easier to learn by reducing the amount of poking at
|
||||||
|
strange attribute sets and general `NIX_PATH` brokenness
|
||||||
|
|
||||||
|
However, at the same time, there are a few things that one might be led to
|
||||||
|
think about flakes that are not the most effective way of doing things. I
|
||||||
|
personally use flakes relatively extensively in my own work, but there are
|
||||||
|
several ways I use them that are not standard, with reason.
|
||||||
|
|
||||||
|
Flakes are *optional*, and as much as some people whose salary depends on it
|
||||||
|
might say otherwise, they are not the (only) future of Nix: they are simply a
|
||||||
|
special entry point for Nix code with a built in pinning system, nothing more,
|
||||||
|
nothing less.
|
||||||
|
|
||||||
|
Nix continues to gather a reputation for bad documentation, in part because the
|
||||||
|
official documentation for nixpkgs and NixOS is *de facto* not allowed to talk
|
||||||
|
about flakes, as a policy. This situation is certainly partially due to a
|
||||||
|
divide between Nix developers and nixpkgs developers, which are groups with
|
||||||
|
surprisingly little overlap.
|
||||||
|
|
||||||
|
Flakes also are a symptom or cause of much intra-community strife between
|
||||||
|
"pro-flakes" and "anti-flakes" factions, but this situation is at some level a
|
||||||
|
sign of broken consensus processes and various actors trying to sidestep them,
|
||||||
|
an assumption by many people that the docs are "outdated" for not using flakes,
|
||||||
|
and the bizarre proliferation of flakes everywhere in blog posts or tutorials
|
||||||
|
leading to a belief that they are required for everything.
|
||||||
|
|
||||||
|
This post is about how to architect Nix projects in general, with a special eye
|
||||||
|
on how to do so with flakes while avoiding their limitations. It tries to
|
||||||
|
dispel misconceptions that can develop in such a monoculture.
|
||||||
|
|
||||||
|
# "Flakes are the composition primitive in Nix"
|
||||||
|
|
||||||
|
The Nix language, functions, and nixpkgs utilities are an effective composition
|
||||||
|
primitive, and are much better suited for putting parts of a project together,
|
||||||
|
especially if it is in a monorepo.
|
||||||
|
|
||||||
|
The most flexible way of building large systems with Nix is to merely use
|
||||||
|
flakes as an entry point, and develop the rest using "old" tools. This is for
|
||||||
|
multitudinous reasons:
|
||||||
|
|
||||||
|
- Flakes couple version control integration, dependency management and lockfile
|
||||||
|
management. In medium sized projects, even at the scale of my dotfiles,
|
||||||
|
locking dependencies of subprojects is often highly undesirable.
|
||||||
|
|
||||||
|
They're not ideal for either working in the large or in the small: in the
|
||||||
|
small, there is too much overhead in writing a separate `flake.nix` for some
|
||||||
|
tiny utility, and in the large, for example, in nixpkgs, if flakes were
|
||||||
|
actually used for dependency management, `flake.nix` would be 100,000 lines
|
||||||
|
of `inputs` long.
|
||||||
|
- In terms of making flexible builds, flakes don't support configuration
|
||||||
|
[except through hilarious abuses of
|
||||||
|
`--override-input`][boolean-option]. This means that all build configuration
|
||||||
|
variants have to be anticipated ahead of time, or that traditional
|
||||||
|
nixpkgs/Nix language primitives need to be used instead.
|
||||||
|
- Flakes as a composition primitive is completely incompatible with cross
|
||||||
|
compilation. Due to the lack of configuration support, `packages.${system}`
|
||||||
|
cannot be used for cross compilation: there is nowhere to specify the
|
||||||
|
architecture to build with.
|
||||||
|
|
||||||
|
[boolean-option]: https://github.com/boolean-option
|
||||||
|
|
||||||
|
Because of all of this, even in a flakes world, to compose software in the
|
||||||
|
large *and* in the small reusably and efficiently, the other composition
|
||||||
|
primitives provided by Nix and nixpkgs remain the best choices to assemble
|
||||||
|
software. A flake can then be relegated to merely an entry point and a way of
|
||||||
|
acquiring dependencies that are required for evaluation (build-time
|
||||||
|
dependencies should use `pkgs.fetchurl`, `fetchFromGitHub`, etc.
|
||||||
|
|
||||||
|
For example, to expose multiple configurations of a program, one might write it
|
||||||
|
the traditional way, using a lambda accepting some configuration parameters,
|
||||||
|
then call that lambda multiple times to expose multiple output attributes
|
||||||
|
inside the flake itself. This separates the capability to configure the
|
||||||
|
software from the actual defined configurations of the software, and avoids
|
||||||
|
letting the configuration non-system of flakes define how the internals of the
|
||||||
|
build definition work.
|
||||||
|
|
||||||
|
One of the largest simultaneous advantages and disadvantages of the Nix
|
||||||
|
language is that it is a Turing complete language, which causes pain to static
|
||||||
|
analysis, but is also one of its largest assets: you can program it. This can
|
||||||
|
be seen as a problem, but it also is awesome: you can programmatically patch
|
||||||
|
packages, define configuration dynamically, read files of arbitrary formats and
|
||||||
|
more.
|
||||||
|
|
||||||
|
Nix is a functional programming language, which means that its fundamental
|
||||||
|
composition primitive is the function. Even "fancy" objects like NixOS modules
|
||||||
|
or overlays are just functions that can be moved into separate files, imported,
|
||||||
|
or created through partial application of other functions (although, since
|
||||||
|
`imports` in modules are deduplicated by file name, NixOS modules generally
|
||||||
|
should be imported by path instead of generated by functions).
|
||||||
|
|
||||||
|
See the next section for concrete ways of composing software together.
|
||||||
|
|
||||||
|
# "Flakes are where you put your Nix code"
|
||||||
|
|
||||||
|
Flakes are merely a fancy schema for making a standardized entry point into Nix
|
||||||
|
code. Most of the Nix code in a project of any significant size should not be
|
||||||
|
in `flake.nix`, for several reasons.
|
||||||
|
|
||||||
|
The most trivial reason to put as little code as possible in `flake.nix` is
|
||||||
|
maintainability: there is as much rightward drift in `flake.nix` as in recent
|
||||||
|
German and Dutch elections (concerningly much!), so from just that perspective,
|
||||||
|
it's useful to move things out of it.
|
||||||
|
|
||||||
|
Let's talk about some standard patterns that have existed before flakes did,
|
||||||
|
which still are relevant in a flakes world.
|
||||||
|
|
||||||
|
## [`package.nix`][package-nix]
|
||||||
|
|
||||||
|
[package-nix]: https://github.com/nixos/nixpkgs/blob/41acc25766fbc611cd10cb043bc7cab91d2fd088/pkgs/by-name/README.md
|
||||||
|
|
||||||
|
I am using `package.nix` to refer to the standard way for writing packages in
|
||||||
|
nixpkgs style, which are invoked with `callPackage`. This is as opposed to
|
||||||
|
writing something directly in `flake.nix` using `pkgs`.
|
||||||
|
|
||||||
|
A `package.nix` file looks something like so:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{ # receives(*) pkgs.hello and pkgs.stdenv
|
||||||
|
hello, stdenv,
|
||||||
|
# can be overridden with `yourPackage.override { enableSomething = true; }`
|
||||||
|
enableSomething ? false
|
||||||
|
}:
|
||||||
|
finalAttrs: # optional finalAttrs to refer to the set below; preferred over using `rec` attr sets
|
||||||
|
stdenv.mkDerivation {
|
||||||
|
# ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Package definitions should be written with `callPackage` if possible, rather
|
||||||
|
than inline in `flake.nix`, since using `package.nix` makes them into small,
|
||||||
|
composable, configurable, and portable units of software. Also, by using
|
||||||
|
`callPackage` and writing in nixpkgs style, it becomes a lot easier to move
|
||||||
|
packages between projects, and indeed to upstream them to nixpkgs, since they
|
||||||
|
look and work a familiar way.
|
||||||
|
|
||||||
|
### Cross compilation
|
||||||
|
|
||||||
|
A lesser-known fact is that `callPackage` is load-bearing for cross
|
||||||
|
compilation. If you write `pkgs.foo` in `nativeBuildInputs`, such a Nix
|
||||||
|
expression will break under cross compilation, but `foo` as an argument from
|
||||||
|
`callPackage` will not. This is because `callPackage` will magically resolve
|
||||||
|
`foo` appearing inside `nativeBuildInputs` to mean `pkgs.buildPackages.foo`;
|
||||||
|
that is, a package built for the build computer.
|
||||||
|
|
||||||
|
`callPackage` evaluates a Nix file multiple times with different
|
||||||
|
arguments and splices the results together such that `buildInputs` magically
|
||||||
|
receives target packages, and `nativeBuildInputs` receives build packages,
|
||||||
|
even if the same package name appears in both. Magic ✨
|
||||||
|
|
||||||
|
That is, in the following intentionally-flawed-for-other-reasons `flake.nix`:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{...}: {
|
||||||
|
outputs = { nixpkgs, ... }:
|
||||||
|
let pkgs = nixpkgs.legacyPackages.x86_64-linux;
|
||||||
|
in {
|
||||||
|
packages.x86_64-linux.x = pkgs.callPackage ./package.nix { };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
then `package.nix`:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{ stdenv, hello, openssl }:
|
||||||
|
stdenv.mkDerivation {
|
||||||
|
# ...
|
||||||
|
# things used in the build go in here
|
||||||
|
nativeBuildInputs = [ hello ];
|
||||||
|
# libraries used by the resulting program go in here
|
||||||
|
buildInputs = [ openssl ];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Incidentally, notice anything there? Yeah, it's flakes completely not
|
||||||
|
supporting cross compilation. See the next point. :D
|
||||||
|
|
||||||
|
It's possible to use the `pkgs.buildPackages` attribute to pull things into
|
||||||
|
`nativeBuildInputs`, and `pkgs` for `buildInputs` but it is not conventional
|
||||||
|
to do so, and is quite verbose.
|
||||||
|
|
||||||
|
[See the manual about these callPackage shenanigans][callPackage-intricacy]
|
||||||
|
for more details. See also: [the manual about dependency categories][cats].
|
||||||
|
|
||||||
|
## [Overlays]
|
||||||
|
|
||||||
|
[Overlays]: https://nixos.org/manual/nixpkgs/stable/#sec-overlays-definition
|
||||||
|
|
||||||
|
[An overlay][Overlays] is a function overriding nixpkgs which is evaluated
|
||||||
|
until it reaches a [fixed point]. An overlay takes two arguments, `final` and
|
||||||
|
`prev` (sometimes also called `self` and `super`), and returns an attribute set
|
||||||
|
that is shallowly replaced on top of nixpkgs with `//`.
|
||||||
|
|
||||||
|
It is useful as a means for distributing sets of software outside of nixpkgs,
|
||||||
|
and still is useful in that role in a flakes world, since overlays are simple
|
||||||
|
functions that can be evaluated against any version of nixpkgs, allowing for
|
||||||
|
cross compilation to work properly. One may notice that the `overlays` flake
|
||||||
|
output is not architecture specific, which follows from their definition as
|
||||||
|
functions that take package sets and return modifications to make.
|
||||||
|
|
||||||
|
Evaluation to a fixed point means that it is evaluated as many times as necessary
|
||||||
|
until it stops referring to the `final` argument (or overflows the stack). This
|
||||||
|
idea appears in many places, including tables of contents in LaTeX, [Typst] or
|
||||||
|
other typesetting programs: by generating the table of contents, you may affect
|
||||||
|
the layout of subsequent pages and change their page numbers, but after the
|
||||||
|
first run of that, the layout will probably not change, since the only change
|
||||||
|
is the numbers, so *that* iteration likely converges to the final result.
|
||||||
|
|
||||||
|
`final` gives the *final* version of the attribute set, after overlays have
|
||||||
|
been evaluated as far as they will go; your overlay may be run multiple times
|
||||||
|
in evaluating an attribute in `final`, or even cause infinite recursion. `prev`
|
||||||
|
gives the version of nixpkgs prior to the present overlay or any further
|
||||||
|
overlays.
|
||||||
|
|
||||||
|
For example, we could write an overlay to override GNU Hello to be a wrapper
|
||||||
|
that makes a [reference to an excellent retrocomputing series][hellorld].
|
||||||
|
Content of `overlay.nix`:
|
||||||
|
|
||||||
|
[hellorld]: https://www.youtube.com/watch?v=gQ6mwbTGXGQ
|
||||||
|
|
||||||
|
```nix
|
||||||
|
final: prev: {
|
||||||
|
hello = final.writeShellScriptBin "hello" ''
|
||||||
|
${prev.hello}/bin/hello -g "hellorld" "$@"
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
```
|
||||||
|
» nix run --impure --expr '(import <nixpkgs> { overlays = [ (import ./overlay.nix) ]; }).hello'
|
||||||
|
hellorld
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, the attribute `hello` of our modified `nixpkgs` now is our script that
|
||||||
|
calls the original `hello` to say "hellorld".
|
||||||
|
|
||||||
|
It's pretty easy to accidentally cause infinite recursion with overlays if
|
||||||
|
their laziness isn't correct. For example, attribute sets' attribute names are
|
||||||
|
evaluated strictly, with all names in an attribute set evaluated immediately,
|
||||||
|
but the values of attributes are lazily evaluated. [There have been attempts to
|
||||||
|
change this][lazy-attrs] but they were canned for performance reasons. Strict
|
||||||
|
attribute names can be a foot-gun, causing confusing infinite recursion in some
|
||||||
|
cases using `mapAttrs` or similar mechanisms on `prev` to generate the set of
|
||||||
|
things to override.
|
||||||
|
|
||||||
|
Infinite recursion is not typically a problem if an overlay doesn't actually
|
||||||
|
replace anything or contain self-references, as may be the case for overlays
|
||||||
|
distributing very simple software, and we can take advantage of that as shown
|
||||||
|
in the next section.
|
||||||
|
|
||||||
|
[Typst]: https://typst.app
|
||||||
|
|
||||||
|
[fixed point]: https://en.wikipedia.org/wiki/Fixed-point_combinator
|
||||||
|
[lazy-attrs]: https://github.com/NixOS/nix/issues/4090
|
||||||
|
|
||||||
|
### The place of overlays in a flakes world
|
||||||
|
|
||||||
|
*Flakes don't support cross compilation.*
|
||||||
|
|
||||||
|
I am being a little bit tricky with the wording here. Flakes don't *stop* you
|
||||||
|
from doing cross compilation, but you have to do an end-run around flakes and
|
||||||
|
do it the "old" way.
|
||||||
|
|
||||||
|
Because of this design fault in flakes, namely, the lack of support for
|
||||||
|
parameters, the most compatible way of writing packaging in a flake project
|
||||||
|
is to write the package definitions into an overlay first, then expose the
|
||||||
|
packages from the overlay. Consumers that need cross compilation can use the
|
||||||
|
overlay, and consumers that don't care can use it through `packages`.
|
||||||
|
|
||||||
|
Keeping in mind ["1000 instances of nixpkgs"][1000-nixpkgs], a reasonable way
|
||||||
|
of writing a flake that *doesn't modify anything in nixpkgs*, just adds stuff
|
||||||
|
is:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
|
inputs.flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
outputs = { self, nixpkgs, flake-utils, ... }:
|
||||||
|
let
|
||||||
|
out = system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
appliedOverlay = self.overlays.default pkgs pkgs;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
packages.myPackage = appliedOverlay.myPackage;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
flake-utils.lib.eachDefaultSystem out // {
|
||||||
|
overlays.default = final: prev: {
|
||||||
|
myPackage = final.callPackage ./package.nix { };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Downstream consumers that need to cross compile in spite of flakes can use
|
||||||
|
the overlay, and other consumers can use `packages` as normal.
|
||||||
|
|
||||||
|
This uses a cute trick of calling the overlay, which is just a function, with
|
||||||
|
both `final` and `prev` as the final nixpkgs attribute set. This definitely
|
||||||
|
does not work on all overlays, since overlays can make self-references using
|
||||||
|
`final`, and indeed often need to do so, if they contain multiple derivations
|
||||||
|
that depend on each other.
|
||||||
|
|
||||||
|
However, with a little bit more work, this can be overcome very cleanly, while
|
||||||
|
also avoiding any possibility of name shadowing problems!
|
||||||
|
|
||||||
|
<aside>
|
||||||
|
|
||||||
|
If you're thinking "just use a `rec` attribute set", that's unfortunately
|
||||||
|
clever but flawed: `rec` will receive the version as of the execution of your
|
||||||
|
file, but not any overridden version, which is not the case for `makeScope` and
|
||||||
|
similar tools from nixpkgs.
|
||||||
|
|
||||||
|
In order to regain the ability to make self-references without being a real
|
||||||
|
overlay that uses `prev`, consider using [makeScope] ([example from
|
||||||
|
nixpkgs][makeScope-ex]) to create a smaller *scope*, within which self
|
||||||
|
references to other things in the same scope are allowed.
|
||||||
|
|
||||||
|
For example, here we create a scope with a dependency between derivations.
|
||||||
|
Content of `test.nix`, which could equivalently be an overlay:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
let pkgs = import <nixpkgs> { };
|
||||||
|
in pkgs.callPackage ./scope.nix { makeScope = pkgs.lib.makeScope; }
|
||||||
|
```
|
||||||
|
|
||||||
|
and `scope.nix`:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{ makeScope, newScope, writeShellScriptBin }: makeScope newScope (self: {
|
||||||
|
meow = writeShellScriptBin "meow" ''
|
||||||
|
echo meow
|
||||||
|
'';
|
||||||
|
meow2 = writeShellScriptBin "meow2" ''
|
||||||
|
echo "meow is at ${self.meow}"
|
||||||
|
'';
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Which gives the following result:
|
||||||
|
|
||||||
|
```
|
||||||
|
» nix run --impure --expr '(import ./test.nix).meow'
|
||||||
|
meow
|
||||||
|
» nix run --impure --expr '(import ./test.nix).meow2'
|
||||||
|
meow is at /nix/store/aj0fhn8is6w8q85h0ramnqz2di92plwc-meow
|
||||||
|
» nix eval --impure --expr 'builtins.attrNames (import ./test.nix)'
|
||||||
|
[ "callPackage" "meow" "meow2" "newScope" "override" "overrideDerivation" "overrideScope" "overrideScope'" "packages" ]
|
||||||
|
```
|
||||||
|
|
||||||
|
[makeScope]: https://github.com/nixos/nixpkgs/blob/6a8b6b8f720b8d6f43ea870164eb489de5316077/lib/customisation.nix#L290-L303
|
||||||
|
[makeScope-ex]: https://github.com/nixos/nixpkgs/blob/7ae4510daf59d5a3724161c55eae96e45aa86801/pkgs/by-name/wi/windowmaker/dockapps/default.nix
|
||||||
|
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
If you do have to use a real overlay that needs to replace things, import
|
||||||
|
nixpkgs again from your flake with the overlay as an argument. It's fine. It's
|
||||||
|
just a second of gratuitous evaluation time:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
let pkgs = import nixpkgs { inherit system; overlays = [ self.overlays.default ]; };
|
||||||
|
in # ....
|
||||||
|
```
|
||||||
|
|
||||||
|
[1000-nixpkgs]: https://discourse.nixos.org/t/1000-instances-of-nixpkgs/17347/
|
||||||
|
|
||||||
|
[callPackage-intricacy]: https://nixos.org/manual/nixpkgs/stable/#ssec-cross-dependency-implementation
|
||||||
|
[cats]: https://nixos.org/manual/nixpkgs/stable/#ssec-stdenv-dependencies
|
||||||
|
|
||||||
|
### NixOS modules
|
||||||
|
|
||||||
|
NixOS modules are, like overlays and `package.nix`, fundamentally just
|
||||||
|
functions which are invoked in a fancy way, and are not a flakes construct.
|
||||||
|
|
||||||
|
As used in flakes with the `nixosModules.*` output, they are
|
||||||
|
*architecture independent* since they are just functions, and if defining a
|
||||||
|
module for software that is built by the same flake, one would generally want to
|
||||||
|
use an overlay in [`nixpkgs.overlays`][nixpkgs.overlays] or the trick above,
|
||||||
|
invoking the overlay with `pkgs` twice, to actually bring it in (again, to
|
||||||
|
remain cross compilation compatible).
|
||||||
|
|
||||||
|
To keep with the theme of putting things outside of `flake.nix` to enable
|
||||||
|
reusability, the code for the module can be placed in a separate file that is
|
||||||
|
imported. Then, `flake.nix` is used to import that module and inject
|
||||||
|
dependencies from its environment.
|
||||||
|
|
||||||
|
[nixpkgs.overlays]: https://nixos.org/manual/nixos/stable/options#opt-nixpkgs.overlays
|
||||||
|
|
||||||
|
#### Injecting dependencies
|
||||||
|
|
||||||
|
There are a couple of ways to inject dependencies into NixOS modules from a
|
||||||
|
flake, one of which is mildly uglier. Injecting values from `flake.nix` into
|
||||||
|
NixOS is required for a couple of reasons, most notably, to use flakes-managed
|
||||||
|
dependencies inside NixOS configurations. It is also necessary to [properly
|
||||||
|
configure `NIX_PATH` so `<nixpkgs>` resolves in a flake
|
||||||
|
configuration][nixpath], since you need the actual inputs from `flake.nix` to
|
||||||
|
get a proper reference to nixpkgs suitable to create a dependency on the actual
|
||||||
|
flake input.
|
||||||
|
|
||||||
|
[nixpath]: https://github.com/NixOS/nixpkgs/pull/254405
|
||||||
|
|
||||||
|
The simplest (and most reasonable, in my view) way to inject dependencies from
|
||||||
|
a flake is to write an inline module that has them in its lexical closure inside of
|
||||||
|
`flake.nix`. If you want to be fancy, you could even make an option to store
|
||||||
|
the injected dependencies:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
let depInject = { pkgs, lib, ... }: {
|
||||||
|
options.dep-inject = lib.mkOption {
|
||||||
|
type = with lib.types; attrsOf unspecified;
|
||||||
|
default = { };
|
||||||
|
};
|
||||||
|
config.dep-inject = {
|
||||||
|
# inputs comes from the outer environment of flake.nix
|
||||||
|
flake-inputs = inputs;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in {
|
||||||
|
nixosModules.default = { pkgs, lib, ... }: {
|
||||||
|
imports = [ depInject ];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The uglier and perhaps more well-known way to inject dependencies into NixOS
|
||||||
|
modules from flakes is [`specialArgs`][specialArgs]. This is uglier, since it gets dumped into
|
||||||
|
the arguments for every module, which is unlike how every other bit of data
|
||||||
|
flow works in NixOS, and it also doesn't work outside of the flake that's
|
||||||
|
actually invoking `nixpkgs.lib.nixosSystem`. The latter is the much more
|
||||||
|
sinister part, and the reason I would strongly recommend inline modules with
|
||||||
|
closures instead of `specialArgs`: they break flake composition.
|
||||||
|
|
||||||
|
To use `specialArgs`, an attribute set is passed into `nixpkgs.lib.nixosSystem`,
|
||||||
|
which then land in the arguments of NixOS modules:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
# ...
|
||||||
|
nixosConfigurations.something = nixpkgs.lib.nixosSystem {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
specialArgs = {
|
||||||
|
myPkgs = nixpkgs;
|
||||||
|
};
|
||||||
|
modules = {
|
||||||
|
{ pkgs, lib, myPkgs }: {
|
||||||
|
# do something with myPkgs
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
[specialArgs]: https://nixos.org/manual/nixos/unstable/options#opt-_module.args
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
For example, this defines a very practical NixOS module that meows at the user
|
||||||
|
on the console on boot:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
|
outputs = { self, nixpkgs, ... }: {
|
||||||
|
overlays.default = final: prev: {
|
||||||
|
meow = final.writeShellScriptBin "meow" ''
|
||||||
|
echo meow
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
nixosModules.default = { pkgs, config, lib, ... }: {
|
||||||
|
imports = [ ./nixos-module.nix ];
|
||||||
|
# inject dependencies from flake.nix, and don't do anything else
|
||||||
|
config = lib.mkIf config.services.meow.enable {
|
||||||
|
nixpkgs.overlays = [ self.overlays.default ];
|
||||||
|
services.meow.package = lib.mkDefault pkgs.meow;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
and `nixos-module.nix` containing the actual code:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{ pkgs, config, lib, ... }:
|
||||||
|
let cfg = config.services.meow; in {
|
||||||
|
options = {
|
||||||
|
services.meow = {
|
||||||
|
enable = lib.mkEnableOption "meow";
|
||||||
|
package = lib.mkOption {
|
||||||
|
description = "meow package to use";
|
||||||
|
type = lib.types.package;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
systemd.services.meow = {
|
||||||
|
description = "meow at the user on the console";
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
ExecStart = "${cfg.package}/bin/meow";
|
||||||
|
StandardOutput = "journal+console";
|
||||||
|
};
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
{% codesample(desc="How I tested the above") %}
|
||||||
|
|
||||||
|
I put this into `flake.nix`:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
nixosConfigurations.test = nixpkgs.lib.nixosSystem {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
modules = [
|
||||||
|
self.nixosModules.default
|
||||||
|
({ pkgs, lib, ... }: {
|
||||||
|
fileSystems."/" = {
|
||||||
|
device = "/dev/sda1";
|
||||||
|
};
|
||||||
|
boot.loader.grub.enable = false;
|
||||||
|
boot.initrd.enable = false;
|
||||||
|
boot.kernel.enable = false;
|
||||||
|
documentation.enable = false;
|
||||||
|
environment.noXlibs = true;
|
||||||
|
|
||||||
|
services.meow.enable = true;
|
||||||
|
|
||||||
|
system.stateVersion = "23.05";
|
||||||
|
})
|
||||||
|
];
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Then I built the system configuration:
|
||||||
|
|
||||||
|
```
|
||||||
|
» nix build .#nixosConfigurations.test.config.system.build.toplevel
|
||||||
|
```
|
||||||
|
|
||||||
|
{% end %}
|
||||||
|
|
||||||
|
# "Flakes are the future of Nix, and the only CLI"
|
||||||
|
|
||||||
|
Many words have been spilled on the new CLI and its design, mostly focusing on
|
||||||
|
flakes. However, this is not the only mode of the new CLI: wherever it makes
|
||||||
|
sense, it actually fully supports non-flake usage.
|
||||||
|
|
||||||
|
To get more exact equivalence with the old CLI, `-L` (`--print-build-logs`) and
|
||||||
|
`--print-out-path` are useful. Equally, the *old* CLI can have its output
|
||||||
|
improved to that of the new CLI by passing `--log-format bar-with-logs`. I
|
||||||
|
would be remiss not to mention [nix-output-monitor] as a much nicer way of
|
||||||
|
watching Nix builds, as well.
|
||||||
|
|
||||||
|
[nix-output-monitor]: https://github.com/maralorn/nix-output-monitor
|
||||||
|
|
||||||
|
Here is a table of the equivalences:
|
||||||
|
|
||||||
|
<div class="table-container">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td><b>Old CLI</b></td>
|
||||||
|
<td><b>Equivalent</b></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><pre>nix-build -A hello</pre></td>
|
||||||
|
<td><pre>nix build -f . hello</pre></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><pre>nix-shell -A blah</pre></td>
|
||||||
|
<td><pre>nix develop -f . blah</pre></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><pre>-</pre></td>
|
||||||
|
<td><pre>nix run -f . hello</pre></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><pre>nix-build -E<br />'(import <nixpkgs> { config.allowUnfree = true; }).blah'</pre></td>
|
||||||
|
<td><pre>nix build --impure --expr<br />'(import <nixpkgs> { config.allowUnfree = true; }).blah'</pre></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><pre>nix-instantiate --eval --strict -E 'blah'</pre></td>
|
||||||
|
<td><pre>nix eval --impure --expr 'blah'</pre></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
# "Flakes are how to manage external dependencies"
|
||||||
|
|
||||||
|
Flakes are one way of managing external dependencies but they have many flaws
|
||||||
|
in that role.
|
||||||
|
|
||||||
|
One flaw is that all the dependencies need to be listed in one file, and there
|
||||||
|
is no way of scoping them into groups.
|
||||||
|
|
||||||
|
A poorly documented limitation of both flake inputs and the built in fetchers
|
||||||
|
in general, which is the reason they are banned in nixpkgs (in addition to
|
||||||
|
[`restrict-eval`][restrict-eval] making them not work), is that they block
|
||||||
|
further evaluation while fetching. The alternative to this is to use a
|
||||||
|
fixed-output derivation that performs the fetching at build time, such as is
|
||||||
|
done with `pkgs.fetchFromGitHub`, `pkgs.fetchurl` and so on.
|
||||||
|
|
||||||
|
The blocking is not necessarily the biggest problem if the dependencies are Nix
|
||||||
|
code required to evaluate the build, but it can be troublesome when the
|
||||||
|
dependencies are not required to evaluate, since it [slows down and serializes
|
||||||
|
evaluation][ifd], downloading just one thing at a time. If the dependencies are
|
||||||
|
required for evaluation, there is little way to make this better, but for
|
||||||
|
instance, for builds requiring many inputs such as a pile of tree-sitter
|
||||||
|
grammars, Haskell package sources, or such, it adds up badly.
|
||||||
|
|
||||||
|
[ifd]: https://jade.fyi/blog/nix-evaluation-blocking/
|
||||||
|
|
||||||
|
## If not flakes then what?
|
||||||
|
|
||||||
|
There's a perfectly reasonable argument to be made for just treating
|
||||||
|
dependencies the same way as nixpkgs and directly calling `pkgs.fetchurl` and
|
||||||
|
such inside Nix source. This works fine, is conventional, and avoids the
|
||||||
|
evaluation-time-build-dependency ("import from derivation" (IFD)) problems.
|
||||||
|
|
||||||
|
It's nice to have tools to automatically update these and grab the appropriate
|
||||||
|
hash, though.
|
||||||
|
|
||||||
|
There are several tools that can maintain a lock file with Nix hashes, such as
|
||||||
|
[Niv], [npins], and [gridlock]. The first two sadly ship Nix files that use
|
||||||
|
built-in fetchers and thus have the evaluation performance issues, and the
|
||||||
|
latter doesn't ship any Nix code.
|
||||||
|
|
||||||
|
Thus, the solution is to ignore any provided Nix code for whichever one you choose
|
||||||
|
to use and write some code to read the tool's JSON file and pull the package
|
||||||
|
URL and hashes out, and call `pkgs.fetchurl` with them. This is quite easy to
|
||||||
|
do and we would recommend it.
|
||||||
|
|
||||||
|
[Niv]: https://github.com/nmattia/niv
|
||||||
|
[npins]: https://github.com/andir/npins
|
||||||
|
[gridlock]: https://github.com/lf-/gridlock
|
||||||
|
|
||||||
|
[restrict-eval]: https://nixos.org/manual/nix/stable/command-ref/conf-file.html#conf-restrict-eval
|
||||||
|
|
||||||
|
# "Flakes are good for installing software locally"
|
||||||
|
|
||||||
|
# What is all this crap for anyway?
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue