A report on stack script: the how and why...

Posted on February 2, 2020

Intro

Why stack script ?

If you share small, single module, self contained haskell examples, stack script gives us an easy way to get reproducible builds, by pinning the dependencies to a Stackage snapshot within a comment at the top of your Haskell code.
There are at least two additional motivations, besides reproducible builds, that you might want to use Stack’s scripting feature:

About

Stack is a build tool primarily designed for reproducible builds, done by specifying a resolver in a configuration file, usually your projects stack.yaml and package.yaml With Stack’s scripting feature, we still get reproducible builds by specifying a resolver, but move this specification to the file we are compiling, or as a command line argument. Therefore, for the sake of simplicity, we’ll assume that these scripts are run outside of a stack project, and stack is invoked in the same directory as the script file.
Note: When running a stack script inside of a stack project, it’s important to consider that stack will read settings from your project.yaml and stack.yaml, which may cause issues.

Code Examples

Outline

This article contains the following examples of using scripting with stack:

Basic example of stack script

For our first example, we’ll use stack to run a single file of Haskell source code as a script.

Here’s the source code we want to run, in a filed called simple.hs:

main :: IO ()
main = putStrLn "compiled & run"

To run this with the stack script interpreter, we can do the following:

$ stack script simple.hs --resolver lts-14.18

The resolver argument is mandatory, and Stack will compile and run the simple.hs file immediately after invocation using the lts-14.18 Stackage snapshot.
Alternatively, we can put all of the configuration information into the script itself, like this:

{- stack script 
 --resolver lts-14.18
-}
main :: IO ()
main = putStrLn "compiled & run"

which can be compiled and run with $ stack simple.hs.

A simple Servant server

The “killer feature” for scripting with stack is probably the ability to pull in packages without having to a stack.yaml or
This can probably be best seen with stack ghci, where the following command will drop you into a ghci repl where you have lens and text packages available from the specificied resolver.

stack ghci --package text --package lens --resolver lts-14.18

An example of this concept with the stack scripting engine, is a quick and dirty file server, explore.hs would be as follows:

~/projects/stack-script$ cat explore.hs
#!/usr/bin/env stack
{- stack script
 --resolver nightly-2019-12-22
 --install-ghc
 --package "servant-server warp"
 --ghc-options -Wall
-}
{-# LANGUAGE DataKinds, TypeOperators, TypeApplications #-}

module FileServer where

import Network.Wai.Handler.Warp( defaultSettings, runSettings, setBeforeMainLoop, setPort)
import Servant (Proxy(Proxy), Raw, serve, serveDirectoryWebApp)

main :: IO ()
main = runSettings settings . serve (Proxy @Raw) $ serveDirectoryWebApp "."
  where port = 8080
        msg = "serving on http://localhost:" ++ show port ++ "/{pathToFile}"
        settings = setPort port $ setBeforeMainLoop (putStrLn msg) defaultSettings

Noting a couple of features

On a fresh compilation, this will take a few minutes to run, as Stack needs to go and grab about 255Mb worth of source code in ~86 dependent packages, compile and link it in order for the above code to run. However, on subsequent runs, Stack can use a local cache of of the packages, and we can reproduce our project build without downloading and building all the dependencies!

Stack Script as a Bash Replacement

It’s possible to use haskell, and Stack scripting feature, along with the Turtle library as a drop in replacement for shell scripting!
To do this, we need the following at the top of our Haskell file:

#!/usr/bin/env stack
{- stack script
 --compile
 --copy-bins
 --resolver lts-14.17
 --install-ghc
 --package "turtle text foldl async"
 --ghc-options=-Wall
-}

This stack script does a couple of things:

With tutle, we get a portable way to to run external shell commands, and I was able to create a nice haskell program to replace the shell script I used to automate the server tasks needed to deploy this blog!
The basics my deploy turtle script are as follows, and you can see the full example on github here

import qualified Turtle as Tu
import qualified Control.Foldl as L
import qualified Data.Text as T
import Control.Concurrent.Async
import System.IO

argParser :: Tu.Parser Tu.FilePath
argParser = Tu.argPath "html" "html destination directory"

main :: IO ()
main = do
  -- 53 files copied over into destinationDir
  hSetBuffering stdout NoBuffering
  destinationDir <- Tu.options "Build blog and copy to directory" argParser
  Tu.with (Tu.mktempdir "/tmp" "deploy") (mainLoop destinationDir)

One nice thing about turtle is the Tu.with function, which lets use run our the main logic of our program with a temporary directory which is subsequently cleaned up after the mainLoop function returns.
Despite turtle being a handy library, I did find some downsides - Use of FilePath, which uses a pretty clunky, String based file representation - Often times clunkier semantics than just writing bash: for instance, cp -r SRC TRG is requires a fold over the result of ls SRC and construction of an explicit cp with each file, instead, you need to use cptree, which took me a while to figure out, so it would be nice if the semantics matched better! - Turtle is a monolithic framework for interacting with OS through a set of mirrored shell commands trying to match coreutiles, and it’s tightly couple parts makes it not very easy to pick the parts you like, and disregard the rest!

Using stack script to run ghci

We’ve already seen a few examples of stack script, but there is one more that should be in every Haskeller’s toolkit. Stack script can be used to launch a ghci repl. Let’s say we are working with a new ADT, and want to write a new QuickCheck instance, how can stack script help us?
The following header will load the listed packages into a ghci repl:

{- stack 
 --resolver nightly
 --install-ghc
 exec ghci
 --package "QuickCheck checkers"
-}
module XTest where

There is one note to make here about the order of the arguments:

ghcid

You can run the above stack script with ghcid to get nearly instant compiler feedback using the following:

bash$ ghcid -c "stack XTest.hs"

Conclusion

I often find myself coding up small Haskell snippets, whether it’s playing around with a new data type, trying out a library, or reproducing an example from a paper or a book. In these cases, Stack’ scripting feature shines at giving me a self contained file where I can specify the dependencies via a snapshot in the file header, and not have to worry about breaking changes, or setting up a project with all the correct dependencies. Thus, I would urge my fellow Haskellers to consider using stack’s scripting feature when they share code online, to help others run their code today, and keep that way far into the future!

Additional Information