Skip to content
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
184 lines (134 sloc) 6.35 KB

Spago2Nix: Why and How

Recently, I spent my time at ZuriHac making Spago2Nix, a tool to generate a Nix expression of PureScript dependencies specified by Spago. In this post, I'll talk about what I wanted to get done with this tool, and how I went about it.

Generating a Nix expression of packages

First off, I need to get an expression built of dependencies needed to build our project. Spago makes this easy enough with a bunch of flag soup:

spago list-packages -f transitive -j # -f: filter by, -j: in JSON format

And of course, I need to prefetch the git repos with the revision information along with the contents hash I need for Nix, so I do this using nix-prefetch-git:

nix-prefetch-git {url} --rev {version} --quiet # --quiet will give me JSON

Of course, using the --quiet flag with nix-prefetch-git will happily fetch you an empty repo, so you must be sure to check if you have cloned an empty repo:

brokenRepoSHA :: SHA256
brokenRepoSHA = SHA256 "0sjjj9z1dhilhpc8pq4154czrb79z9cm044jvn75kxcjv6v5l2m5"

Once I have this information, then each package can be built up:

    "PKGNAME" = pkgs.stdenv.mkDerivation { # PKGNAME e.g. aff
        name = "PKGNAME";
        version = "VERSION"; # VERSION e.g. v1.0.0
        src = pkgs.fetchgit {
          url = "URL"; # URL e.g.
          rev = "REV"; # REV being some SHA
          sha256 = "SHA256"; # SHA256 being the prefetch SHA contents
        phases = "installPhase";
        installPhase = "ln -s $src $out";

The reason I use this method of defining derivations is because I want to be able to represent the same content as the repo fetch, but be free to change the metadata around the cloned repo as needed. I have found overriding the properties to create new entries to be wasteful, so this is what I have now.

Installing these packages

So let's actually think about what "installing" means, because there are multiple different phases involved.

  1. A package should be downloaded by Nix into the Nix Store
  2. We want to copy this Nix Store entry into our project under .spago so Spago can use it, and so we can also modify the sources we use in our project whenever we wish (e.g. for debugging).

So the first step can be performed by building a derivation which uses the information. Which leads us to the second: how should we copy Nix Store entries to our local project?

After much consideration, I found the most realistic answer to be to write a derivation which creates a bash script.


Imagine we started our output template in such a way:

{ pkgs ? import <nixpkgs> {} }:

  inputs = {
INPUTS # these are the packages from above i will shove in

With this structure, inputs is a set we could get the attribute values for to map a function for what individual operations we need. So we could very well start writing a bash script derivation like so:

  installSpagoStyle = pkgs.runCommand "install-spago-style" {} ''
      >>$out echo "#!/usr/bin/env bash"
      >>$out echo
      >>$out echo "echo installing dependencies..."
      >>$out echo "${builtins.toString ( cpPackage (builtins.attrValues inputs))}"
      >>$out echo "echo done."
      chmod +x $out

Another thing we should keep in mind is that the PureScript compiler (as of 0.13.0) is not smart enough about rebuilding: because it uses a timestamp to figure out if corresponding outputs need to be rebuilt. This a defect that will be fixed eventually. Either way:

  cpPackage = pkg:
      target = ".spago/${}/${pkg.version}";
    in ''
      if [ ! -e ${target} ]; then
        echo "Installing ${target}."
        mkdir -p ${target}
        cp --no-preserve=mode,ownership,timestamp -r ${toString pkg.outPath}/* ${target}
        echo "${target} already exists. Skipping."

Of course, I have to use the no-preserve flags because copying straight from the Nix Store will give you unusable files by default.

Building the project

Same thing but with purs compile, and taking in any more args accordingly:

  getGlob = pkg: ''\".spago/${}/${pkg.version}/src/**/*.purs\"'';

# ...

  buildSpagoStyle = pkgs.runCommand "build-spago-style" {} ''
      >>$out echo "#!/usr/bin/env bash"
      >>$out echo
      >>$out echo "echo building project..."
      >>$out echo "purs compile \"\$@\" ${builtins.toString ( getGlob (builtins.attrValues inputs))}"
      >>$out echo "echo done."
      chmod +x $out


All of this is implemented as a simple PureScript-Node program that calls out to the various programs needed:

foreign import argv :: Array String

args :: List String
args = List.drop 2 $ List.fromFoldable argv

main :: Effect Unit
main = Aff.launchAff_ do
  case args of
    "generate" : List.Nil -> Generate.generate
    "install" : rest -> install rest
    "build" : rest -> build rest
    "help" : rest -> log help
    List.Nil -> log help
    _ -> do
      log $ "Unknown arguments: " <> List.intercalate " " args

I prebuild this program and have it packaged on npm, but it is also available to install via a fetchFromGitHub derivation:

in pkgs.stdenv.mkDerivation {
  name = "spago2nix";

  src = ./.;

  buildInputs = [ pkgs.makeWrapper ];

  installPhase = ''
    mkdir -p $out/bin

    >>$target echo '#!/usr/bin/env node'
    >>$target echo "require('$src/bin/output.js')";

    chmod +x $target

    wrapProgram $target \
      --prefix PATH : ${pkgs.lib.makeBinPath [


I hope this has shown you how Spago2Nix works, and how you can easily extend this to your own usages or make your own tool for your own needs.


Honestly though, this is written for my own usage. If you want it to do some other things, you might open some issue on the repo, but I mostly want other people to make PRs to implement features if they really need them. In general, Nix is something that does take some effort to use, so I expect that people who want to use this project are willing to ask some questions and help out. At the very least, help me help you.


You can’t perform that action at this time.