1
0
Fork 0
mirror of https://github.com/NixOS/nix.dev.git synced 2024-10-18 14:32:43 -04:00
nix.dev/source/tutorials/packaging-existing-software.md
fricklerhandwerk 8afee98203 dissolve the Learning Journey section
This does not yet follow #701, because the change to get there would be
too big. The goal is to keep the table of contents meaningful at all
times and the diff of each pull request manageable.

The packaging tutorial is quite important, so it's not wrong to have it
visible on the front page until we have enough material for a packaging
section.

The sharing dependencies article should really be a very brief guide, but
because that would be quite a big change, it's only moved to the Guides
section for now.
2023-11-02 00:13:32 +01:00

20 KiB

myst
html_meta
description lang=en keywords
Packaging Existing Software With Nix Nix, packaging

(packaging-existing-software)=

Packaging Existing Software With Nix

One of Nix's primary use-cases is in addressing common difficulties encountered while packaging software, like managing dependencies.

In the long term, Nix helps tremendously in alleviating that stress, but when first packaging existing software with Nix, it's common to encounter missing dependencies preventing builds from succeeding.

In this tutorial, you'll create your first Nix derivations to package C/C++ software, taking advantage of the Nixpkgs Standard Environment (stdenv) which automates much of the work of building self-contained C/C++ packages.

The tutorial begins by considering hello, an implementation of "hello world" which only requires dependencies provided by stdenv.

Next, you will build more complex packages with their own dependencies, leading you to use additional derivation features.

You'll encounter and address Nix error messages, build failures, and a host of other issues, developing your iterative debugging techniques along the way.

:::{note} A package is an informally defined Nixpkgs concept referring to a Nix derivation representing an installation of some project. Packages have mostly standardised attributes and output layouts, allowing them to be discovered in searches and installed into environments alongside other packages.

For the purposes of this tutorial, "package" means something like "result of a derivation"; this is the artifact you or others will use, as a consequence of having "packaged existing software with Nix". :::

A Simple Project

To start, consider this skeleton derivation:

{ stdenv }:

stdenv.mkDerivation {	};

This is a function which takes an attribute set containing stdenv, and produces a derivation (which currently does nothing).

As you progress through this tutorial, you will update this several times, adding more details while following the general pattern.

Hello, World!

GNU Hello is an implementation of the "hello world" program, with source code accessible from the GNU Project's FTP server.

To begin, you should add a name attribute to the set passed to mkDerivation; every derivation needs a name, and Nix will throw error: derivation name missing without one.

