nh2
nh2

Reputation: 25705

How to access cabal/Setup.hs information from within my Haskell source files?

I need to use some Cabal-level information from within my Haskell .hs source files.

For example, obtaining the path to the dist/ build directory (configDistPref; I don't want to hardcode dist/), so that I can look up some contents in it using runIO in TemplateHaskell.

From normal Haskell files, I don't seem to be able to access Cabal-level information.

What ways are there for me to bring that information from a custom Setup.hs file into my Haskell source files?

Upvotes: 2

Views: 197

Answers (1)

nh2
nh2

Reputation: 25705

It seems Cabal and ghc have no out-of-the-box functionality from you to learn your dist/ directory or similar info. Here are some approaches to do it manually:

Solution 1: Serialising the entire Cabal information into a known place

One solution is shown by the cabal-toolkit library.

It gives you a hook modifier userHooksWithBuildInfo :: UserHooks -> UserHooks that you call in your Setup.hs, which serialises the LocalBuildInfo (which includes all important Cabal-level information, including ConfigFlags{ configDistPref }) into a dotfile (called .lbi.buildinfo) in the source directory.

It then provides a TemplateHaskell function localBuildInfoTypedQ :: Q (TExp LocalBuildInfo) which reads that file so that you can load the information in TH.

(I the Hackage version I linked above probably lacks an addDependentFile call here to notice when the serialised info changes, but the Github version already has a fix for it).


However, if you'd like temp files like this to end up in the dist/ directory, this also doesn't help (since as mentioned, the dotfile is in the toplevel of your project and you should add it e.g. to .gitignore).

Solution 2: Passing specific information to GHC using -D flags

Via a confHook, that calls cabal configure with "-ghc-options=-D__CPP_CABAL_DIST_DIR__=" ++ configDistPref. Example for your Setup.hs file:

main = do
  defaultMainWithHooks $
    simpleUserHooks
      { confHook = \inputs configFlags@ConfigFlags{ configDistPref = configDistPref } -> do
          putStrLn "In Cabal configure hook"

          let distDir = fromFlagOrDefault "dist" configDistPref

          (confHook simpleUserHooks) inputs (configFlags{ configProgramArgs = ("ghc", ["-D__CPP_CABAL_DIST_DIR__=" ++ distDir]) : configProgramArgs configFlags })
      }

Then the only difficulty is to actually splice the __CPP_CABAL_DIST_DIR__ macro into the code because you can't write "__CPP_CABAL_DIST_DIR__" in a Haskell String literal, that won't get substituted by CPP, so you have to use a string quasiquoter like this one, e.g. [r|__CPP_CABAL_DIST_DIR__] just to get that macro in; the usual way of doing CPP stringification with #__CPP_CABAL_DIST_DIR__ doesn't work in ghc's CPP, as explained here:

There are three main reasons why code with CPP might not work as expected:

You used #, ## or __VA_ARGS__. GHC runs CPP in traditional mode, which disables all advanced features.

Solution 3: Passing specific information by generating a Haskell module with a preprocessor

You can add a hookedPreProcessors to your hooks in Setup.hs. In this hook you render as Haskell literals or strings any information from Cabal that you want to access.

An example on how to write a preprocessor can be found here in the Cabal documentation, or even clearer, in this answer.

Don't forget to add the module to be generated by your pre-processor to the exposed-modules or other-modules in your .cabal file, and put a file matching the preprocessor extension into your project, otherwise the preprocessor will not create the .hs file.


For my own use cases, I'm using Solution 3 because it is clean, doesn't need any CPP or TemplateHaskell magic, gives good error messages and does not require the equivalent of runhaskell Setup.hs configure to run, build is enough because preprocessors are run at build time.

Upvotes: 1

Related Questions