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.
pnameandversioncombine into the package name and the output path.srcis where the source comes from, usually a fetcher (see below).nativeBuildInputsare tools that run during the build, on the build machine. Compilers,pkg-config,cmake,makeWrappergo here.buildInputsare 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.gzThe 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-hostAdvanced
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.