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