Nicer Package Organization with Stack!
In last week's article we explored Haskell package management using Cabal. This tool has been around for a while and serves as the backbone for Haskell development even to this day. We explored the basics of this tool, but also noticed a few issues. These issues centered around dependency management, and what happens when package versions conflict.
Nowadays, most Haskell developers prefer the Stack tool to Cabal. Stack still uses many of the features of Cabal, but adds an extra layer, which helps deal with the problems we saw. In this article, we'll do a quick overview of the Stack tool and see how it helps.
For a more in depth look at Stack, you should take our free Stack mini-course . While you're at it, you can also download our Beginner Checklist. This will help ensure you're up to speed with the basics of Haskell.
Creating a Stack Project
Making a new project with Stack is a little more streamlined than with Cabal by itself. To start with, we don't need the extra step of creating our project directory before hand. The stack new
command handles creating this directory.
>> stack new MyStackProject
>> cd MyStackProject
By default, Stack will also generate some basic source files for our project. We get a library file in src/Lib.hs
, an executable program in app/Main.hs
, and a test in test/Spec.hs
. It also lists these files in the .cabal
file. So if you're newer to using Haskell, it's easier to see how the file works.
You can use different templates to generate different starter files. For example, the servant
template will generate boilerplate for a Servant web server project. The .cabal
file includes some dependencies for using Servant. Plus, the generated starter code will have a very simple Servant server.
>> stack new MyStackProject servant
Basic Commands
As with Cabal, there are a few basic commands we can use to compile and run our Haskell code with Stack. The stack build
command will compile our library and executables. Then we can use stack exec
with an executable name to run that executable:
stack exec run-project-1
stack exec run-project-2
Finally, we can run stack test
to run the different test suites in our project. Certain commands are actually variations on stack build
, with different arguments. In these next examples, we run all the tests with --test
, and then run a single test suite by name.
>> stack build --test
>> stack build MyStackProject:test:test-suite-1
Stack File
Stack still uses Cabal under the hood, so we still have a .cabal
file for describing our package format. But, we also have another package file called stack.yaml
. If you look at this file, you'll see some new fields. These fields provide some more information about our project as a whole. This information will help us access dependencies better.
The resolver
field tells us which set of packages we're using from Stackage. We'll discuss this more later in the article.
resolver: lts-13.19
Then the packages
field gives a list of the different packages in our project. Stack allows us to manage multiple packages at once with relative ease compared to Cabal. Each entry in this list refers to a single directory path containing a package. Each individual package has its own .cabal
file.
packages:
- ./project-internal
- ./project-parser
- ./project-public
Then we also see a field for extra-deps
. This lists dependency packages outside of our current resolver set. By default, it should be empty. Again, we'll explore this a bit later once we understand the concept of resolvers better.
Installing Packages
Besides the basic commands, we can also use stack install
. But its functionality is a bit different from cabal install
. If we just use stack install
by itself, this will "install" the executables for this project on our local path. Then we can run them from any directory on our machine.
>> stack install
...
Copied executables to /home/username/.local/bin/
- run-project-1
- run-project-2
>> cd ..
>> run-project-1
"Hello World!"
This is different from the way we installed dependency packages using cabal install
. We could use stack install
in a similar way. But this is more for different Haskell programs we want to use. For example, we can install the hlint
code linter like so:
stack install hlint
But unlike with vanilla Cabal, it's unnecessary to do this with dependency packages! Using stack build
installs dependencies for us! We can add a dependency (say the split
package), and build our code without a separate install
command!
This is one advantage we get from using Stack, but on its own it still seems small. Let's start looking at the real benefits from Stack when it comes to dependency conflicts.
Cross-Project Conflicts
Recall what happened when using different versions of a package on different projects on our machine. We encountered a conflict, since the global index could only have one version of the package. We solved this with Cabal by using sandboxes. A sandbox ensured that a project had an isolated location for its dependencies. Installing packages on other projects would have no effect on this sandbox.
Stack solves this problem as well by essentially forcing us to use sandboxing. It is the default behavior. Whenever we build our project, Stack will generate the .stack-work
directory. This directory contains all our dependencies for the project. It also stores our compiled code and executables.
So with Stack, you don't have to remember if you already initialized the sandbox when running normal commands. You also don't have to worry about deleting the sandbox on accident.
Dependency Conflicts
Sandboxing solves the issue of inter-project dependency conflicts. But what about within a project? Stack's system of resolvers is the solution to these. You'll see a "resolver version" in your stack.yaml
file. By default, this will be the latest lts
version at the time you make your project. Essentially, a resolver is a set of packages that have no conflicts with each other.
Most Haskell dependencies you use live on the Hackage repository. But Stack adds a further layer with Stackage. A resolver set in Stackage contains many of the most common Haskell libraries out there. But there's only a single version of each. Stack maintainers have curated all the resolver sets. They've exhaustively checked that there are no dependency conflicts between the package versions in the set.
Here's an example. The LTS-14.20 resolver uses version 1.4.6.0 of the aeson library. All packages within the resolver that depend on this library will be compatible with this version.
So if you stick to dependencies within the resolver set, you won't have conflicts! This means you can avoid the manual work of finding compatible versions.
There's another bonus here. Stack will determine the right version of the package for us. So we generally don't need version constraints in our .cabal
files. We would just list the dependency and Stack will do the rest.
library
exposed-modules: Lib
build-depends:
base
, split
...
You'll generally want to stick to "long term support" (lts) resolvers. But there are also nightly
resolvers if you need more bleeding edge libraries. Each resolver also corresponds to a particular version of GHC. So Stack figures out the proper version of the compiler your project needs. You can also rest assured that Stack will only use dependencies that work for that compiler.
Adding Extra Packages
While it's nice to have the assurance of non-conflicting packages, we still have a problem. What if we need Haskell code that isn't part of the resolver set we're using? We can't expect the Stack curators to think of every package we'll ever need. There's a lot of code on Github we could use. And indeed, even many libraries on Hackage are not in a Stackage resolver set.
We can still import these packages though! This is what the extra-deps
field of stack.yaml
is there for. We can add entries to this list in a few different formats. Generally, if you provide a name and a version number, Stack will look for this library on Hackage. You can also provide a Github repository, or a URL to download. Here are a couple examples:
extra-deps:
- snappy-0.2.0.2
- git: https://github.com/tensorflow/haskell.git
commit: d741c3ee59a5c9569fb2026bd01c1c8ff22fa7c7
Note that you'll still have to add the package name as a dependency in your .cabal
file. You can omit the version number though, as we did with other packages above.
Often, using one package as an "extra dependency" will then require other packages outside the resolver set. Getting all these can be a tedious process. But you can usually use the stack solver
command to get the full list of packages (and versions) that you need.
Unfortunately, when you introduce extra dependencies you're no longer guaranteed of avoiding conflicts. But if you do encounter conflicts, you have a clear starting point. It's up to you to figure out a version of the new package you can use that is compatible with your other dependencies. So the manual work is much more limited.
Conclusion
That wraps up our look at Stack and how it solves some of the core problems with using Cabal by itself. Next week, we'll explore the hpack tool, which simplifies the process of making our .cabal
file. In a couple weeks, we'll also take a look at the Nix package manager. This is another favorite of Haskell developers!