Writing packages

Sooner or later you want something that is not in nixpkgs, or you want to ship a small script of your own as a proper command. This page goes from the trivial case, wrapping a shell script, up to a real stdenv.mkDerivation, fetchers and hashes, and exposing the result from your flake so pkgs.mypkg works inside configuration.nix. The Editing your config page already covers override and overrideAttrs for tweaking existing packages, so here the focus is building your own. Every claim follows the official Nixpkgs manual .

When you need this

Reach for a custom package in three situations.

  • The software is not in nixpkgs at all, so there is no attribute to add to a package list.
  • You wrote a script or have a prebuilt binary and want it installed like any other command, on PATH, with its dependencies pinned.
  • An existing package is close but you need a patch, a newer version, or a build flag. That last case is usually overrideAttrs, covered briefly in Advanced below and in full on the editing page.

If none of these apply, you probably want a package name in environment.systemPackages or an option in Options, not a new derivation.

The trivial case, wrapping a script

Most of the time you are not compiling anything. You have a few lines of shell and you want them to become a command. Nixpkgs ships trivial builders for exactly this, and they are the right tool far more often than people expect.

writeShellApplication is the best default. It writes your script, puts the tools it needs on PATH, and runs it through shellcheck at build time so a typo fails the build instead of your morning.

# backup-home.nix
{ writeShellApplication, rsync, openssh }:
 
writeShellApplication {
  name = "backup-home";
  runtimeInputs = [ rsync openssh ];
  text = ''
    dest="''${1:?usage: backup-home <host>}"
    rsync -avz --delete "$HOME/" "$dest:/backups/$USER/"
  '';
}

