__structuredAttrs in Nix

By Robin Gloster | Mon, 20 Jan 2020

In Nix 2 a new parameter to the derivation primitive was added. It changes how information is passed to the derivation builder.

Current State

In order to show how it changes the handling of parameters to derivation, the first example will show the current state with __structuredAttrs set to false and the stdenv.mkDerivation wrapper around derivation. All parameters are passed to the builder as environment variables, canonicalised by Nix in imitation of shell script conventions:

 1$ cat <<'EOF' | nix-build - with import (builtins.fetchTarball https://github.com/nixos/nixpkgs/archive/nixos-unstable.tar.gz) {};
 2
 3stdenv.mkDerivation {
 4  name = "foo-1.2.3";
 5  dontUnpack = true;
 6
 7  EXAMPLE_BOOL_TRUE = true;
 8  EXAMPLE_BOOL_FALSE = false;
 9  EXAMPLE_INT = 123;
10  EXAMPLE_INT_NEG = -123;
11  EXAMPLE_STR = "foo bar";
12  EXAMPLE_LIST = [ "foo" "bar" ];
13  # EXAMPLE_ATTRS = { foo = "bar"; };  # currently not possible
14
15  installPhase = ''
16    env | grep "EXAMPLE" | grep -v installPhase > $out
17  '';
18}
19EOF
20
21$ cat result
22EXAMPLE_BOOL_TRUE=1
23EXAMPLE_INT_NEG=-123
24EXAMPLE_INT=123
25EXAMPLE_LIST=foo bar
26EXAMPLE_STR=foo bar
27EXAMPLE_BOOL_FALSE=

From the above, it can be seen than Nix lists are concatenated to space-seperated strings, and booleans are represnted by 1 and '', in accordance with the way bash conditionals work. (Actually bash has two conventions: 1 vs 0 or non-empty string vs empty-string; Nix tries to appease both). Attribute sets cannot be passed at all, resulting in an error:

error: cannot coerce a set to a string

This means that potentially relevant information cannot be passed faithfully. An example is quoting of flags:

1configureFlags = [ "--foo=bar baz" "--qux" ];
2configureFlags2 = [ "--foo=bar" "baz" "--qux" ];

These two parameters would evaluate to the same contents in the respective environment variables, even though the quoting information is essential for the configure script to work properly. For these cases there are workarounds in nixpkgs similar to this:

1preConfigure = ''
2  configureFlagsArray=("--foo=bar baz" "--qux")
3'';

This configureFlagsArray then gets appended to configureFlags in the stdenv bash code. This obviously is a non-ideal solution as the list cannot be passed in Nix but only in bash hooks.

stdenv with __structuredAttrs

We have created an experimental branch enabling __structuredAttrs and fixing up support for it.

 1$ cat <<'EOF' | nix-build - with import (builtins.fetchTarball https://github.com/nixos/nixpkgs/archive/structured-attrs.tar.gz) {};
 2
 3stdenv.mkDerivation {
 4  name = "foo-1.2.3";
 5  dontUnpack = true;
 6
 7  EXAMPLE_BOOL_TRUE = true;
 8  EXAMPLE_BOOL_FALSE = false;
 9  EXAMPLE_INT = 123;
10  EXAMPLE_INT_NEG = -123;
11  EXAMPLE_STR = "foo bar";
12  EXAMPLE_LIST = [ "foo" "bar" ];
13  EXAMPLE_NESTED_LIST = [ [ "foo" "bar"] [ "baz" ] ];
14  EXAMPLE_ATTRS = { foo = "bar"; };
15  EXAMPLE_NESTED_ATTRS = { foo.bar = "baz"; };
16
17  installPhase = ''
18    mkdir -p $out
19    cat .attrs.sh | grep "EXAMPLE" | grep -v installPhase > $out/sh
20    cat .attrs.json > $out/json
21  '';
22}
23EOF
24
25$ cat result/sh
26declare -A EXAMPLE_ATTRS=(['foo']='bar' )
27declare EXAMPLE_BOOL_FALSE=
28declare EXAMPLE_BOOL_TRUE=1
29declare EXAMPLE_INT=123
30declare EXAMPLE_INT_NEG=-123
31declare -a EXAMPLE_LIST=('foo' 'bar' )
32declare EXAMPLE_STR='foo bar'
33
34$ cat result/json | jq
35{
36  ...
37  "EXAMPLE_ATTRS": { "foo": "bar" },
38  "EXAMPLE_BOOL_FALSE": false,
39  "EXAMPLE_BOOL_TRUE": true,
40  "EXAMPLE_INT": 123,
41  "EXAMPLE_INT_NEG": -123,
42  "EXAMPLE_LIST": [ "foo", "bar" ],
43  "EXAMPLE_NESTED_ATTRS": { "foo": { "bar": "baz" } },
44  "EXAMPLE_NESTED_LIST": [ [ "foo", "bar" ], [ "baz" ] ],
45  "EXAMPLE_STR": "foo bar",
46  ...
47}

As can be seen from this example, two files—.attrs.sh and .attrs.json—are created if __structuredAttrs is set to true. The JSON file contains all attributes passed to derivation serialised by Nix in the same manner the builtins.toJSON built-in function works; the shell file contains all parameters that are at most one level deep, with Nix lists being converted to Bash arrays and Nix attribute sets to Bash associative arrays. All data nested deeper than one level is only included in the JSON file.

Another difference to the current state is that the variables in .attrs.sh are not exported—they are just shell variables, not environment variables. This is to be consistent with the other parameters passed to derivation, as Bash arrays and associative arrays cannot be exported. Therefore a new parameter env is added to stdenv.mkDerivation which only allows primitive types and includes logic to export these variables. This should be used in the future for env.NIX_CFLAGS_COMPILE, etc.

Enabling this feature also allows us to clean up and refactor some things in stdenv. Due to the constraints that the old method imposed, we decided to use hardeningEnable = [ ... ] and hardeningDisable = [ ... ], instead of hardening.feature = true which better represents that there is a single association between each feature and one of {true, false, default}. This would be no problem with __structuredAttrs, however. Also, the *FlagsArray parameters can be removed and replaced by the simple *Flags as they are escaped and split properly now. passAsFile is removed as the data can always be retrieved from the JSON file.

New for shell builders is that they have to source .attrs.sh mentioned above; builders in other languages are even easier, as they can simply read the structured .attrs.json and take advantage of all the nested structure rather than contorting to use the bare-bones ENV.

A few issues still need work on in Nix itself before this can be merged, though. e.g. nix-shell -A does not work yet, but generally while experimenting, the experience has been positive and seems that it will result in a cleaner and more consistent stdenv.

Do you have a question about Nix?

Just ask us! We would love to talk to you!

Check out our crew