In the last section I discussed creating your first derivation, which
allows you to make a first nix package. As you might have noticed, the
default execution environment is incredibly barebones, to the point that
you needed to include such fundamental tools as chmod and cp. If that
process had to be repeated by every Nix user, it would be very inconvenient.
Luckily, there is nixpkgs, which provides a number of packages that the
community has already built, along with the standard environment which
includes a number of tools to use in building your own. You may remember
installing some of these packages in part 3.
mkDerivation
The first item from the standard environment I'm going to discuss is
stdenv.mkDerivation. This is a function that is an enhanced version
of the built in derivation. It provides a number of advantages over
using derivation directly:
- It does the bootstrapping for you ensuring you have a decent minimal
environment to build on. The full list of packages
can be seen in your current nixpkgs manual, but it includes things like
GNU coreutils, bash, tar, gzip, etc. The bash shell is used as the default
interpreter for shell scripts, compared to
derivationwhich only promises a bourne compatible shell. - It provides an easy way to add extra dependencies to your specific
derivation and includes them in the path so you don't have to use the
$cp,$chmodvariables in the scripts like was required last time. - It provides a default builder which runs in bash and does a
./configure && makeinstallation, with some variables that lets you override parts of it without having to write a whole new script.
Here's a modified version of the "hello world" derivation from the last part.
derivation-stdenv.nix
let pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/0b20bf89e0035b6d62ad58f9db8fdbc99c2b01e8.tar.gz") {};
in pkgs.stdenv.mkDerivation {
src = ./hello.sh;
name = "hello-1.0";
system = "x86_64-linux";
dontUnpack = true;
installPhase = ''
cp $src $out
chmod +x $out
'';
}
Here fetchTarball is a function from nix's builtins that
downloads a tarball from the internet and stores it in the nix store. The
code then calls import on the returned result. import evaluates the
downloaded value as nix code and then assign the result object to the pkgs
name. The variable name pkgs is customary for nixpkgs, so you may see it used
in docs without explaining where it comes from.
In the call to mkDerivation, there are the following differences to the builtin
derivation example from last time:
- There's no
builderprovided - in this example the standard builder provided by thestdenvis used, with one phase that is overridden from the.nixfile. - The
installPhaseoption is what overrides this one phase. The script is included inline using Nix's multi-line strings, which are signified with the doubled up single quotes. - The
dontUnpackattribute also tells the builder not to try unpack the source. Since most software sources have more than one file, the standard builder defaults to treating the src as an archive and trying to unpack it. Here thesrcis just a shell script, so that unpacking is not required. - Inside the standard environment, items like
cpandchmodare just available on the path, so they don't need to be explicitly passed - the script just uses them as in regular command line use.
Dependencies
Now that there's no longer a need to rebuild the world inside each derivation, it's time to start a more challenging package, one that needs some dependencies. Let's take an example of a Rust version of the hello world program from before. Even if you've never written Rust before, and don't have any Rust toolchain you can use Nix to get everything needed.
First get Cargo, Rust's build tool/package manager from Nix and have it scaffold a Rust project for this example.
You will see a download progress bar as it downloads cargo from the nixpkgs
cache, then it runs that cargo command with the rest of the arguments,
as if you had run cargo init rust-hello with a locally installed version.
Unlike nix profile install, the downloaded version of cargo isn't kept
around permanently - it will be deleted at the next GC.
Cargo will then generate two files. Conveniently, when you generate a new rust project, it prefills in a hello world program to get you started.
rust-hello/Cargo.toml
[]
= "rust-hello"
= "0.1.0"
= "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[]
rust-hello/src/main.rs
Now it's time to write a derivation to produce a build output from this.
The first new feature needed is a new argument to stdenv.mkDerivation. This
argument is nativeBuildInputs. This is used to download dependencies that need
to run on the system building the package, and produce output suitable for
the system intended to run the package.
nativeBuildInputs = [pkgs.cargo]
The second new feature used is that the script now overrides a second phase
of the standard builder, the buildPhase. As the name suggests, this is where
you should run commands to build your software. In this case cargo build --release
is the command to build an optimised version of a rust program. Since
pkgs.cargo was added to the nativeBuildInputs section, cargo is available
on the PATH of the build script.
buildPhase = ''
cargo build --release
'';
Finally, this time the script places the output in $out/bin rather than copying it
direct to $out. This is because stdenv puts the $out/bin directory onto the path
of anything declaring this package as a dependency, so by placing the binary here it will
be on the path of anything that uses this as a dependency.
installPhase = ''
mkdir -p $out/bin
cp target/release/rust-hello $out/bin/rust-hello
chmod +x $out
''
Putting all of these together, the final derivation is below
rust-derivation.nix
let pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/0b20bf89e0035b6d62ad58f9db8fdbc99c2b01e8.tar.gz") {};
in pkgs.stdenv.mkDerivation {
src = ./rust-hello;
name = "rust-hello-1.0";
system = "x86_64-linux";
nativeBuildInputs = [ pkgs.cargo ];
buildPhase = ''
cargo build --release
'';
installPhase = ''
mkdir -p $out/bin
cp target/release/rust-hello $out/bin/rust-hello
chmod +x $out
'';
}
Other builders
stdenv.mkDerivation is the foundational tool for working with derivations
in Nixpkgs, but there's also a library of other builders for common languages
and use cases. For example, rather than build the derivation for the first shell
example manually, the nix expression could have used the
pkgs.buildShellApplication builder.
In this case the updated shell derivation would be as follows:
shell-app.nix
let pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/0b20bf89e0035b6d62ad58f9db8fdbc99c2b01e8.tar.gz") {};
in pkgs.writeShellApplication {
name = "hello";
text = ''
echo Hello World
'';
}
You can also see the Nixpkgs manual on languages and frameworks to see if there's any builders or utilities for your preferred programming language.
Next time, I will cover flakes, one of the biggest changes to Nix packaging ever, and the foundation of a lot of the newer methods of interacting with Nix.