Creating a simple shell script package using flake-parts in nix
I spent probably a day or so trying to figure out how to package up a simple collection of shell scripts as a package in nix, specifically nix-darwin.
The context is pretty simple: Create a package that installs a set of git-hooks that I can use across all my git repos. I don't want to have to manually configure each git repo either. Instead I just want to specify it globally and spend a bit of time creating reusable robust git-hooks that ensure that all my repos work the way that I want them to. Spoiler, this also helps with enterprise development standardization to create simple, straightforward ways to ensure that code looks and feels the same across an entire organization of developers.
So here's the finished product, a simple collection of shell scripts installed as a package using flake-parts:
{
inputs,
config,
lib,
...
}: {
perSystem = {
config,
lib,
pkgs,
...
}: let
# use the non-bin to write it to a file in the store path, not great for public exposure
# also, this lets you substitute in any package you want inside of nix packages, just like the bin version
aContentFile = pkgs.writeShellScript "aContentFile" ''
set -euo pipefail
echo "help, I've fallen and I can't get up!"
'';
# use the *Bin version to write it to /bin/hello-world
helloWorld = pkgs.writeShellScriptBin "hello-world" ''
set -euo pipefail
CONTENT=$(${aContentFile})
${pkgs.cowsay}/bin/cowsay "HELLO $CONTENT"
'';
# make the actual package derivation that we can assign to flake-parts.packages.<pkgname>
helloWorldPackage = pkgs.stdenv.mkDerivation {
name = "hello-world";
version = "1.0.0";
dontBuild = true;
dontUnpack = true;
installPhase = ''
mkdir -p "$out/bin"
# add our shell script (or scripts!)
cp ${helloWorld}/bin/hello-world $out/bin/
'';
};
in {
packages.hello-world = helloWorldPackage;
};
}
So lets break this down a bit more. I've made some comments in that scope to make some things more obvious.
The secret is that pkgs is only available inside of the perSystem scope. It's not available as a parameter to the top level flake-parts call. That took me a long time to figure out from the documentation. Once I got that part down, the secret is to do most of the setup inside of the let...in block ahead of the packages section. Create your derivation like you would normally, and then just assign it to the defined package that you want available in your flake-parts based flake.
This also cooperates with the dendritic pattern, so you can just throw it into any nix file anywhere in your flake, and the package will be available to be packaged. You can test the build of the package trivally: nix build .#hello-world It's available and easy to build. flake-parts will make sure it's available for all the system architectures, and it should just work.
I'm writing this down mostly for me to reference later, because it took me a long time to figure it out, and maybe it'll be useful for you as well.