runtimeInputs is the important part. Anything you list there is guaranteed to be on PATH when the script runs, so rsync resolves to the exact nixpkgs build you pinned rather than whatever happens to be installed on the host. The doubled single quotes (''${...}) escape a ${ that you want passed through to bash instead of interpreted by Nix.

If you want the bare minimum with no shellcheck and no PATH wrangling, use writeShellScriptBin. It produces a derivation with a single executable under bin/ named after the first argument.

pkgs.writeShellScriptBin "hello-me" ''
  echo "hello, $USER, the time is $(date)"
''

For a plain file rather than an executable, writeText writes its second argument to the store and returns the path. This is handy for config files you want to reference from elsewhere.

pkgs.writeText "motd.txt" ''
  Managed by Ternix. Do not edit by hand.
''

To actually install a wrapped script system-wide, call the file and drop it into environment.systemPackages. The file takes its dependencies as named arguments, which callPackage fills in from the package set for you.

# configuration.nix
{ pkgs, ... }:
{
  environment.systemPackages = [
    (pkgs.callPackage ./backup-home.nix { })
  ];
}

After a rebuild the backup-home command is on every shell's PATH. The trivial builders chapter lists the full family, including writeShellScript, symlinkJoin, and runCommand.

Building real software with mkDerivation

When there is source to compile, stdenv.mkDerivation is the foundation almost every package in nixpkgs is built on. A derivation is a description of how to turn inputs into an output in the Nix store. You give it a name, a version, a source, the tools it needs, and Nix runs a fixed sequence of phases.

The core attributes are small in number.

  • pname and version combine into the package name and the output path.
  • src is where the source comes from, usually a fetcher (see below).
  • nativeBuildInputs are tools that run during the build, on the build machine. Compilers, pkg-config, cmake, makeWrapper go here.
  • buildInputs are libraries the built program links against and needs at runtime. The split matters for cross compilation, where build tools and target libraries come from different package sets.

The build runs through phases in order. The ones you touch most are unpackPhase (extract src), buildPhase (compile), and installPhase (copy results into $out, the output path in the store). For a standard autotools or Makefile project you write none of them. The defaults run ./configure if a configure script exists, then make, then make install, which is why many nixpkgs derivations have almost no phase code.

Here is a complete derivation for a tiny Makefile-based C program. The Makefile honours a PREFIX, so the default installPhase would even work, but writing it out shows where the output goes.

# hello-c.nix
{ stdenv, fetchFromGitHub }:
 
stdenv.mkDerivation rec {
  pname = "hello-c";
  version = "1.0.0";
 
  src = fetchFromGitHub {
    owner = "example";
    repo = "hello-c";
    rev = "v${version}";
    hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
  };
 
  # gcc and make come from stdenv already, so nativeBuildInputs is empty here.
 
  installPhase = ''
    runHook preInstall
    mkdir -p $out/bin
    cp hello $out/bin/
    runHook postInstall
  '';
}

Two details worth keeping. $out is the only place a build may write its results, and the directory layout under it (bin, lib, share) is what the rest of the system expects. The runHook preInstall and runHook postInstall calls preserve the hooks other code may attach to the phase, so include them when you override a phase by hand.

If the project uses CMake, add the build tool to nativeBuildInputs and let the setup hooks configure it for you.

{ stdenv, fetchFromGitHub, cmake, zlib }:
 
stdenv.mkDerivation rec {
  pname = "example-tool";
  version = "2.3.1";
 
  src = fetchFromGitHub {
    owner = "example";
    repo = "example-tool";
    rev = "v${version}";
    hash = "sha256-BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=";
  };
 
  nativeBuildInputs = [ cmake ];
  buildInputs = [ zlib ];
  # cmake's setup hook supplies configurePhase, buildPhase, and installPhase.
}

The full reference is the stdenv chapter, which documents every phase and the setup hooks that fill them in.

Fetchers and hashes

src almost always comes from a fetcher, a function that downloads something and verifies it against a hash. The hash is what makes the download reproducible. If the bytes ever change, the build fails instead of silently using different source.

fetchFromGitHub is the common one for source repositories.

src = fetchFromGitHub {
  owner = "BurntSushi";
  repo = "ripgrep";
  rev = "14.1.0";
  hash = "sha256-aXLMOM6tEY+sjL3xMjmsuQ5IkPar3GZ6INxC+Up3Wgw=";
};

fetchurl fetches a single file by URL, for release tarballs or prebuilt binaries.

src = fetchurl {
  url = "https://example.com/releases/tool-1.2.0.tar.gz";
  hash = "sha256-CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=";
};

You will not know the hash before the first build. The standard trick is to put a fake hash in, let the build fail, and copy the real one out of the error. Use lib.fakeHash, which is a well-known all-zero placeholder.

src = fetchFromGitHub {
  owner = "example";
  repo = "thing";
  rev = "v1.0.0";
  hash = lib.fakeHash;   # replace after the first build
};

Build it once. Nix downloads the source, computes the real hash, sees it does not match the placeholder, and prints both values.

nixos-rebuild build --flake .#nixos-host
# error: hash mismatch in fixed-output derivation ...
#          specified: sha256-AAAA...
#             got:    sha256-aXLMOM6tEY+sjL3xMjmsuQ5IkPar3GZ6INxC+Up3Wgw=

Copy the got value into hash and rebuild. It now matches and the source is pinned. For a single URL you can also compute the hash up front without a failing build.

nix-prefetch-url --type sha256 https://example.com/releases/tool-1.2.0.tar.gz

The fetcher reference lives in the fetchers chapter.

callPackage and wiring a package file

Every example above wrote the package as a function of its inputs, for instance { stdenv, fetchFromGitHub }: .... That is the nixpkgs convention, and callPackage is what makes it pleasant. It looks at the function's argument names, finds matching attributes in the package set, and passes them in automatically. You only supply the arguments it cannot find.

# anywhere you have pkgs
pkgs.callPackage ./hello-c.nix { }

If your function takes an argument that is not a standard package, pass it in the second set.

pkgs.callPackage ./hello-c.nix {
  stdenv = pkgs.clangStdenv;   # build with clang instead of gcc
}

Keeping each package in its own file with a callPackage-style signature means the same file works unchanged whether you call it from a config, from an overlay, or from your flake outputs. The callPackage section covers the mechanism in detail.

Language framework builders

For software written in a single language, nixpkgs ships dedicated builders that handle dependency resolution and the language's tooling. They wrap mkDerivation, so the attributes you already know still apply, with a few extra fields for the dependency lock.

Rust, using rustPlatform.buildRustPackage, with cargoHash covering the dependency tree.

{ rustPlatform, fetchFromGitHub }:
 
rustPlatform.buildRustPackage rec {
  pname = "mytool";
  version = "0.4.0";
  src = fetchFromGitHub {
    owner = "me";
    repo = "mytool";
    rev = "v${version}";
    hash = "sha256-DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD=";
  };
  cargoHash = "sha256-EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE=";
}

Node, using buildNpmPackage, with npmDepsHash over the lockfile.

{ buildNpmPackage, fetchFromGitHub }:
 
buildNpmPackage rec {
  pname = "myapp";
  version = "1.1.0";
  src = fetchFromGitHub {
    owner = "me";
    repo = "myapp";
    rev = "v${version}";
    hash = "sha256-FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF=";
  };
  npmDepsHash = "sha256-GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG=";
}

Python applications, using buildPythonApplication, with dependencies from the Python package set.

{ python3Packages, fetchPypi }:
 
python3Packages.buildPythonApplication rec {
  pname = "mycli";
  version = "2.0.0";
  pyproject = true;
  src = fetchPypi {
    inherit pname version;
    hash = "sha256-HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH=";
  };
  build-system = [ python3Packages.setuptools ];
  dependencies = with python3Packages; [ click requests ];
}

Each language has its own quirks around lock hashes and offline builds. The language framework reference is the Languages and frameworks chapter, which has a section per ecosystem.

Exposing the package from your flake

Once a package builds, you usually want two things. To build it directly with nix build, and to have it available as pkgs.mypkg everywhere in your config. Both come from the flake.

Add it under packages.<system> so nix build and nix run can find it. Ternix generates a host named nixos-host on x86_64-linux, so use that system.

# flake.nix
{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
 
  outputs = { self, nixpkgs, ... }:
    let
      system = "x86_64-linux";
      pkgs = nixpkgs.legacyPackages.${system};
    in {
      packages.${system}.mypkg = pkgs.callPackage ./mypkg.nix { };
 
      nixosConfigurations.nixos-host = nixpkgs.lib.nixosSystem {
        inherit system;
        modules = [ ./configuration.nix ];
      };
    };
}

That alone lets you run nix build .#mypkg. To make pkgs.mypkg resolve inside configuration.nix, add the package through an overlay. An overlay is a function that extends the package set, and nixpkgs.overlays is the option that applies it to the whole system.

# configuration.nix
{ pkgs, ... }:
{
  nixpkgs.overlays = [
    (final: prev: {
      mypkg = final.callPackage ./mypkg.nix { };
    })
  ];
 
  environment.systemPackages = [ pkgs.mypkg ];
}

Now pkgs.mypkg works in any module of this host, exactly like a package that shipped in nixpkgs. Rebuild to apply it.

sudo nixos-rebuild switch --flake .#nixos-host

Advanced

Overlays in depth

An overlay is a function of two arguments, conventionally final and prev. prev is the package set before this overlay, and final is the set after every overlay has been applied, including this one. The rule of thumb is to take dependencies from final so later overlays can still override them, and to refer to the thing you are replacing through prev.

(final: prev: {
  # a brand new package, dependencies resolved from the final set
  mypkg = final.callPackage ./mypkg.nix { };
 
  # a modified version of an existing package, based on the previous one
  ripgrep = prev.ripgrep.overrideAttrs (old: {
    doCheck = false;
  });
})

Overlays compose. Each one sees the results of the ones before it, which is how nixpkgs lets independent changes stack without conflicting. The overlays chapter explains the final and prev contract precisely.

overrideAttrs for patching upstream

When an existing package is almost right, overrideAttrs patches the attributes that went into its mkDerivation call, without re-declaring the whole thing. This is the tool for applying a patch, pinning a different version, or adding a build flag to something nixpkgs already packages.

(final: prev: {
  hello = prev.hello.overrideAttrs (old: {
    version = "2.12.1";
    patches = (old.patches or [ ]) ++ [ ./fix-greeting.patch ];
  });
})

The function receives the old attribute set, so old.patches or [ ] keeps any patches the upstream package already carried and appends yours. For changing the inputs a package was called with rather than its build attributes, use override, which the Editing your config page covers alongside more examples.

Next

  • Editing your config covers override and overrideAttrs for tweaking existing packages.
  • Flakes explains inputs, the lock file, and how outputs like packages fit together.
  • Examples shows worked configs that put overlays and packages to use.
  • Troubleshooting helps when a build fails, including hash mismatches.