2

I'm working on a project that's chosen CMake as its build tool. The project is made up of several executables and since a few months back a few of them are written in Haskell. We strongly wish all executables to show the same version number when called as foo --version. Ideally that version should be recorded in one place, and ideally that place should be the top-level CMakeLists.txt (this is where the source for all the other executables get it, via the use of CMake's configure_file function).

Is there some nice way of achieving this?

Some extra information that might be useful:

  • The source for each executable lives in its own dir, with its own Cabal file.
  • We use stack to build, and there is a single stack.yaml file that points to all directories with Haskell code.
Magnus
  • 4,644
  • 1
  • 33
  • 49
  • 1
    I'm not too familiar with CMake, but based on what the docs say about `configure_file`, it should work for Haskell as well, in much the same way you do it in C. You can have a field like `cpp-options: -DLIB_VERSION="${LIB_VERSION}"` (where `${LIB_VERSION}` is whichever variable identifies the version) in the prototype of your .cabal file, and then have `libVersion :: String; libVersion = LIB_VERSION` in some Haskell source file (in which `-XCPP` is enabled). You can do the same in the stack.yaml file with e.g. `ghc-options: "$everything": -D..`, which might be easier if you have many packages. – user2407038 Aug 31 '17 at 08:46
  • The issue with `configure_file`, as I see it, is that CMake likes to split building into a separate dir so the generated files end up out-of-source. This, unfortunately, isn't always so easy to deal with in Cabal+stack. Using CPP is something I didn't think of though, I'll look into passing the information via the build command instead of via a generated file. – Magnus Sep 01 '17 at 06:49
  • "CMake likes to split building into a separate dir so the generated files end up out-of-source" - I'm not sure I understand - can you not tell CMake to put a generated file in a particular place (the place being wherever stack/cabal expect their config files to be, i.e. at the root of the project source)? And yes, another option is to just pass `--cpp-options=-D..` on the command line to stack/cabal, which saves you from having to generate `stack.yaml` or `*.cabal` with configure_file - I guess depending on your particular setup, this could be easier or harder. – user2407038 Sep 01 '17 at 08:02
  • Yes, I can tell `configure_file` exactly where to put things, but I'd rather not go against the convention and put generated files in-source (it's already a point of contention that _stack_ leaves stuff in-source when building). Also, I kind of like that my Haskell bits are complete after check-out, i.e. I don't have to remember running CMake to generate missing bits before opening Haskell source in my editor (which integrates with intero/stack). I know, both reasons aren't very good but a solution that fulfils them both would be nice. – Magnus Sep 01 '17 at 08:09
  • I think these are fine reasons (not wanting to interrupt your workflow, and not placing the undue burden of an additional dependency of your users for the purpose of a somewhat minor feature). Thanks for explaining! I suggest passing `-cpp-options=-D..` on the command line in the CMake build, and having `version :: Maybe String; #ifdef VERSION; version = Just VERSION; #else; version = Nothing` - this allows the package to be built without CMake (but then of course you have no knowledge of the version - but I think that's the best you can do). – user2407038 Sep 01 '17 at 08:22
  • Yes, that's basically what I just checked in :) I'll write a proper answer as soon as our CI is happy with the changes. There are a few details that might be worth documenting in a proper answer. – Magnus Sep 01 '17 at 08:32

1 Answers1

1

I thought I'd document the solution I've landed on.

the source changes

I added the CPP language extension and use a macro (VERSION) to chose between a version provided as a CPP macro and the version provided by Cabal:

{-# LANGUAGE CPP #-}
module Main where

import           Data.Version
import           Text.ParserCombinators.ReadP
.... -- other imports

#ifdef VERSION
#define xstr(s) str(s)
#define str(s) #s
version = fst $ last $ readP_to_S parseVersion xstr(VERSION)
#else
import Paths_<Cabal project name> (version)
#endif

The need for the double-expansion (xstr and str) is explained in the answer to another question.

building

The above code will unfortunately not build with a simple stack build command. This apparently has to do with the default CPP that ghc uses (/usr/bin/gcc) and the flags it passes to it (as I understand it the culprit is -traditional). The solution is to tell ghc to use GNU CPP:

stack build --ghc-options "-pgmP=/usr/bin/cpp -DVERSION=1.2.3"

or as I put it in my CMakeLists.txt (I use ExternalProject to integrate stack into our CMake-based build):

ExternalProject_Add(haskell-bits
...
BUILD_COMMAND cd <SOURCE_DIR>
  && ${HaskellStack_EXE} --local-bin-path <BINARY_DIR> --install-ghc
         install --ghc-options "-pgmP=/usr/bin/cpp -DVERSION=${<CMake proj name>_VERSION}"
...
)
Magnus
  • 4,644
  • 1
  • 33
  • 49