Nix: How to Package a Shell Script

Published by Philipp Schuster on

Nix with its ecosystem is amazing tooling and the library functions in nixpkgs provide great builders to package certain programs, scripts, and stuff. But in this jungle of options, it’s also sometimes not clear which option is the best or most pragmatic solution.

In this blog post, I’m going to present you the most pragmatic and easiest way to package a shell script in Nix from my year-long experience of packaging things. In case anyone knows a better approach, please leave a comment! 😉

Firstly, there are two main ways of packaging a shell script in Nix:

  • package a script in-place (script embedded in Nix file)
    –> pkgs.writeShellScriptBin
  • package a standalone script (script.sh file + Nix file)
    –> pkgs.makeWrapper (“wrapProgram”)

Packaging a Shell Script In-place

By using writeShellScriptBin, we can easily package a shell script as follows. This basically means we embed the bash script as a string in Nix. This has the advantage that we can use Nix variable expansion while generating the script:

package.nix

{
  lib,
  writeShellScriptBin,
  # runtime deps (choose what you need)
  python3Minimal,
}:

writeShellScriptBin "my-shell-script" ''
  export PATH="${
    lib.makeBinPath ([
      python3Minimal
    ])
  }:$PATH"
 
  echo hello from script
  python3 --version
''

This one is easy and works well for small shell scripts that you have full control over, i.e., non-external scripts. However, there are situations where one prefers to have a dedicated script.sh file as you want to package a shell script as is. Then, you need a more flexible approach.

Packaging a Dedicated Shell Script File

nixpkgs provides the Nix attribute makeWrapper which exports the setup hooks (“build helpers”) makeWrapper and wrapProgram. wrapProgram is a convenient wrapper around makeWrapper and what you typically want to use. We can combine everything together to package a shell script like so:

package.nix

{
  lib,
  makeWrapper,
  runCommand,
  # runtime deps (depend on script)
  bash,
  tree,
}:

let
  # Your external shell script.
  src = ./some-script.sh:
  binName = "some-script";
  deps = [
    bash
    tree
  ];
in
runCommand "${binName}"
  {
    nativeBuildInputs = [ makeWrapper ];
    meta = {
      mainProgram = "${binName}";
    };
  }
  ''
    mkdir -p $out/bin
    install -m +x ${src} $out/bin/${binName}

    wrapProgram $out/bin/${binName} \
      --prefix PATH : ${lib.makeBinPath deps}
  ''

Notice how wrapProgram is used to prepare the environment for the bash script as the script expects it? That’s convenient!

Using runCommand plus wrapProgram is what I consider to be the most idiomatic way for these class of packaging problems. Some developers prefer to use mkDerivation, but I think that mkDerivation is a little weird to use there, as we do not have a buildPhase and just need an installPhase.

Summary

Above, I’ve shown two ways of how to package shell scripts in Nix. It depends on your use-case and personal taste which one to pick. There is no single solution to rule them all, but the two versions above cover almost all typical use-cases.

Hope that helps. Cheers!

More Links & Info


Philipp Schuster

Hi, I'm Philipp and interested in Computer Science. I especially like low level development, making ugly things nice, and de-mystify "low level magic".

0 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *