May 5, 2021
Written by Artyom Kazak
This post describes a small pattern that lets us avoid bugs in production.
It is very old, and has been around since the days of configure scripts. However, Haskell packages usually don't have configure scripts, and Setup.hs is slowly dying.
First, some givens:
stack.yaml
or default.nix
file in the repository.stack.yaml
or default.nix
file.Now, you might already know that whatever you write in stack.yaml
won't be taken into account when somebody builds a package depending on your repository. So, if you ever wrote this:
# stack.yaml
extra-deps:
# Our fork of 'somelib' fixing X, Y, Z
- git: https://github.com/myorg/somelib
commit: ...
...you had better make sure that whoever depends on your repository will also use the somelib
fork. If you didn't know this — well, now you do.
There are a few obvious solutions to make sure everyone will use the right version of the library:
99999
and add a bound check. Breaks if somebody uses allow-newer: true
or doJailbreak
.README.md
. Eh.Instead, this is what I came up with: I include an extra module, SanityCheck.hs
, that does nothing other than verify that we have the right dependencies.
Let's say I hit a rare 'cryptonite' bug — AES breaks nondeterministically on some machines. It goes away when I disable AESNI. Luckily, cryptonite provides a Cabal flag to disable AESNI, but I can't use something like CPP to check build configuration of your dependencies — only the version numbers. So instead I do this:
{-# LANGUAGE TemplateHaskell #-}
module SanityCheck where
import Control.Monad
import Crypto.System.CPU
import Language.Haskell.TH
$(do
when (AESNI `elem` processorOptions) $
reportError $ unlines [
"cryptonite was compiled with Cabal flag support_aesni=true!",
" This can lead to nondeterministic runtime failures, see:",
" https://github.com/haskell-crypto/cryptonite/issues/329" ]
pure [])
Let's say I absolutely need to make sure that I use a particular fork of Aeson that doesn't output numbers in scientific notation. I do this:
import Data.Aeson
$(do
let hasDoubleFix = and [
encode (0.00000000001 :: Double) == "0.00000000001",
encode (1000000000000 :: Double) == "1000000000000",
encode (toJSON (0.00000000001 :: Double)) == "0.00000000001",
encode (toJSON (1000000000000 :: Double)) == "1000000000000" ]
unless hasDoubleFix $
reportError $ unlines [
"aeson does not have the Double encoding fix!",
" This will lead to bugs in production." ]
pure []
)
(Why do I check encode
with and without toJSON
? Because those are going to go via entirely different code paths, thanks to toEncoding
.)
Finally, let's say the library doesn't expose a list of feature flags (like cryptonite) and the bug is not easy to reproduce. In this case, I just make sure to always export a constant when I fork a library:
hasSuchAndSuchFix :: Bool
hasSuchAndSuchFix = True
This is the easiest case. We don't even need Template Haskell here — our package just would not compile if the constant is present.
Now I'm happy not having to watch others' repositories like a hawk.
Have anything to add? Write a comment on Reddit, or ping us on Twitter.