Skip to content

pjones/nix-hs

Repository files navigation

Haskell + nixpkgs = nix-hs

tests GitHub tag (latest by date) cachix

A thin layer over the existing Haskell infrastructure in nixpkgs which adds all of the tools needed for interactive development. Here are some of the features that you might find the most useful:

  • An interactive development environment (via nix-shell) that includes:

    NOTE: The following tools do not currently support GHC 9.0.1:

    • haskell-language-server

    • ormolu

    • stan

  • Easy to use system for overriding Haskell packages (e.g., use a package not on Hackage, fix a package marked as broken, etc.) without having to write tons of Nix code.

  • Works seamlessly with direnv, lorri, and niv if you already have those tools installed.

  • Switch GHC versions by passing an argument to nix-build or nix-shell.

  • Strip dependencies from a package so you can deploy just a binary without tons of other packages coming along for the ride.

  • Create an interactive development environment without adding nix-hs as a project dependency.

  • Fetch pre-built tools from the binary cache.

Geting Started

Create a default.nix file that looks something like this:

{ pkgs ? import <nixpkgs> { }
}:
let
  nix-hs =
    import
      (fetchTarball
        "https://github.com/pjones/nix-hs/archive/release-21.05.tar.gz")
      {
        inherit pkgs;
      };
in
nix-hs {
  cabal = ./test/hello-world/hello-world.cabal;
}

And a shell.nix that looks like this:

# Load an interactive environment:
(import ./. {}).interactive

That's it! Now nix-build and nix-shell just work!

Configuration

In addition to the cabal argument to the nix-hs function, there are other ways to control how your package is built.

Enable Flags from the Cabal File

If you have a flag defined in your package's cabal file you can enable it using the flags argument:

nix-hs {
  cabal = ./mypackage.cabal;
  flags = [ "someflagname" ];
}

Using a Broken Package

If one of your package's dependencies can't be built you can try overriding it:

nix-hs {
  cabal = ./mypackage.cabal;

  overrides = lib: self: super: with lib; {
    pipes-text = unBreak (dontCheck (doJailbreak super.pipes-text));
  };
}

In the example above, the overrides function takes three arguments:

  1. lib: An attribute set of library functions. These are the functions provided by the pkgs.haskellPackages.lib set plus a few more that you might find useful such as:

    • unBreak: Remove the broken flag from a package
    • compilerName: The nixpkgs name of the Haskell compiler being used (e.g. ghc884)
    • pkgs: The full nixpkgs package set, after overriding
  2. self: The final set of Haskell packages after applying all overrides. This refers to the future version of the package set so if you're not careful you can fall into infinite recursion. When in doubt use super instead.

  3. super: The set of Haskell packages that are being modified. Use this attribute set to refer to existing Haskell packages. You can also use super to access useful functions such as callCabal2nix and callHackageDirect.

The overrides function should return an attribute set of Haskell packages. The set of returned packages will be merged into the final set used to build your package.

An example package using the overrides function can be found in test/overrides/default.nix. The full list of additional lib functions is in lib/haskell.nix.

Working with Multi-Package Cabal Projects

If you have a project that contains multiple Cabal packages you can build them all with a single default.nix. The cabal argument to nix-hs can either be a path to a Cabal file or an attribute set of Cabal files:

nix-hs {
  cabal = {
    package1 = ./package1/package1.cabal;
    package2 = ./package1/package2.cabal;
  };
}

Integrating Your Text Editor and Shell

The best way to let your text editor and shell use the environment created from Nix is to use direnv. Here's an example .envrc file:

# Use lorri if it's installed, otherwise load shell.nix:
if has lorri; then
  eval "$(lorri direnv)"
else
  use nix
fi

# Reload if these files change:
watch_file $(find . -name '*.cabal' -o -name '*.nix')

NOTE: Make sure you have a shell.nix file that exposes the interactive attribute of the derivation, like the example above.

Interactive Environments Without nix-hs

If you don't want to use nix-hs to control your default.nix you can still use it for building an interactive development environment. Just clone this repository and use the shell/default.nix file.

For example, to drop into an interactive shell:

$ nix-shell /path/to/nix-hs/shell

Or

$ nix-shell --argstr compiler 8.8.3 /path/to/nix-hs/shell

Even better, use direnv so your normal shell and text editor can see all the installed development tools. Here's an example .envrc file:

use nix /path/to/nix-hs/shell

Access to Binary-Only Packages

The derivation generated by the nix-hs function makes it easy to access a "binary only" derivation. This is perfect for deployments or Docker containers where you don't want to bring along all of your package's dependencies (including GHC).

The bin attribute of the derivation gives you access to this binary only derivation. For example, to create a docker container put the following in docker.nix:

{ pkgs ? import <nixpkgs> { }
}:

let
  mypackage = (import ./. { inherit pkgs; }).bin;

in pkgs.dockerTools.buildImage {
  name = "mypackage";
  tag  = "latest";

  config = {
    Cmd = [ "${mypackage}/bin/hello" ];
  };
}

Using the Binary Cache

If you don't want to spend all day compiling the tools needed to build your Haskell package and its development environment you can use the nix-hs cache on Cachix.

The cache is populated after each git push via a GitHub action.

NOTE: Due to disk space limitations the cache is limited to the current LTS version of GHC.