Posted on April 19, 2021 by Nick Sigulya

What Is Nix and How to Use It?

Our take on an introduction to Nix: the language and the package manager

Part 2

Here at Typeable, we wanted to publish a small series of posts about the way Nix helps us (and slightly hinders) in software development. We would like to start with an introduction to Nix which we might refer to further on.

You can find the files for this post here.

Where to get it?

Apart from NixOS, where you don’t need to do anything, Nix can be installed on any (or almost any) Linux distribution. To this end, you just have to run the following command:

$ sh <(curl -L https://nixos.org/nix/install)

After that, the installation script will do everything on its own. The recent changes in MacOS have made the installation more difficult. Before the changes, the above-mentioned command was sufficient. You can read about the installation on the latest MacOS versions here.

The Nix language

When you speak about Nix, you often imply two different entities: Nix as a language and nixpkgs as the package repository also constituting the basis of NixOS. Let’s start with the first one.

Nix is a lazy functional language with dynamic typing. The syntax looks much like the languages of the ML family (SML, OCaml, Haskell), which is why those who know them are not likely to face any issues.

You can start getting familiar with the language simply by running the interpreter.

$ nix repl
Welcome to Nix version 2.3.10. Type :? for help.

nix-repl>

There is no special syntax used to declare the functions in Nix. The functions are defined by assigning, similarly to other values.

nix-repl> "Hello " + "World!"
"Hello World!"

nix-repl> add = a: b: a + b

nix-repl> add 1 2
3

All functions are curried, in the same way as in the languages which have influenced Nix.

nix-repl> addOne = add 1

nix-repl> addOne 3
4

In addition to the primitive types such as numbers and lines, Nix supports the lists and dictionaries (attribute sets in the Nix terminology).

nix-repl> list = [ 1 2 3 ]

nix-repl> set = { a = 1; b = list; }

nix-repl> set
{ a = 1; b = [ ... ]; }

nix-repl> set.b
[ 1 2 3 ]

The values within the local scope can be set using the expression let...in. For example, here is a simple function implementing a factorial, as it is usually done in other posts on functional programming.

fac.nix:

let
  fac = n:
    if n == 0
    then 1
    else n * fac (n - 1);
in { inherit fac; }

Directive inherit introduces or “inherits” the term from the current scope and gives it the same name. The example above is equivalent to the record let fac = ... in { fac = fac; }.

$ nix repl fac.nix
Welcome to Nix version 2.3.10. Type :? for help.

Loading 'fac.nix'...
Added 1 variables.

nix-repl> fac 3
6

When files or modules are uploaded to REPL, Nix expects that the module computation will result in a set whose elements will be imported in the current scope.

To download the code from other files, Nix uses the function import accepting the path to the code file and returning the result of this code.

mul.nix:

let
  mul = a: b: a * b;
in { inherit mul; }

New fac.nix:

let
  multMod = import ./mul.nix;
  fac = n:
    if n == 0
    then 1
    else multMod.mul n (fac (n - 1));
in { inherit fac; }

Though assigning the module to an individual variable is done rather often, it looks somewhat awkward here, doesn’t it? Nix includes the with directive adding all names from the set passed as the parameter to the current scope.

fac.nix using with:

with import ./mul.nix;
let
  fac = n:
    if n == 0
    then 1
    else mul n (fac (n - 1));
in { inherit fac; }

Building programs

Building programs and individual components is the main function of the Nix language.

When working with packages, the main tool you should know about is Derivation. In itself, Derivation is a special file containing the recipe for a machine-readable build. The derivation compiling a program in C that displays “Hello World!” looks approximately as follows:

Derive([("out","/nix/store/1nq46fyv3629slgxnagqn2c01skp7xrq-hello-world","","")],[("/nix/store/60xqp516mkfhf31n6ycyvxppcknb2dwr-build-hello.drv",["out"])],["/nix/store/wiviq2xyz0ylhl0qcgfgl9221nkvvxfj-hello.c"],"x86_64-linux","/nix/store/r5lh8zg768swlm9hxxfrf9j8gwyadi72-build-hello",[],[("builder","/nix/store/r5lh8zg768swlm9hxxfrf9j8gwyadi72-build-hello"),("name","hello-world"),("out","/nix/store/1nq46fyv3629slgxnagqn2c01skp7xrq-hello-world"),("src","/nix/store/wiviq2xyz0ylhl0qcgfgl9221nkvvxfj-hello.c"),("system","x86_64-linux")])

As you can see, this expression includes the path to the resulting build and the paths to the source files, build script, and metadata: the project name and platform. It should also be noted that the paths to the source code start with /nix/store. During the build, Nix copies everything it needs to this directory. After that, the build is carried out in an isolated environment (sandbox). Thus, the reproducibility of all package builds is achieved.

Surely, it’s insanity to write this manually! For simple cases, Nix offers the built-in derivation function accepting the build description.

simple-derivation/default.nix:

{ pkgs ? import <nixpkgs> {} }:

derivation {
  name = "hello-world";
  builder = pkgs.writeShellScript "build-hello" ''
    ${pkgs.coreutils}/bin/mkdir -p $out/bin
    ${pkgs.gcc}/bin/gcc $src -o $out/bin/hello -O2
  '';
  src = ./hello.c;
  system = builtins.currentSystem;
}

Let’s analyze this example. The entire file is the definition of the function accepting one parameter – the dictionary containing the pkgs field. If it was not passed during the function call, the default value will be used: import <nixpkgs> {}.

derivation is the function also accepting the dictionary with the build parameters: The name is the package name, the builder is the build script, the src is the source code, the system is the system or list of systems the package can be built for.

The writeShellScript is one of the nixpkgs functions accepting the script name and code and returning the executable file path. For multiline text, Nix offers an alternative syntax with two pairs of single quotes.

Using the nix build command you can run this build recipe and obtain a working binary file.

$ nix build -f ./simple-derivation/default.nix
[1 built]

$ ./result/bin/hello
Hello World!

When you run nix build, the symbolic link result referring to the package created in the /nix/store will be generated in the current directory.

$ ls -l result
lrwxrwxrwx 1 user users 50 Mar 29 17:53 result -> /nix/store/vpcddray35g2jrv40dg1809xrmz73awi-simple

$ find /nix/store/vpcddray35g2jrv40dg1809xrmz73awi-simple
/nix/store/vpcddray35g2jrv40dg1809xrmz73awi-simple
/nix/store/vpcddray35g2jrv40dg1809xrmz73awi-simple/bin
/nix/store/vpcddray35g2jrv40dg1809xrmz73awi-simple/bin/hello

Building programs, advanced version

derivation is the fairly low-level function Nix uses as the basis for far more powerful primitives. As an example, we can consider the build of the well-known cowsay utility.

{ lib, stdenv, fetchurl, perl }:

stdenv.mkDerivation rec {
  version = "3.03+dfsg2";
  pname = "cowsay";

  src = fetchurl {
    url = "http://http.debian.net/debian/pool/main/c/cowsay/cowsay_${version}.orig.tar.gz";
    sha256 = "0ghqnkp8njc3wyqx4mlg0qv0v0pc996x2nbyhqhz66bbgmf9d29v";
  };

  buildInputs = [ perl ];

  postBuild = ''
    substituteInPlace cowsay --replace "%BANGPERL%" "!${perl}/bin/perl" \
      --replace "%PREFIX%" "$out"
  '';

  installPhase = ''
    mkdir -p $out/{bin,man/man1,share/cows}
    install -m755 cowsay $out/bin/cowsay
    ln -s cowsay $out/bin/cowthink
    install -m644 cowsay.1 $out/man/man1/cowsay.1
    ln -s cowsay.1 $out/man/man1/cowthink.1
    install -m644 cows/* -t $out/share/cows/
  '';

  meta = with lib; {
    description = "A program which generates ASCII pictures of a cow with a message";
    homepage = "https://en.wikipedia.org/wiki/Cowsay";
    license = licenses.gpl1;
    platforms = platforms.all;
    maintainers = [ maintainers.rob ];
  };
}

The original script can be found here.

stdenv is a special derivation containing the build rules for the current system: the required compiler, flags, and other parameters. Its main content is the huge Bash script named setup working as the builder script in our simple example shown above.

 $ nix build nixpkgs.stdenv

 $ find result/
result/
result/setup
result/nix-support

$ wc -l result/setup
1330 result/setup

mkDerivation is the function creating the derivation with this script and simultaneously filling out other fields.

Those readers who used to write package build scripts in Arch Linux or Gentoo might see a pretty familiar structure here. Just as in other distributions, the build is broken down into phases, dependencies enumeration is available (buildInputs), and so on.

Conclusion

In this part, I’ve tried to describe the most basic aspects of using Nix as the build description language. In the next posts, I’m going to show you the ways we use Nix at Typeable and the ways you’d better not use it. Stay tuned!

Besides, a far more detailed introduction to Nix is published on the website of the project itself under the name of Nix pills.

Recommended

You may also like

Want to know more?
Get in touch with us!
Contact Us

Privacy policy

Last updated: 1 September 2021

Typeable OU ("us", "we", or "our") operates https://typeable.io (the "Site"). This page informs you of our policies regarding the collection, use and disclosure of Personal Information we receive from users of the Site.

We use your Personal Information only for providing and improving the Site. By using the Site, you agree to the collection and use of information in accordance with this policy.

Information Collection And Use

While using our Site, we may ask you to provide us with certain personally identifiable information that can be used to contact or identify you. Personally identifiable information may include, but is not limited to your name ("Personal Information").

Log Data

Like many site operators, we collect information that your browser sends whenever you visit our Site ("Log Data").

This Log Data may include information such as your computer's Internet Protocol ("IP") address, browser type, browser version, the pages of our Site that you visit, the time and date of your visit, the time spent on those pages and other statistics.

In addition, we may use third party services such as Google Analytics that collect, monitor and analyze this ...

Cookies

Cookies are files with small amount of data, which may include an anonymous unique identifier. Cookies are sent to your browser from a web site and stored on your computer's hard drive.

Like many sites, we use "cookies" to collect information. You can instruct your browser to refuse all cookies or to indicate when a cookie is being sent. However, if you do not accept cookies, you may not be able to use some portions of our Site.

Security

The security of your Personal Information is important to us, so we don't store any personal information and use third-party GDPR-compliant services to store contact data supplied with a "Contact Us" form and job applications data, suplied via "Careers" page.

Changes To This Privacy Policy

This Privacy Policy is effective as of @@privacePolicyDate​ and will remain in effect except with respect to any changes in its provisions in the future, which will be in effect immediately after being posted on this page.

We reserve the right to update or change our Privacy Policy at any time and you should check this Privacy Policy periodically. Your continued use of the Service after we post any modifications to the Privacy Policy on this page will constitute your acknowledgment of the modifications and your consent to abide and be bound by the modified Privacy Policy.

If we make any material changes to this Privacy Policy, we will notify you either through the email address you have provided us, or by placing a prominent notice on our website.

Contact Us

If you have any questions about this Privacy Policy, please contact us.