Nix: How to Package a Shell Script
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
- Setup hook:
- A utility bash function executed in one of the Nix derivation builder phases to perform common tasks
- nixpkgs Manual
- Definition of
makeSetupHook
in nixpkgs
makeWrapper
attribute definition in nixpkgsmakeWrapper
setup-hook (script) implementation
0 Comments