Extra-project Stack

Introduction

In my experience stack1 works pretty well out-of-the-box when you use it to compile projects whose dependencies can be found in Stackage.

In fact, stack works so well that I could use it without understanding how it worked. All fine, until I wanted to use stack for standalone .hs files, or use a local library. All the information below is in stack’s excellent official documentation2, but I found it hard to put together.

Normal operation

It seems wise to document how I understand stack works, at least the bits which are relevant to the discussions below. The details of this might well be wrong: regard it less as an accurate description of stack than an approximation which might be helpful later.

Suppose we have some project which we want to compile with stack. To zeroth order it looks just like a normal cabal project. In particular, there’s a foo.cabal file which lists the targets, the dependencies and so on. None of that changes under stack. We do however, get a couple of new things in the project directory:

stack.yaml which lets us specify extra stuff;

a .stack-work directory in which everything is built.

When we execute e.g. stack build stack compiles everything, dependencies and all, in the .stack-work directory. I think it’s worth reiterating that stacks know what to build, and which dependencies are needed by looking in the project’s .cabal file.

Project-free use

Of course there’s nothing to stop us invoking stack outside a project, but it slightly begs the question of what stack will do about the .cabal and stack.yaml files, and the .stack-work directory.

Let’s proceed by experimentation, and launch ghci in my home directory:

Well that explains what’s happening about stack.yaml: stack has set up a global-project directory under ~/.stack, and is using the stack.yaml file within it. Further experiments would show that this directory is used every time we invoke stack outside a project. Effectively, there’s a single ‘not-in-a-real-project’ project, though perhaps it should have been called global-noproject!

You might guess that this is where we’d find .stack-work too, and you’d be right:

Finally we come to the missing .cabal file. This shouldn’t really be a surprise, because it’s precisely the situation we had in the pre-stack era. This does mean though that there’s nowhere to specify dependencies, so we’ll have to manage them manually as we used to do with cabal. Whether this means there’s potential for stack global-project hell isn’t clear to me!

As an example, suppose we want to play with Data.Digits3 in ghci. This module is part of the digits package, which is included in stackage. However, just trying to import it fails:

At first I found the meaning of this rather confusing. Now I think of it as stack where to find .cabal files for packages outside of stackage:

in the project directory;

in the specified commit in the GitHub repo.

Specifying a particular commit in the repo seems to work better with respect to upgrades than just specifying the master tarball.

There’s also the extra_dep: true line, which means that stack should treat the GitHub location as a dependency i.e. more like the things we get from stackage rather than as a addition to the local project files.

Having added those lines to slack.yaml, it all just works.

The extra-project case

As discussed above, stack handles project-free files by effectively putting them into a special global-project. So, you might think that all you need to do is add the location to the global-project/slack.yaml file. You do!

What you must not do is also add the location: . line as well. If you do slack seems to think that global-project is just a normal project, and so tries to read a .cabal file. This fails and slack tells you, but I didn’t quite understand the issue5.