packages feed

hoppy-runtime-0.9.0: src/Foreign/Hoppy/Setup.hs

-- This file is part of Hoppy.
--
-- Copyright 2015-2024 Bryan Gardiner <bog@khumba.net>
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
--     http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.

{-# LANGUAGE CPP #-}

-- | Implementations of Cabal setup programs for use in packages of generated
-- bindings.
--
-- Much like the default Setup.hs that Cabal recommends for packages,
--
-- @
-- import "Distribution.Simple"
-- main = 'Distribution.Simple.defaultMain'
-- @
--
-- this module provides simplified configuration of packages for generated
-- bindings.  Suppose you have a project named foobar that is composed of Cabal
-- packages named \"foobar-generator\" for the code generator, \"foobar-cpp\"
-- for the C++ gateway, and \"foobar\" for the Haskell gateway.  The C++ gateway
-- package can use the following code:
--
-- @
-- import "Foreign.Hoppy.Setup" ('ProjectConfig' (..), 'cppMain')
--
-- main =
--   cppMain $
--   'ProjectConfig'
--   { generatorExecutableName = \"foobar-generator\"
--   , cppPackageName = \"foobar-cpp\"
--   , cppPackagedSourcesLocation = Nothing
--   , cppSourcesDir = 'GenerateInAutogenDir' \"\"
--   , hsSourcesDir = 'GenerateInAutogenDir' \"\"
--   }
-- @
--
-- The Haskell gateway uses the same code, except calling 'hsMain' instead of
-- 'cppMain'.  This causes all (C++, Haskell) generated sources to be placed in
-- the \"autogen\" directories provided by Cabal, which keeps the source
-- directory clean.  See the documentation of the fields of 'ProjectConfig' for
-- more information on how to set up your project's build process.
--
-- The gateway packages need to set @build-type: Custom@ in their @.cabal@ files
-- to use these setup files.
module Foreign.Hoppy.Setup (
  ProjectConfig (..),
  GenerateLocation (..),
  combinedMain,
  combinedUserHooks,
  cppMain,
  cppUserHooks,
  hsMain,
  hsUserHooks,
  ) where

import Control.Monad (unless, when)
import Data.List (isInfixOf)
import qualified Data.Map as M
import Distribution.InstalledPackageInfo (libraryDirs)
import qualified Distribution.ModuleName as ModuleName
#if MIN_VERSION_Cabal(2,0,0)
import Distribution.Package (mkPackageName)
#else
import Distribution.Package (PackageName (PackageName))
#endif
import Distribution.PackageDescription (
  HookedBuildInfo,
  PackageDescription,
  autogenModules,
  emptyBuildInfo,
  extraLibDirs,
  )
import Distribution.Simple (defaultMainWithHooks, simpleUserHooks)
import Distribution.Simple.BuildPaths (autogenComponentModulesDir)
import Distribution.Simple.LocalBuildInfo (
  LocalBuildInfo,
  absoluteInstallDirs,
  buildDir,
  installedPkgs,
  libdir,
  withPrograms,
  )
import Distribution.Simple.PackageIndex (lookupPackageName)
import Distribution.Simple.Program (
  runDbProgram,
  runProgram,
  simpleProgram,
  )
import Distribution.Simple.Program.Find (
  ProgramSearchPathEntry (ProgramSearchPathDefault),
  findProgramOnSearchPath,
  )
import Distribution.Simple.Program.Types (
  ConfiguredProgram,
  Program,
  ProgramLocation (FoundOnSystem),
  simpleConfiguredProgram,
  )
import Distribution.Simple.Setup (
  CopyDest (CopyTo, NoCopyDest),
  RegisterFlags,
  buildVerbosity,
  cleanVerbosity,
  configVerbosity,
  copyDest,
  copyVerbosity,
  flagToMaybe,
  fromFlagOrDefault,
  installDistPref,
  installVerbosity,
  regInPlace,
  regVerbosity,
  replVerbosity,
  testVerbosity,
  )
import Distribution.Simple.UserHooks (
  UserHooks (
    buildHook,
    hookedPrograms,
    cleanHook,
    copyHook,
    instHook,
    regHook,
    postConf,
    preBuild,
    preCopy,
    preInst,
    preReg,
    preRepl,
    preTest
    ),
  )
#if MIN_VERSION_Cabal(2,0,0)
import Distribution.Simple.Utils (die')
#else
import Distribution.Simple.Utils (die)
#endif
import Distribution.Simple.Utils (installOrdinaryFile)
import Distribution.Types.ComponentName (ComponentName (CLibName))
#if MIN_VERSION_Cabal(3,0,0)
import Distribution.Types.LibraryName (LibraryName (LMainLibName))
#endif
import Distribution.Types.LocalBuildInfo (componentNameCLBIs)
import Distribution.Verbosity (Verbosity, normal)
import Foreign.Hoppy.Generator.Language.Haskell (getModuleName)
import Foreign.Hoppy.Generator.Main (run)
import Foreign.Hoppy.Generator.Spec (Interface, interfaceModules)
import System.Directory (createDirectoryIfMissing, doesFileExist, getCurrentDirectory)
import System.FilePath ((</>), takeDirectory)

-- | Configuration parameters for a project using Hoppy.
data ProjectConfig = ProjectConfig
  { interfaceResult :: Either String Interface
    -- ^ The interface to run the generator with.  This result is returned from
    -- @Foreign.Hoppy.Generator.Spec.interface@ and some may have failed; the
    -- string is a message indicating the problem.

  , cppPackageName :: String
    -- ^ The name of the C++ gateway package.
    --
    -- When using separate Cabal packages for the C++ and Haskell gateway
    -- packages, this must be nonempty.  When using a single combined gateway
    -- package with 'combinedMain', this must be empty.

  , cppPackagedSourcesLocation :: Maybe FilePath
    -- ^ If the C++ gateway package includes C++ files needed for compliation
    -- (either sources or headers), then this should point to the base directory
    -- holding these files, relative to the root of the project.  The project
    -- root itself may be specified with @Just \"\"@.
    --
    -- When present, this is passed to the C++ package's makefile in the
    -- environment variable @HOPPY_CPP_PKG_DIR@.  A value of @Just \"\"@ is
    -- passed with @HOPPY_CPP_PKG_DIR@ set to the base directory of the Cabal
    -- package (equivalent to @Just \".\"@).
    --
    -- This is also added automatically as a system include path (i.e. @gcc -I@)
    -- for the C++ compiler when compiling the test program for enum
    -- autodetection.

  , cppGeneratedSourcesLocation :: GenerateLocation
    -- ^ Specifies the directory where C++ sources will be generated.
    --
    -- This is passed to the C++ package's makefile in the environment variable
    -- @HOPPY_CPP_GEN_DIR@.
  }

-- | Where to generate sources for binding packages.
data GenerateLocation =
  GenerateInAutogenDir FilePath
  -- ^ Generate sources in the package's autogen directory provided by Cabal.
  -- This is preferrable as it keeps the source directory clean.
  --
  -- Sources are generated below the given @FilePath@ if nonempty, otherwise
  -- sources are generated directly in the autogen directory.
  | GenerateInSourcesDir FilePath
  -- ^ Generate sources in the package's root source directory, i.e. the
  -- directory with the @.cabal@ file.
  --
  -- Sources are generated below the given @FilePath@ if nonempty, otherwise
  -- sources are generated directly in the root directory.

-- | The name of the file we'll use to hold the enum evaluation cache.
enumEvalCacheFileName :: FilePath
enumEvalCacheFileName = "hoppy-enum-eval-cache"

-- | Extracts the 'Interface' from a 'ProjectConfig', checking its
-- 'interfaceResult' and aborting the program if the result is unsuccessful.
getInterface :: ProjectConfig -> Verbosity -> IO Interface
getInterface project verbosity = case interfaceResult project of
  Left errorMsg ->
#if MIN_VERSION_Cabal(2,0,0)
    die' verbosity $
#else
    die $
#endif
    "Error initializing interface: " ++ errorMsg
  Right iface -> return iface

-- | A @main@ implementation to be used in the @Setup.hs@ of a single Hoppy
-- binding package that combined both the C++ and Haskell gateway code in one
-- package.
--
-- @combinedMain project = 'defaultMainWithHooks' $ 'combinedUserHooks' project@
combinedMain :: ProjectConfig -> IO ()
combinedMain project = defaultMainWithHooks $ combinedUserHooks project

-- | Cabal user hooks for a combined gateway package.  When overriding
-- overriding fields in the result, be sure to call the previous hook.
--
-- The following hooks are defined:
--
-- - 'postConf': Runs the generator to generate C++ and Haskell sources.
combinedUserHooks :: ProjectConfig -> UserHooks
combinedUserHooks project =
  simpleUserHooks
  { postConf = \args flags pkgDesc localBuildInfo -> do
      let verbosity = fromFlagOrDefault normal $ configVerbosity flags

      when (not $ null $ cppPackageName project) $
        die' verbosity $
        "combinedMain expects an empty cppPackageName, found \"" ++
        cppPackageName project ++ "\"."

      iface <- case interfaceResult project of
        Left errorMsg ->
          die' verbosity $
          "Error initializing interface: " ++ errorMsg
        Right iface -> return iface

      genCpp project verbosity localBuildInfo iface
      genHs verbosity localBuildInfo iface
      postConf simpleUserHooks args flags pkgDesc localBuildInfo

  , preBuild = \_ flags -> myHsPreHook project (fromFlagOrDefault normal $ buildVerbosity flags)
  , preTest = \_ flags -> myHsPreHook project (fromFlagOrDefault normal $ testVerbosity flags)
  , preCopy = \_ flags -> myHsPreHook project (fromFlagOrDefault normal $ copyVerbosity flags)
  , preInst = \_ flags -> myHsPreHook project (fromFlagOrDefault normal $ installVerbosity flags)
  , preReg = \_ flags -> myHsPreHook project (fromFlagOrDefault normal $ regVerbosity flags)
  , preRepl = \_ flags -> myHsPreHook project (fromFlagOrDefault normal $ replVerbosity flags)
  }

  where genCpp :: ProjectConfig -> Verbosity -> LocalBuildInfo -> Interface -> IO ()
        genCpp project verbosity localBuildInfo iface = do
          (_, cppGenDir) <-
            getAutogenAndCppGenDir project verbosity localBuildInfo
          cppPackagedSourcesDir <- case cppPackagedSourcesLocation project of
            Nothing -> return ""
            Just subpath -> fmap (</> subpath) getCurrentDirectory
          createDirectoryIfMissing True cppGenDir
          createDirectoryIfMissing True $ buildDir localBuildInfo
          _ <- run [iface]
                   [ "--enum-eval-cache-mode", "refresh"
                   , "--enum-eval-cache-path", buildDir localBuildInfo </> enumEvalCacheFileName
                   , "--gen-cpp", cppGenDir, cppPackagedSourcesDir
                   ]
          return ()

        genHs :: Verbosity -> LocalBuildInfo -> Interface -> IO ()
        genHs verbosity localBuildInfo iface = do
          (_, hsGenDir) <- getAutogenAndHsGenDir verbosity localBuildInfo
          createDirectoryIfMissing True hsGenDir
          _ <- run [iface]
                   [ "--enum-eval-cache-mode", "must-exist"
                   , "--enum-eval-cache-path", buildDir localBuildInfo </> enumEvalCacheFileName
                   , "--gen-hs", hsGenDir
                   ]
          return ()

        -- See also hsPreHook.
        myHsPreHook :: ProjectConfig -> Verbosity -> IO HookedBuildInfo
        myHsPreHook project verbosity = do
          iface <- getInterface project verbosity
          let moduleNames =
                map (ModuleName.fromString . getModuleName iface) $
                M.elems (interfaceModules iface)

          -- Injected autogenerated modules here works for most commands, however sdist
          -- is not hookable, so autogen-modules should still be written out manually in
          -- your Cabal file!  (See Cabal issue #6180.)
          return (Just emptyBuildInfo { autogenModules = moduleNames }, [])

-- | A @main@ implementation to be used in the @Setup.hs@ of a C++ gateway
-- package.
--
-- @cppMain project = 'defaultMainWithHooks' $ 'cppUserHooks' project@
cppMain :: ProjectConfig -> IO ()
cppMain project = defaultMainWithHooks $ cppUserHooks project

-- | Cabal user hooks for a C++ gateway package.  When overriding fields in the
-- result, be sure to call the previous hook.
--
-- The following hooks are defined:
--
-- - 'postConf': Runs the generator program to generate C++ sources.  Checks if
-- a @configure@ script exists in the C++ gateway root, and calls it if so
-- (without arguments).
--
-- - 'buildHook': Runs @make@ with no arguments from the C++ gateway root.
--
-- - 'copyHook' and 'instHook': Runs @make install libdir=$libdir@ where
-- @$libdir@ is the directory into which to install the built shared library.
--
-- - 'cleanHook': Removes files created by the generator, then calls @make
-- clean@.
cppUserHooks :: ProjectConfig -> UserHooks
cppUserHooks project =
  simpleUserHooks
  { hookedPrograms = [makeProgram]

  , postConf = \args flags pkgDesc localBuildInfo -> do
      let verbosity = fromFlagOrDefault normal $ configVerbosity flags
      cppConfigure project verbosity localBuildInfo
      postConf simpleUserHooks args flags pkgDesc localBuildInfo

  , buildHook = \pkgDesc localBuildInfo hooks flags -> do
      buildHook simpleUserHooks pkgDesc localBuildInfo hooks flags
      let verbosity = fromFlagOrDefault normal $ buildVerbosity flags
      cppBuild project verbosity localBuildInfo

  , copyHook = \pkgDesc localBuildInfo hooks flags -> do
      copyHook simpleUserHooks pkgDesc localBuildInfo hooks flags
      let verbosity = fromFlagOrDefault normal $ copyVerbosity flags
          dest = fromFlagOrDefault NoCopyDest $ copyDest flags
      cppInstall project verbosity pkgDesc localBuildInfo dest

  , instHook = \pkgDesc localBuildInfo hooks flags -> do
      instHook simpleUserHooks pkgDesc localBuildInfo hooks flags
      let verbosity = fromFlagOrDefault normal $ installVerbosity flags
          dest = maybe NoCopyDest CopyTo $
                 flagToMaybe $ installDistPref flags
      cppInstall project verbosity pkgDesc localBuildInfo dest

  , regHook = \pkgDesc localBuildInfo hooks flags -> do
      regHook simpleUserHooks pkgDesc localBuildInfo hooks flags
      let verbosity = fromFlagOrDefault normal $ regVerbosity flags
      cppRegister project verbosity localBuildInfo flags

  , cleanHook = \pkgDesc z hooks flags -> do
      cleanHook simpleUserHooks pkgDesc z hooks flags
      let verbosity = fromFlagOrDefault normal $ cleanVerbosity flags
      cppClean project verbosity
  }

makeProgram :: Program
makeProgram = simpleProgram "make"

defaultLibComponentName :: ComponentName
defaultLibComponentName =
#if MIN_VERSION_Cabal(3,0,0)
  CLibName LMainLibName
#else
  CLibName
#endif

-- | Locates the autogen directory for the library component.
getAutogenDir :: Verbosity -> LocalBuildInfo -> IO FilePath
getAutogenDir verbosity localBuildInfo = do
  let libCLBIs = componentNameCLBIs localBuildInfo defaultLibComponentName
#if MIN_VERSION_Cabal(2,0,0)
      dieFn = die' verbosity
#else
      dieFn = die
#endif

  case libCLBIs of
    [libCLBI] -> return $ autogenComponentModulesDir localBuildInfo libCLBI
    _ ->
      -- TODO Show interface name, and "C++" or "Haskell"?
      dieFn $ concat
      ["Expected one library ComponentLocalBuildInfo, found ",
       show $ length libCLBIs, "."]

getAutogenAndGenDir :: GenerateLocation
                    -> Verbosity
                    -> LocalBuildInfo
                    -> IO (FilePath, FilePath)
getAutogenAndGenDir genLoc verbosity localBuildInfo = do
  autogenDir <- getAutogenDir verbosity localBuildInfo
  case genLoc of
    GenerateInAutogenDir subpath ->
      return (autogenDir, autogenDir </> subpath)
    GenerateInSourcesDir subpath -> do
      curDir <- getCurrentDirectory
      return (autogenDir, curDir </> subpath)

getAutogenAndCppGenDir :: ProjectConfig -> Verbosity -> LocalBuildInfo -> IO (FilePath, FilePath)
getAutogenAndCppGenDir project verbosity localBuildInfo =
  getAutogenAndGenDir (cppGeneratedSourcesLocation project) verbosity localBuildInfo

getAutogenAndHsGenDir :: Verbosity -> LocalBuildInfo -> IO (FilePath, FilePath)
getAutogenAndHsGenDir verbosity localBuildInfo =
  getAutogenAndGenDir (GenerateInAutogenDir "") verbosity localBuildInfo

getCppDirEnvVars :: ProjectConfig -> Verbosity -> LocalBuildInfo -> IO [String]
getCppDirEnvVars project verbosity localBuildInfo = do
  (autogenDir, cppGenDir) <- getAutogenAndCppGenDir project verbosity localBuildInfo
  return $
    [ "HOPPY_AUTOGEN_DIR=" ++ autogenDir
    , "HOPPY_CPP_GEN_DIR=" ++ cppGenDir
    ] ++
    (case cppPackagedSourcesLocation project of
      Just "" -> ["HOPPY_CPP_PKG_DIR=."]
      Just subpath -> ["HOPPY_CPP_PKG_DIR=" ++ subpath]
      Nothing -> [])

cppConfigure :: ProjectConfig -> Verbosity -> LocalBuildInfo -> IO ()
cppConfigure project verbosity localBuildInfo = do
  -- Invoke the generator to create C++ code.
  (_, cppGenDir) <- getAutogenAndCppGenDir project verbosity localBuildInfo
  cppPackagedSourcesDir <- case cppPackagedSourcesLocation project of
    Nothing -> return ""
    Just subpath -> fmap (</> subpath) getCurrentDirectory
  iface <- getInterface project verbosity
  createDirectoryIfMissing True cppGenDir
  createDirectoryIfMissing True $ buildDir localBuildInfo
  _ <- run [iface]
           [ "--enum-eval-cache-mode", "refresh"
           , "--enum-eval-cache-path", buildDir localBuildInfo </> enumEvalCacheFileName
           , "--gen-cpp", cppGenDir, cppPackagedSourcesDir
           ]

  -- When there is a configure script, then run it.
  maybeConfigureProgram <- findConfigure
  case maybeConfigureProgram of
    Just configureProgram -> runProgram verbosity configureProgram []
    Nothing -> return ()

  where findConfigure :: IO (Maybe ConfiguredProgram)
        findConfigure = do
          hasConfigure <- doesFileExist "configure"
          return $ if hasConfigure
                   then Just $ simpleConfiguredProgram "configure" $ FoundOnSystem "./configure"
                   else Nothing

cppBuild :: ProjectConfig -> Verbosity -> LocalBuildInfo -> IO ()
cppBuild project verbosity localBuildInfo = do
  hasMakefile <- doesFileExist "Makefile"
  unless hasMakefile $
#if MIN_VERSION_Cabal(2,0,0)
    die' verbosity
#else
    die
#endif
    "No Makefile found."

  cppDirEnvVars <- getCppDirEnvVars project verbosity localBuildInfo

  let programDb = withPrograms localBuildInfo
  runDbProgram verbosity makeProgram programDb cppDirEnvVars

cppInstall :: ProjectConfig
           -> Verbosity
           -> PackageDescription
           -> LocalBuildInfo
           -> CopyDest
           -> IO ()
cppInstall project verbosity pkgDesc localBuildInfo dest = do
  hasMakefile <- doesFileExist "Makefile"
  unless hasMakefile $
#if MIN_VERSION_Cabal(2,0,0)
    die' verbosity
#else
    die
#endif
    "No Makefile found."
  let programDb = withPrograms localBuildInfo
      libDir = libdir $ absoluteInstallDirs pkgDesc localBuildInfo dest
  createDirectoryIfMissing True libDir
  cppDirEnvVars <- getCppDirEnvVars project verbosity localBuildInfo
  runDbProgram verbosity makeProgram programDb $
    ["install", "libdir=" ++ libDir] ++ cppDirEnvVars

  -- We're doing an old-style install, so copy the enum eval cache file from the
  -- build directory to the library directory where the Haskell side of the
  -- bindings will find it.
  let enumEvalCacheFilePath = buildDir localBuildInfo </> enumEvalCacheFileName
  enumEvalCacheExists <- doesFileExist enumEvalCacheFilePath
  when enumEvalCacheExists $
    installOrdinaryFile verbosity
                        enumEvalCacheFilePath
                        (libDir </> enumEvalCacheFileName)

cppRegister :: ProjectConfig -> Verbosity -> LocalBuildInfo -> RegisterFlags -> IO ()
cppRegister project verbosity localBuildInfo flags = do
  hasMakefile <- doesFileExist "Makefile"
  unless hasMakefile $
#if MIN_VERSION_Cabal(2,0,0)
    die' verbosity
#else
    die
#endif
    "No Makefile found."
  when (fromFlagOrDefault False (regInPlace flags)) $ do
    let programDb = withPrograms localBuildInfo
        libDir = buildDir localBuildInfo
    createDirectoryIfMissing True libDir
    cppDirEnvVars <- getCppDirEnvVars project verbosity localBuildInfo
    runDbProgram verbosity makeProgram programDb $
      ["install", "libdir=" ++ libDir] ++ cppDirEnvVars

cppClean :: ProjectConfig -> Verbosity -> IO ()
cppClean project verbosity = do
  iface <- getInterface project verbosity
  -- We can remove generated sources if we wrote them to the source directory.
  -- We don't have access to the autogen directory from this hook though.
  -- Although, Cabal ought to remove the autogen directory already when
  -- cleaning...
  case cppGeneratedSourcesLocation project of
    GenerateInAutogenDir _ -> return ()
    GenerateInSourcesDir subpath -> do
      cppPackagedSourcesDir <- case cppPackagedSourcesLocation project of
        Nothing -> return ""
        Just subpath -> fmap (</> subpath) getCurrentDirectory
      _ <- run [iface] ["--clean-cpp", subpath, cppPackagedSourcesDir]
      return ()

  hasMakefile <- doesFileExist "Makefile"
  unless hasMakefile $
#if MIN_VERSION_Cabal(2,0,0)
    die' verbosity
#else
    die
#endif
    "No Makefile found."
  make <- findSystemProgram verbosity "make"
  runProgram verbosity make ["clean"]  -- No Hoppy directories passed in here!

findSystemProgram :: Verbosity -> FilePath -> IO ConfiguredProgram
findSystemProgram verbosity basename = do
  maybePath <-
#if MIN_VERSION_Cabal(1,24,0)
    fmap (fmap fst) $  -- We don't care about failed search paths.
#endif
    findProgramOnSearchPath verbosity [ProgramSearchPathDefault] basename
  case maybePath of
    Just path -> return $ simpleConfiguredProgram basename $ FoundOnSystem path
    Nothing ->
#if MIN_VERSION_Cabal(2,0,0)
      die' verbosity $
#else
      die $
#endif
      "Couldn't find program " ++ show basename ++ "."

-- | A @main@ implementation to be used in the @Setup.hs@ of a Haskell gateway
-- package.
--
-- @hsMain project = 'defaultMainWithHooks' $ 'hsUserHooks' project@
hsMain :: ProjectConfig -> IO ()
hsMain project = defaultMainWithHooks $ hsUserHooks project

-- | Cabal user hooks for a Haskell gateway package.  When overriding fields in
-- the result, be sure to call the previous hook.
--
-- The following hooks are defined:
--
-- - 'postConf': Finds the shared library directory for the installed C++
-- gateway package, and writes this path to a @dist\/build\/hoppy-cpp-libdir@
-- file.  Runs the generator program to generate Haskell sources.
--
-- - 'preBuild', 'preTest', 'preCopy', 'preInst', 'preReg': Reads the C++
-- library directory from @dist\/build\/hoppy-cpp-libdir@ and adds it to the
-- library search path ('extraLibDirs').
--
-- - 'cleanHook': Removes files created by the generator.
hsUserHooks :: ProjectConfig -> UserHooks
hsUserHooks project =
  simpleUserHooks
  { postConf = \args flags pkgDesc localBuildInfo -> do
      postConf simpleUserHooks args flags pkgDesc localBuildInfo
      let verbosity = fromFlagOrDefault normal $ configVerbosity flags
      hsConfigure project verbosity localBuildInfo

  , preBuild = \_ flags -> hsPreHook project (fromFlagOrDefault normal $ buildVerbosity flags)
  , preTest = \_ flags -> hsPreHook project (fromFlagOrDefault normal $ testVerbosity flags)
  , preCopy = \_ flags -> hsPreHook project (fromFlagOrDefault normal $ copyVerbosity flags)
  , preInst = \_ flags -> hsPreHook project (fromFlagOrDefault normal $ installVerbosity flags)
  , preReg = \_ flags -> hsPreHook project (fromFlagOrDefault normal $ regVerbosity flags)
  , preRepl = \_ flags -> hsPreHook project (fromFlagOrDefault normal $ replVerbosity flags)
  }

hsCppLibDirFile :: FilePath
hsCppLibDirFile = "dist/build/hoppy-cpp-libdir"

hsConfigure :: ProjectConfig -> Verbosity -> LocalBuildInfo -> IO ()
hsConfigure project verbosity localBuildInfo = do
  iface <- getInterface project verbosity
  libDir <- lookupCppLibDir
  storeCppLibDir libDir
  generateSources iface libDir

  where lookupCppLibDir = do
          -- Look for an installed -cpp package.
          let packageName = cppPackageName project
          cppPkg <- case lookupPackageName (installedPkgs localBuildInfo) $
#if MIN_VERSION_Cabal(2,0,0)
                         mkPackageName packageName
#else
                         PackageName packageName
#endif
                         of
            [(_, [pkg])] -> return pkg
            results ->
#if MIN_VERSION_Cabal(2,0,0)
              die' verbosity $
#else
              die $
#endif
              "Failed to find a unique " ++ packageName ++
              " installation.  Found " ++ show results ++ "."

          -- Look up the libdir of the package we found.  The filter here is for NixOS,
          -- where libraryDirs includes the library directories of dependencies as well.
          case filter (\x -> packageName `isInfixOf` x) $ libraryDirs cppPkg of
            [libDir] -> return libDir
            libDirs ->
#if MIN_VERSION_Cabal(2,0,0)
              die' verbosity $
#else
              die $
#endif
              "Expected a single library directory for " ++ packageName ++
              ", got " ++ show libDirs ++ "."

        storeCppLibDir libDir = do
          createDirectoryIfMissing True $ takeDirectory hsCppLibDirFile
          writeFile hsCppLibDirFile libDir

        generateSources iface libDir = do
          (_, hsGenDir) <- getAutogenAndHsGenDir verbosity localBuildInfo
          createDirectoryIfMissing True hsGenDir
          _ <- run [iface]
                   [ "--enum-eval-cache-mode", "must-exist"
                   , "--enum-eval-cache-path", libDir </> enumEvalCacheFileName
                   , "--gen-hs", hsGenDir
                   ]
          return ()

-- See also myHsPreHook.
hsPreHook :: ProjectConfig -> Verbosity -> IO HookedBuildInfo
hsPreHook project verbosity = do
  iface <- getInterface project verbosity
  let moduleNames =
        map (ModuleName.fromString . getModuleName iface) $
        M.elems (interfaceModules iface)

  libDir <- readFile hsCppLibDirFile

  -- Injected autogenerated modules here works for most commands, however sdist
  -- is not hookable, so autogen-modules should still be written out manually in
  -- your Cabal file!  (See Cabal issue #6180.)
  return (Just emptyBuildInfo
          { autogenModules = moduleNames
          , extraLibDirs = [libDir]
          }
         , [])