10 KiB
Snack
snack is a build tool that uses the power of Nix to build Haskell projects.
Snack requires Nix >= 2.0
It will
- use your existing Hpack file or a Nix-based config (described below).
- build your project incrementally: running
snack build
will only rebuild the modules that have been modified since the previous build. - work in the Nix sandbox.
- give you lots of cool Nix features for free: strong reproducibility guarantees, remote caching, remote builds, and more.
- improve build performance in some cases, for instance:
- all Haskell modules are built in parallel.
- there is a single linking step performed (typically) on a fast tmpfs.
Excited? Check out the install and usage sections. Make sure to also check out the Caveat Emptor section.
Why should I use Snack?
There are plenty of Haskell build tools around (Cabal, Stack, Bazel, ...). Unfortunately none of these allow what I consider to be an ideal workflow:
- The same build tool is used by developers and on CI.
- The build tool guarantees that builds are reproducible.
- The builds are incremental, i.e. if a library contains 300 modules and I
modify the
main
function, only theMain
module will be rebuilt.
Using Cabal inside of Nix solves (2); however this means that the builds are not incremental anymore (3). This may not be a problem on CI but definitely is when developing locally. The way to work around that is to use Cabal inside a nix-shell locally and call cabal2nix on CI. This means that developers use a different tool locally than on CI (1). Moreover, a lot of projects nowadays use Stack and, somewhat more importantly, Stackage LTSs. This makes local builds quite easy (in spite of the occasional rebuild when changing flags) but in order to perform a Nix build one has to generate some Nix boilerplate through tools like stackage2nix or stack2nix (which do not always work on CI).
In comparison, snack performs the exact same build on the developer's machine as on CI. The builds are incremental, maybe more so than Cabal builds: if you depend on a snack package foo from package bar, and modify a module Foo from foo which isn't used in bar, no recompilation will occur. Moreover, you benefit from your CI's cache. Finally, because snack is just Nix (and works with the Nix sandbox) you have pretty good guarantees that your builds are reproducible.
Caveat Emptor
The snack library and executable are in their very early stages. They need a lot of testing and massaging. The main (advertised) features are there, but (1) may break for your particular project and (2) may break more in the future.
Now that this is out of the way, install snack, break it, and help me improve it!
Install
See the Hacking section if you want to hack on snack
Assuming that Nix is installed on your machine, clone this repo and run:
$ ./script/install
which is equivalent to
$ nix-env -f ./default.nix -iA snack-exe
The snack executable is now in your PATH
:
$ snack --help
Usage: snack [-l|--lib DIR] [-b|--snack-nix PATH] [-j|--cores INT]
([-s|--package-nix PATH] | [-p|--package-yaml PATH]) COMMAND
Available options:
-l,--lib DIR Path to the directory to use as the Nix library
instead of the default one bundled with the snack
executable.
-j,--cores INT How many cores to use during the build
-h,--help Show this help text
Available commands:
build
run
ghci
Unavailable commands:
test Use build, run or ghci commands with test suites.
Snack can be used to build, run and interact with packages. There is no test command as we treat test suites as we do executables, giving each test suite its own package description.
Usage
There are two ways to tell snack about a package;
- Use
--package-nix
if you need more control over your build. - Use
--package-yaml
for simple builds or if you already have apackage.yaml
file.
If a package option is not supplied then snack will run as if
--package-nix=package.nix
was given as the package option.
The next two sections show an example config for each option. They use the following example project which displays the title of the top-rated post on the haskell subreddit (you can also find the code here):
.
├── app
│ └── Main.hs
└── src
└── Lib.hs
src/Lib.hs :
module Lib where
import Control.Lens
import Network.Wreq
import Data.Aeson.Lens
import Data.Text (Text)
topReddit :: IO Text
topReddit =
getWith opts url
<&> (^. responseBody
. key "data"
. key "children"
. nth 0
. key "data"
. key "title"
. _String)
where
url = "https://www.reddit.com/r/haskell/top.json"
opts = defaults
& param "limit" .~ ["1"]
& param "t" .~ ["all"]
app/Main.hs :
module Main where
import Lib
main :: IO ()
main = topReddit >>= print
Hpack
The project can have this minimal package.yaml
:
name: snack-readme
dependencies:
- lens
- wreq
library:
source-dirs: ./src
executable:
main: Main.hs
source-dirs: ./app
dependencies:
- snack-readme
default-extensions:
- OverloadedStrings
This command will build the project and display the top-rated post's title:
$ snack run --package-yaml ./package.yaml
You can also build without executing:
$ snack build --package-yaml ./package.yaml
Alternatively you can load up the project in ghci
:
$ snack ghci --package-yaml ./package.yaml
GHCi, version 8.2.2: http://www.haskell.org/ghc/ :? for help
[1 of 2] Compiling Lib ( /home/nicolas/projects/nmattia/snack/tests/readme/src/Lib.hs, interpreted )
[2 of 2] Compiling Main ( /home/nicolas/projects/nmattia/snack/tests/readme/app/Main.hs, interpreted )
Ok, two modules loaded.
*Main>
Nix
To build the project the following Nix config is sufficient:
let
lib =
{ src = ./src;
dependencies = [ "wreq" "lens" ];
extensions = [ "OverloadedStrings"];
};
in
{ main = "Main";
src = ./app;
packages = [ lib ];
dependencies = [ "wreq" "lens" ];
}
Building and running the project is as simple as
$ snack run # looks for a file called package.nix by default
Alternatively, use $ snack build
or $ snack ghci
if you only want to build,
or fire up ghci
, respectively.
Using other versions of GHC and nixpkgs
The snack executable comes with a bundled version of
nixpkgs and uses the GHC executable provided
by haskell.packages.ghc822.ghcWithPackages
. You may override those default by
providing a snack.nix
:
$ snack --snack-nix ./snack.nix build
This file looks like the following:
rec {
# If you only wish to change the version of GHC being used, set
# `ghc-version`. The following versions are currently available:
# * ghc7103
# * ghc7103Binary
# * ghc802
# * ghc821Binary
# * ghc822
# * ghc841
# * ghc842
# * ghcHEAD
# * ghcjs
# * ghcjsHEAD
# * integer-simple
# NOTE: not all versions have been tested with snack.
ghc-version = "ghc802";
# Alternatively you can provide you own `ghcWithPackages`, which should have
# the same structure as that provided by
# `pkgs.haskell.packages.<version>.ghcWithPackages:
ghcWithPackages = pkgs.haskellPackages.ghcWithPackages;
# Finally you can provide your own set of Nix packages, which should evaluate
# to an attribute set:
pkgs = import ./nix;
}
Advanced Nix Example
You may want custom builds that involve things such as archiving and base64 encoding entire directories.
snack builds itself, so its package.nix
is a good example
of an advanced configuration. You can also check out the test
folder.
Hacking
There are two different components you can hack:
- The snack executable in
bin/Snack.hs
- The snack library in
snack-lib/
Make sure you have a working version of snack installed, e.g.
$ git co master
$ nix-env -f ./default.nix -iA snack-exe
If you are hacking on the snack executable, just start snack in a GHCi session:
$ snack ghci -s ./bin/package.nix
Temporarily symlinking /nix/store/j1x5vkxjr2ibabddfkdih4sm4kwinfda-spec-json/spec.json to spec.json...
done.
Temporarily symlinking /nix/store/w42y6dzgfmli9r8kmgh8akqk6kyda31x-lib64/lib.tar.gz.b64 to lib.tar.gz.b64...
done.
GHCi, version 8.2.2: http://www.haskell.org/ghc/ :? for help
[1 of 1] Compiling Main ( /home/nicolas/projects/nmattia/snack/bin/Snack.hs, interpreted )
Ok, one module loaded.
*Main>
If you are hacking on the library, specify -l/--lib
when running snack (this
works in GHCi too):
*Main> :main ghci -l ./snack-lib/ -s ./tests/readme/package.nix
GHCi, version 8.2.2: http://www.haskell.org/ghc/ :? for help
[1 of 2] Compiling Lib ( /home/nicolas/projects/nmattia/snack/tests/readme/src/Lib.hs, interpreted )
[2 of 2] Compiling Main ( /home/nicolas/projects/nmattia/snack/tests/readme/app/Main.hs, interpreted )
Ok, two modules loaded.
*Main> :main
"\"Category Theory for Programmers\" has been finished!"
Mustn't be afraid to dream a little bigger, darling.
Thanks
Big thanks to