public/nix.md
2024-08-18 03:44:55 -07:00

6.1 KiB

::: header

nix

::: ::: section

nix, the language

Nix is a language. It has numbers. You can add them.

1 + 2
= 3

The above expression evaluates to 3. If you have nix installed, try running nix repl and pasting that in. Nix also has strings.

"hello nix!"
= "hello nix!"
"escapes\nand\nnewlines" == "escapes
and
newlines"
= true
''alternative syntax''
= "alternative syntax"

Nix has booleans.

!(true && false) || false
= true

Nix has sets, or key value pairs. These are known as records, maps, or objects in other lanugages.

{ foo = 123; }.foo
= 123

Sets can be nested.

# There isn't any hard rule on which style to use
{ foo = { bar = "baz"; }; } == { foo.bar = "baz";  }
= true

The let and in keyword defines a variable for a given scope.

let
  foo = 3;
  bar = foo * 3;
in
  bar
= 9

Nix has pure functions that transform inputs to outputs without side effects.

let
  incr = n: n + 1;
in
  incr 5
= 6

To use multiple parameters, either pass a set or use currying (create a function that returns another function).

let
  addSet = input: input.a + input.b;
  addDestructure = { a, b }: a + b;
  addCurry = a: b: a + b;
in {
  foo = addSet { a = 10; b = 20; };
  bar = addDestructure { a = 10; b = 20; };
  baz = addCurry 30 40;
}
= { bar = 30; baz = 70; foo = 30; }

Destructuring optional fields.

let
  # this will use b = 5 if b is not defined
  addOptional = { a, b ? 5 }: a + b;
in {
  foo = addOptional { a = 1; };
  bar = addOptional { a = 1; b = 1; };
}
= { bar = 2; foo = 6; }

The inherit keyword.

let
  key1 = "something here";
  key2 = "something else";
in {
  inherit key1 key2;
}
= { key1 = "something here"; key2 = "something else"; }

Nix provides many builtin functions in the builtins set. If you're using the repl, press tab to get a list of them.

{
  system = builtins.currentSystem;
}
= { system = "x86_64-linux"; }

Finally, nix has a path type.

{
  sourceCode = ./path/to/sources;
}
= { sourceCode = /absolute/path/to/sources; }
:::

::: section

nix, the build tool

Nix provides a builtin function to create derivations. These are special descriptions of how to derive build outputs from source code. Build outputs can be anything, such as binaries, shared libraries, or a docker tarball.

derivation {
  name = "foobar";
  # here is where you'd
  # src = ./path/to/source
  
  # the $out variable has the location of where the build outputs should go
  # the build outputs can be a file or a folder of any structure
  # the $src variable has the location of the sources
  builder = "/bin/sh";
  args = ["-c" "echo 'Hello, derivation!' > $out"];
  
  system = builtins.currentSystem;
}
= derivation «/nix/store/119h84n7a58069l5zi0rgs7q06rhrlh3-foobar.drv»

This code outputs a hash-based path to a derivation, nix's version of a build script. Derivations derive build outputs from build inputs. To build the derivation, use :b (copy-pasted code) in the repl. It should print a path to the build outputs. If you cat the output, you should get back "Hello, derivation!".

NixOS/nixpkgs is a combination package repository and standard library, containing thousands of derivations and functions. Using the derivation function is almost always unnecessary, since nixpkgs probably has a helper for whatever language or build system you want to use. Even if you need to manually make a derivation, there is mkDerivation which is a bit higher level than derivation.

Nix flakes are how derivations and packages are managed, and is what you probably want to be using.

You've probably heard it before, but it's worth repeating: although more difficult to set up, using nix over other build systems gives you reproducability. This means that the same nix expression and source code will always produce the same outputs. If it works on one nix user's machine, it will work every nix user's machine. If a build fails, it fails for everyone.

Nix flakes are a new tool that makes the inputs explicit as well, instead of only the build steps. Anything that isn't explicitly defined as an input, whether it's binaries, paths, or environment variables, is not given to the derivation's builder. Here's an example flake:

{
  description = "any useful description here";

  inputs = {
    # Inputs take in flake references
    # https://nix.dev/manual/nix/2.18/command-ref/new-cli/nix3-flake#flake-references
    nixpkgs.url = "github:NixOS/nixpkgs";
  };

  outputs = { self, nixpkgs }: {
    # reuse an existing derivation for simplicity
    packages.x86_64-linux.default = nixpkgs.legacyPackages.x86_64-linux.hello;
  };
}

This code should be put inside flake.nix at the root of a git repository. Running nix flake new hello-world would create a directory called hello-world pre-initialized with a flake like this. From there, run nix build to build (the binary will be in result/bin/hello), or nix run to build and run the binary.

Maybe you've noticed already, but the flake isn't really building anything new - it's exporting an already made derivation from nixpkgs. Here is another flake:

{
  description = "any useful description here";

  inputs.nixpkgs.url = "github:NixOS/nixpkgs";
  
  outputs = { self, nixpkgs }: let
    pkgs = import nixpkgs { system = "x86_64-linux"; };
  in {
    packages.x86_64-linux.default = pkgs.stdenv.mkDerivation {
      name = "hello";
      src = self;
      buildPhase = ''
        echo -e '#!/bin/sh\necho hello world' > bin
        chmod +x bin
      '';
      installPhase = ''
        mkdir -p $out/bin
        mv bin $out/bin/hello
      '';
    };
  };
}

:::