Continuous Builds in Haskell, Part 1

Haskell can be a very easy language to develop in, requiring few tools besides a compiler. I’m a crazy person and prefer to use a basic text editor to edit and a command line shell to compile. I find IDEs generally to not be worth the performance and stability overhead, especially with a powerful compiler like GHC. But, sometimes it’s nice to be assured that your code is correctly compiling (and running tests) in the background while you develop.

To achieve this, I’m going to show you how to build your own local, continuous build system for Haskell. It’s easier than you think.

This post applies to Linux users only, due to build dependencies. Hackage does not yet have a corresponding package for Windows.

This post also assumes a basic but passable familiarity with developing in Haskell. If you don’t know how to use cabal yet, this is probably a bit advanced for you.

Required Packages

First, you’ll want to get the hinotify package from Hackage. From a command line:

cabal update && cabal install hinotify

This is a lightweight package that wraps around the Linux Kernel’s inotify feature. It provides many capabilities for listening to events on a file handle, but we’re just going to follow the basics: Listening for writes that add, change, or remove files.

Now that that’s done, whip up a new file and start writing your Haskell:

Setting up our Imports

import System.Directory
import System.Environment (getArgs)
import System.INotify
import System.IO

Your mileage may vary, but the key piece is to import System.INotify. The rest is mainly for syntactic sugar for our example.

Managing the Watcher

Initializing an inotifier is pretty easy:

  n <- initINotify

To add a watch to an inotifier, you’ll need to specify a list of EventVariety‘s, the directory to watch, and a function to catch events:

  wd <- addWatch n  -- our inotifier
                 [ Modify, CloseWrite, Create, Delete, MoveIn, MoveOut ]  -- the EventVariety types to watch for
                 dir  -- the directory to monitor
                 eventHandler  -- the event handling function

We’ll need to remove our file watcher when we’re done with it:

  removeWatch wd

And don’t forget to close your inotifier before exiting:

  killINotify n

In production code, you’ll want to bracket both the inotifier and the watcher, to make sure all of our resources are closed properly.

Putting this all together in main (with some added boilerplate for basic I/O), we get:

main = do
  args <- getArgs
  let dir = head args
  putStrLn $ "Watching directory: " ++ dir
  n <- initINotify
  putStrLn "Press <Enter> to exit"
  print n
  wd <- addWatch n
                 [ Modify, CloseWrite, Create, Delete, MoveIn, MoveOut ]
                 dir
                 eventHandler
  print wd
  getLine
  removeWatch wd
  killINotify n

Handling File Events

For the event handler, we’ve set up a pretty simple pattern matched function. We only match against those Event‘s we’re interested in, discarding the rest. For the matching events, we’ll pass the event and the file path down to a filter function (more on that in a bit). We could just pass the file path itself for our simple little continuous builder, but we’ll use the event to monitor what kinds of messages we’re handling.

eventHandler :: Event -> IO ()
eventHandler x@(Modified _ (Just fp)) = handleFilteredFile x fp
eventHandler x@(MovedIn  _ fp _)      = handleFilteredFile x fp
eventHandler x@(MovedOut _ fp _)      = handleFilteredFile x fp
eventHandler x@(Created  _ fp)        = handleFilteredFile x fp
eventHandler x@(Deleted  _ fp)        = handleFilteredFile x fp
eventHandler _ = return ()

Filtering Events by File Type

You can filter file types in different ways. We’re going to keep this pretty basic: If the file ends in a .hs extension, let’s do some work on it, otherwise, let’s ignore it.

-- If the file is a .hs file, output the observed file event and do some work on
-- the file. Otherwise, just ignore it.
handleFilteredFile evt fp = do
  if filterHS fp
    then print evt >> doWork fp
    else return ()

-- Simply check that the file extension is ".hs"
filterHS fp = (== "hs")
            $ reverse
            $ takeWhile (/= '.')
            $ reverse fp

Do Some Work

Unless you prefer sipping mai tais on a beach, we’ll now need to do some work on our watched file. But for this post (and because we like sipping mai tais!) we’re just going to do nothing:

doWork :: FilePath -> IO ()
doWork fp = return ()

Wrapping up

That’s it! If you run this in ghci or as an executable, you’ll see events start firing as you move .hs files around or write changes to .hs files in your monitored directory. Just press <Enter> to exit the program.

In the next part, we’ll take a look at doing something useful with our file path input.

As always, the full cabal project is available on https://github.com/stormont/continuous-hs. To get the version used in this post, use the tag part-1.

Here’s the full code we’ve developed:

import System.Directory
import System.Environment (getArgs)
import System.INotify
import System.IO

main = do
  args <- getArgs
  let dir = head args
  putStrLn $ "Watching directory: " ++ dir
  n <- initINotify
  putStrLn "Press <Enter> to exit"
  print n
  wd <- addWatch n
                 [ Modify, CloseWrite, Create, Delete, MoveIn, MoveOut ]
                 dir
                 eventHandler
  print wd
  getLine
  removeWatch wd
  killINotify n

eventHandler :: Event -> IO ()
eventHandler x@(Modified _ (Just fp)) = handleFilteredFile x fp
eventHandler x@(MovedIn _ fp _) = handleFilteredFile x fp
eventHandler x@(MovedOut _ fp _) = handleFilteredFile x fp
eventHandler x@(Created _ fp) = handleFilteredFile x fp
eventHandler x@(Deleted _ fp) = handleFilteredFile x fp
eventHandler _ = return ()

handleFilteredFile evt fp = do
  if filterHS fp
    then print evt >> doWork fp
    else return ()

filterHS fp = (== "hs")
            $ reverse
            $ takeWhile (/= '.')
            $ reverse fp

doWork :: FilePath -> IO ()
doWork fp = return ()

One comment

  1. Pingback: Continuous Builds in Haskell, Part 2 | voyageintech