...
stdenv.mkDerivation {
+ name = "hello";
...

Next, you will download the latest version of hello using fetchzip, which takes the URI path to the download file and a SHA256 hash of its contents.

:::{note} fetchzip can fetch more archives than just zip files! :::

The hash cannot be known until after the tarball has been downloaded and unpacked, but Nix will complain if the hash supplied to fetchzip was incorrect, so it is common practice to supply a fake one with lib.fakeSha256 and change the derivation definition after Nix reports the correct hash:

# hello.nix
{ lib
, stdenv
, fetchzip
}:

stdenv.mkDerivation {
  name = "hello";

  src = fetchzip {
    url = "https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz";
    sha256 = lib.fakeSha256;
  };
}

Save this file to hello.nix and try to build it with nix-build, observing your first build failure:

$ nix-build hello.nix
error: cannot evaluate a function that has an argument without a value ('lib')
       Nix attempted to evaluate a function as a top level expression; in
       this case it must have its arguments supplied either by default
       values, or passed explicitly with '--arg' or '--argstr'. See
       https://nixos.org/manual/nix/stable/language/constructs.html#functions.

       at /home/nix-user/hello.nix:2:3:

            1| # hello.nix
            2| { lib
             |   ^
            3| , stdenv

Problem: the expression in hello.nix is a function, which only produces its intended output if it is passed the correct arguments.

A New Command

lib is available from nixpkgs, which must be imported with another Nix expression in order to pass it as an argument to this derivation.

The recommended way to do this is to create a default.nix in the same directory as hello.nix, with the following contents:

# default.nix
let
  pkgs = import <nixpkgs> { };
in
{
  hello = pkgs.callPackage ./hello.nix { };
}

This allows you to use nix-build -A hello to realize the derivation in hello.nix, similar to the current convention used in nixpkgs.

:::{note} callPackage automatically passes attributes from pkgs to the given function, if they match attributes required by that function's argument attrset.

In this case, callPackage will supply lib, and stdenv to the function defined in hello.nix. :::

Now run the nix-build command with the new argument:

$ nix-build -A hello
error:
...
       … while evaluating attribute 'src' of derivation 'hello'

         at /home/nix-user/hello.nix:9:3:

            8|
            9|   src = fetchzip {
             |   ^
           10|     url = "https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz";

       error: hash mismatch in file downloaded from 'https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz':
         specified: sha256:0000000000000000000000000000000000000000000000000000
         got:       sha256:0xw6cr5jgi1ir13q6apvrivwmmpr5j8vbymp0x6ll0kcv6366hnn

Finding The File Hash

As expected, the incorrect file hash caused an error, and Nix helpfully provided the correct one, which you can now substitute into hello.nix to replace lib.fakeSha256:

# hello.nix
{ lib
, stdenv
, fetchzip
}:

stdenv.mkDerivation {
  name = "hello";

  src = fetchzip {
    url = "https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz";
    sha256 = "0xw6cr5jgi1ir13q6apvrivwmmpr5j8vbymp0x6ll0kcv6366hnn";
  };
}

Now run the previous command again:

$ nix-build -A hello
this derivation will be built:
  /nix/store/rbq37s3r76rr77c7d8x8px7z04kw2mk7-hello.drv
building '/nix/store/rbq37s3r76rr77c7d8x8px7z04kw2mk7-hello.drv'...
...
configuring
...
configure: creating ./config.status
config.status: creating Makefile
...
building
... <many more lines omitted>

Great news: the derivation built successfully!

The console output shows that configure was called, which produced a Makefile that was then used to build the project. It wasn't necessary to write any build instructions in this case because the stdenv build system is based on autoconf, which automatically detected the structure of the project directory.

Build Result

Check your working directory for the result:

$ ls
default.nix hello.nix  result

This result is a symbolic link to a Nix store location containing the built binary; you can call ./result/bin/hello to execute this program:

$ ./result/bin/hello
Hello, world!

Congratulations, you have successfully packaged your first program with Nix!

Next, you'll package another piece of software with external-to-stdenv dependencies that present new challenges, requiring you to make use of more mkDerivation features.

Something Bigger

Now you will package a somewhat more complicated program, icat, which allows you to render images in your terminal.

To start, modify the default.nix from the previous section by adding a new attribute for icat:

# default.nix
let
  pkgs = import <nixpkgs> { };
in
{
  hello = pkgs.callPackage ./hello.nix { };
  icat = pkgs.callPackage ./icat.nix { };
}

Now copy hello.nix to a new file, icat.nix, and update the name attribute in that file:

# icat.nix
{ lib
, stdenv
, fetchzip
}:

stdenv.mkDerivation {
  name = "icat";

  src = fetchzip {
    ...
  };
}

Now to download the source code. icat's upstream repository is hosted on GitHub, so you should modify the previous source fetcher, this time using fetchFromGitHub instead of fetchzip, updating the argument attribute set to the function accordingly:

# icat.nix
{ lib
, stdenv
, fetchFromGitHub
}:

stdenv.mkDerivation {
  name = "icat";

  src = fetchFromGitHub {
    ...
  };
}

Fetching Source from GitHub

While fetchzip required url and sha256 arguments, more are needed for fetchFromGitHub.

The source is hosted on GitHub at https://github.com/atextor/icat, which already gives the first two arguments:

  • owner: the name of the account controlling the repository; owner = "atextor";
  • repo: the name of the repository to fetch; repo = "icat";

You can navigate to the project's Releases page to find a suitable rev, such as the git commit hash or tag (e.g. v1.0) corresponding to the release you want to fetch.

In this case, the latest release tag is v0.5.

As in the hello example, a hash must also be supplied.

This time, instead of using lib.fakeSha256 and letting nix-build report the correct one in an error, you can fetch the correct hash in the first place with the nix-prefetch-url command.

You need the SHA256 hash of the contents of the tarball (as opposed to the hash of the tarball file itself), so you will need to pass the --unpack and --type sha256 arguments too:

$ nix-prefetch-url --unpack https://github.com/atextor/icat/archive/refs/tags/v0.5.tar.gz --type sha256
path is '/nix/store/p8jl1jlqxcsc7ryiazbpm7c1mqb6848b-v0.5.tar.gz'
0wyy2ksxp95vnh71ybj1bbmqd5ggp13x3mk37pzr99ljs9awy8ka

Now you can supply the correct hash to fetchFromGitHub:

# icat.nix
{ lib
, stdenv
, fetchFromGitHub
}:

stdenv.mkDerivation {
  name = "icat";

  src = fetchFromGitHub {
    owner = "atextor";
    repo = "icat";
    rev = "v0.5";
    sha256 = "0wyy2ksxp95vnh71ybj1bbmqd5ggp13x3mk37pzr99ljs9awy8ka";
  };
}

Missing Dependencies

Running nix-build with the new icat attribute, an entirely new issue is reported:

$ nix-build -A icat
these 2 derivations will be built:
  /nix/store/86q9x927hsyyzfr4lcqirmsbimysi6mb-source.drv
  /nix/store/l5wz9inkvkf0qhl8kpl39vpg2xfm2qpy-icat.drv
...
error: builder for '/nix/store/l5wz9inkvkf0qhl8kpl39vpg2xfm2qpy-icat.drv' failed with exit code 2;
       last 10 log lines:
       >                  from /nix/store/hkj250rjsvxcbr31fr1v81cv88cdfp4l-glibc-2.37-8-dev/include/stdio.h:27,
       >                  from icat.c:31:
       > /nix/store/hkj250rjsvxcbr31fr1v81cv88cdfp4l-glibc-2.37-8-dev/include/features.h:195:3: warning: #warning "_BSD_SOURCE and _SVID_SOURCE are deprecated, use _DEFAULT_SOURCE" [8;;https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html#index-Wcpp-Wcpp8;;]
       >   195 | # warning "_BSD_SOURCE and _SVID_SOURCE are deprecated, use _DEFAULT_SOURCE"
       >       |   ^~~~~~~
       > icat.c:39:10: fatal error: Imlib2.h: No such file or directory
       >    39 | #include <Imlib2.h>
       >       |          ^~~~~~~~~~
       > compilation terminated.
       > make: *** [Makefile:16: icat.o] Error 1
       For full logs, run 'nix log /nix/store/l5wz9inkvkf0qhl8kpl39vpg2xfm2qpy-icat.drv'.

A compiler error! The icat source was pulled from GitHub, and Nix tried to build what it found, but compilation failed due to a missing dependency: the imlib2 header.

If you search for imlib2 on search.nixos.org, you'll find that imlib2 is already in nixpkgs.

You can add this package to your build environment by adding imlib2 to the set of inputs to the expression in icat.nix, and then adding imlib2 to the list of buildInputs in stdenv.mkDerivation:

# icat.nix
{ lib
, stdenv
, fetchFromGitHub
, imlib2
}:

stdenv.mkDerivation {
  name = "icat";

  src = fetchFromGitHub {
    owner = "atextor";
    repo = "icat";
    rev = "v0.5";
    sha256 = "0wyy2ksxp95vnh71ybj1bbmqd5ggp13x3mk37pzr99ljs9awy8ka";
  };

  buildInputs = [ imlib2 ];
}

Run nix-build -A icat again and you'll encounter another error, but compilation proceeds further this time:

$ nix-build -A icat
this derivation will be built:
  /nix/store/bw2d4rp2k1l5rg49hds199ma2mz36x47-icat.drv
...
error: builder for '/nix/store/bw2d4rp2k1l5rg49hds199ma2mz36x47-icat.drv' failed with exit code 2;
       last 10 log lines:
       >                  from icat.c:31:
       > /nix/store/hkj250rjsvxcbr31fr1v81cv88cdfp4l-glibc-2.37-8-dev/include/features.h:195:3: warning: #warning "_BSD_SOURCE and _SVID_SOURCE are deprecated, use _DEFAULT_SOURCE" [8;;https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html#index-Wcpp-Wcpp8;;]
       >   195 | # warning "_BSD_SOURCE and _SVID_SOURCE are deprecated, use _DEFAULT_SOURCE"
       >       |   ^~~~~~~
       > In file included from icat.c:39:
       > /nix/store/4fvrh0sjc8sbkbqda7dfsh7q0gxmnh9p-imlib2-1.11.1-dev/include/Imlib2.h:45:10: fatal error: X11/Xlib.h: No such file or directory
       >    45 | #include <X11/Xlib.h>
       >       |          ^~~~~~~~~~~~
       > compilation terminated.
       > make: *** [Makefile:16: icat.o] Error 1
       For full logs, run 'nix log /nix/store/bw2d4rp2k1l5rg49hds199ma2mz36x47-icat.drv'.

You can see a few warnings which should be corrected in the upstream code, but the important bit for this tutorial is fatal error: X11/Xlib.h: No such file or directory: another dependency is missing.

:::{note} Determining from where to source a dependency is currently a somewhat-involved process: it helps to become familiar with searching the nixpkgs source for keywords.

Consider using nix-locate from the nix-index tool to find derivations that provide what you need. :::

You will need the Xlib.h headers from the X11 C package, the Nixpkgs derivation for which is libX11, available in the xorg package set.

Add this to your derivation's input attribute set and to buildInputs:

# icat.nix
{ lib
, stdenv
, fetchFromGitHub
, imlib2
, xorg
}:

stdenv.mkDerivation {
  name = "icat";

  src = fetchFromGitHub {
    owner = "atextor";
    repo = "icat";
    rev = "v0.5";
    sha256 = "0wyy2ksxp95vnh71ybj1bbmqd5ggp13x3mk37pzr99ljs9awy8ka";
  };

  buildInputs = [ imlib2 xorg.libX11 ];
}

:::{note} Only add the top-level xorg derivation to the input attrset, rather than the full xorg.libX11, as the latter would cause a syntax error.

Because Nix is lazily-evaluated, using xorg.libX11 means that we only include the libX11 attribute and the derivation doesn't actually include all of xorg into the build context. :::

Run the last command again:

$ nix-build -A icat
this derivation will be built:
  /nix/store/x1d79ld8jxqdla5zw2b47d2sl87mf56k-icat.drv
...
error: builder for '/nix/store/x1d79ld8jxqdla5zw2b47d2sl87mf56k-icat.drv' failed with exit code 2;
       last 10 log lines:
       >   195 | # warning "_BSD_SOURCE and _SVID_SOURCE are deprecated, use _DEFAULT_SOURCE"
       >       |   ^~~~~~~
       > icat.c: In function 'main':
       > icat.c:319:33: warning: ignoring return value of 'write' declared with attribute 'warn_unused_result' [8;;https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html#index-Wunused-result-Wunused-result8;;]
       >   319 |                                 write(tempfile, &buf, 1);
       >       |                                 ^~~~~~~~~~~~~~~~~~~~~~~~
       > gcc -o icat icat.o -lImlib2
       > installing
       > install flags: SHELL=/nix/store/8fv91097mbh5049i9rglc73dx6kjg3qk-bash-5.2-p15/bin/bash install
       > make: *** No rule to make target 'install'.  Stop.
       For full logs, run 'nix log /nix/store/x1d79ld8jxqdla5zw2b47d2sl87mf56k-icat.drv'.

The missing dependency error is solved, but there is now another problem: make: *** No rule to make target 'install'. Stop.

installPhase

The stdenv is automatically working with the Makefile that comes with icat: you can see in the console output that configure and make are executed without issue, so the icat binary is compiling successfully.

The failure occurs when the stdenv attempts to run make install: the Makefile included in the project happens to lack an install target, and the README in the icat repository only mentions using make to build the tool, leaving the installation step up to users.

To add this step to your derivation, use the installPhase attribute, which contains a list of command strings to execute to perform the installation.

Because the make step completes successfully, the icat executable is available in the build directory, and you only need to copy it from there to the output directory.

In Nix, the output directory is stored in the $out variable, accessible in the derivation's component scripts. Create a bin directory within the $out directory and copy the icat binary there:

# icat.nix
{ lib
, stdenv
, fetchFromGitHub
, imlib2
, xorg
}:

stdenv.mkDerivation {
  name = "icat";

  src = fetchFromGitHub {
    owner = "atextor";
    repo = "icat";
    rev = "v0.5";
    sha256 = "0wyy2ksxp95vnh71ybj1bbmqd5ggp13x3mk37pzr99ljs9awy8ka";
  };

  buildInputs = [ imlib2 xorg.libX11.dev ];

  installPhase = ''
    mkdir -p $out/bin
    cp icat $out/bin
  '';
}

Phases and Hooks

Nixpkgs stdenv.mkDerivation derivations are separated into phases, each of which is intended to control some aspect of the build process.

You saw earlier how stdenv.mkDerivation expected the project's Makefile to have an install target, and failed when it didn't.

To fix this, you defined a custom installPhase containing instructions for copying the icat binary to the correct output location, in effect installing it.

Up to that point, the stdenv.mkDerivation automatically determined the buildPhase information for the icat package.

During derivation realisation, there are a number of shell functions ("hooks", in nixpkgs) which may execute in each derivation phase, which do things like set variables, source files, create directories, and so on.

These are specific to each phase, and run both before and after that phase's execution, controlling the build environment and helping to prevent environment-modifying behavior defined within packages from creating sources of nondeterminism within and between Nix derivations.

It's good practice when packaging software with Nix to include calls to these hooks in the derivation phases you define, even when you don't make direct use of them; this facilitates easy overriding of specific parts of the derivation later, in addition to the previously-mentioned reproducibility benefits.

You should now adjust your installPhase to call the appropriate hooks:

# icat.nix
...
  installPhase = ''
    runHook preInstall
    mkdir -p $out/bin
    cp icat $out/bin
    runHook postInstall
  '';
...

A successful build

Running the nix-build command once more will finally do what you want, and more safely than before; you can ls in the local directory to find a result symlink to a location in the Nix store:

$ ls
default.nix hello.nix icat.nix result

result/bin/icat is the executable built previously. Success!

References

Next steps