strictly speaking, the Nix language does not have variables because what we call "variables" in the mathematical sense are names assigned to values that do not change, but can rather have different possible but fixed values depending on context. this change is mainly to make use of words consistent with the Nix language tutorial in order to avoid any ambiguity and confusion for beginners, who may wonder why one article says "there are no variables" and the other one liberally uses the term regardless. always using "name" makes unmistakably clear that it's a variable in the mathematical sense.
6.1 KiB
Best practices
URLs
The Nix language syntax supports bare URLs, so one could write https://example.com
instead of "https://example.com"
RFC 45 was accepted to deprecate unquoted URLs and provides a number of arguments how this feature does more harm than good.
:::{tip} Always quote URLs. :::
(rec-expression)=
Recursive attribute set rec { ... }
rec
allows you to reference names within the same attribute set.
Example:
:class: expression
rec {
a = 1;
b = a + 2;
}
:class: value
{ a = 1; b = 3; }
There are a couple of pitfalls:
- It's possible to introduce a hard to debug error
infinite recursion
when shadowing a name, the simplest example beinglet b = 1; a = rec { b = b; }; in a
. - Combining with overriding logic such as the
overrideAttrs
function in {term}Nixpkgs
has a surprising behaviour of not overriding every reference.
:::{tip}
Avoid rec
. Use let ... in
.
Example:
:class: expression
let
a = 1;
in {
a = a;
b = a + 2;
}
:::
with
scopes
It's still common to see the following expression in the wild:
:class: expression
with (import <nixpkgs> {});
# ... lots of code
This brings all attributes of the imported expression into scope of the current expression.
There are a number of problems with that approach:
- Static analysis can't reason about the code, because it would have to actually evaluate this file to see which names are in scope.
- When more than one
with
used, it's not clear anymore where the names are coming from. - Scoping rules for
with
are not intuitive, see this Nix issue for details.
:::{tip}
Do not use with
at the top of a Nix file.
Explicitly assign names in a let
expression.
Example:
:class: expression
let
pkgs = import <nixpkgs> {};
inherit (pkgs) curl jq;
in
# ...
:::
Smaller scopes are usually less problematic, but can still lead to surprises due to scoping rules.
:::{tip}
If you want to avoid with
altogether, try replacing expressions of this form
:class: expression
buildInputs = with pkgs; [ curl jq ];
with the following:
:class: expression
buildInputs = builtins.attrValues {
inherit (pkgs) curl jq;
};
:::
(search-path)=
<...>
search path
You will often encounter Nix language code samples that refer to <nixpkgs>
.
<...>
is special syntax that was introduced in 2011 to conveniently access values from the environment variable $NIX_PATH
.
This means, the value of a search path depends on external system state. When using search paths, the same Nix expression can produce different results.
In most cases, $NIX_PATH
is set to the latest channel when Nix is installed, and is therefore likely to differ from machine to machine.
:::{note} Channels are a mechanism for referencing remote Nix expressions and retrieving their latest version. :::
The state of a subscribed channel is external to the Nix expressions relying on it. It is not easily portable across machines. This may limit reproducibility.
For example, two developers on different machines are likely to have <nixpkgs>
point to different revisions of the nixpkgs
repository.
Builds may work for one and fail for the other, causing confusion.
:::{tip} Declare dependencies explicitly using the techniques shown in .
Do not use search paths, except in examples. :::
Some tools expect the search path to be set. In that case:
::::{tip}
Set $NIX_PATH
to a known value in a central location under version control.
:::{admonition} NixOS
On NixOS, $NIX_PATH
can be set permanently with the nix.nixPath
option.
:::
::::
Updating nested attribute sets
The attribute set update operator merges two attribute sets.
Example:
:class: expression
{ a = 1; b = 2; } // { b = 3; c = 4; }
:class: value
{ a = 1; b = 3; c = 4; }
However, names on the right take precedence, and updates are shallow.
Example:
:class: expression
{ a = { b = 1; }; } // { a = { c = 3; }; }
:class: value
{ a = { c = 3; }; }
Here, key b
was completely removed, because the whole a
value was replaced.
:::{tip}
Use the pkgs.lib.recursiveUpdate
Nixpkgs function:
:class: expression
let pkgs = import <nixpkgs> {}; in
pkgs.recursiveUpdate { a = { b = 1; }; } { a = { c = 3;}; }
:class: value
{ a = { b = 1; c = 3; }; }
:::
Reproducible source paths
:class: expression
let pkgs = import <nixpkgs> {}; in
pkgs.stdenv.mkDerivation {
name = "foo";
src = ./.;
}
If the Nix file containing this expression is in /home/myuser/myproject
, then the store path of src
will be /nix/store/<hash>-myproject
.
The problem is that now your build is no longer reproducible, as it depends on the parent directory name. That cannot declared in the source code, and results in an impurity.
If someone builds the project in a directory with a different name, they will get a different for src
and everything that depends on it.
:::{tip}
Use builtins.path
with the name
attribute set to something fixed.
This will derive the symbolic name of the store path from name
instead of the working directory:
:class: expression
let pkgs = import <nixpkgs> {}; in
pkgs.stdenv.mkDerivation {
name = "foo";
src = builtins.path { path = ./.; name = "myproject"; };
}
:::