packages feed

sbv-14.8: SBVTestSuite/Utils/CCodeGen.hs

-----------------------------------------------------------------------------
-- |
-- Module    : Utils.CCodeGen
-- Copyright : (c) Levent Erkok
-- License   : BSD3
-- Maintainer: erkokl@gmail.com
-- Stability : experimental
--
-- Shared dependency discovery for generated-C integration tests.
-----------------------------------------------------------------------------

{-# LANGUAGE ScopedTypeVariables #-}

{-# OPTIONS_GHC -Wall -Werror #-}

module Utils.CCodeGen (locateLibBF, generatedMakeOptions) where

import Control.Exception (IOException, catch)
import Control.Concurrent.MVar (MVar, modifyMVar, newMVar)
import Data.List (isInfixOf, isPrefixOf, isSuffixOf, sort)
import System.Directory (doesDirectoryExist, doesFileExist, listDirectory)
import System.Environment (lookupEnv)
import System.FilePath ((</>), takeDirectory)
import System.Process (readProcessWithExitCode)
import System.Exit (ExitCode(..))
import System.IO.Unsafe (unsafePerformIO)

-- | Locate the header and static archive installed for the Haskell @libBF@
-- dependency so integration tests exercise the same C implementation.
locateLibBF :: IO (FilePath, FilePath)
locateLibBF = modifyMVar libBFLocation $ \cached -> do
  result <- maybe discoverLibBF pure cached
  pure (Just result, result)

-- | Share immutable dependency discovery across parallel integration tests.
{-# NOINLINE libBFLocation #-}
libBFLocation :: MVar (Maybe (FilePath, FilePath))
libBFLocation = unsafePerformIO (newMVar Nothing)

-- | Allow explicit installations; otherwise find a matching header/archive
-- pair from one Cabal package, never from different package versions.
discoverLibBF :: IO (FilePath, FilePath)
discoverLibBF = do
  includeOverride <- lookupEnv "SBV_C_LIBBF_INCLUDE"
  libraryOverride <- lookupEnv "SBV_C_LIBBF_LIBRARY"
  case (includeOverride, libraryOverride) of
    (Just includeDir, Just archive) -> do
      headerExists <- doesFileExist (includeDir </> "libbf.h")
      archiveExists <- doesFileExist archive
      if headerExists && archiveExists then pure (includeDir, archive)
        else fail "SBV_C_LIBBF_INCLUDE/SBV_C_LIBBF_LIBRARY must name an installed libbf.h and library."
    (Nothing, Nothing) -> discoverStoredLibBF
    _ -> fail "Set both SBV_C_LIBBF_INCLUDE and SBV_C_LIBBF_LIBRARY, or neither."

-- | Discover the installed Haskell dependency's bundled C library.
discoverStoredLibBF :: IO (FilePath, FilePath)
discoverStoredLibBF = do
  (pathExit, pathOutput, pathError) <- readProcessWithExitCode "cabal" ["path"] ""
  case pathExit of
    ExitSuccess -> pure ()
    _ -> fail $ "Unable to query Cabal store: " ++ pathError
  let storePrefix = "compiler-store-path: "
      stores      = [drop (length storePrefix) line | line <- lines pathOutput, storePrefix `isInfixOf` line]
  store <- case stores of
             path:_ -> pure path
             []     -> fail $ "Unable to find Cabal store: " ++ pathError
  packages <- listDirectory store
  let libBFDirs = [store </> entry | entry <- sort packages, "lbBF-" `isPrefixOf` entry]
  firstSuccessful [do header <- findInstalledFile path "libbf.h"
                      archive <- findInstalledFile path "libHSlbBF"
                      pure (takeDirectory header, archive)
                  | path <- libBFDirs]

-- | Recursively find an installed file by exact name or filename prefix.
findInstalledFile :: FilePath -> String -> IO FilePath
findInstalledFile root sought = do
  entries <- listDirectory root
  search entries
 where search []           = fail $ "Unable to find " ++ sought ++ " below " ++ root
       search (entry:rest) = do
         let path = root </> entry
         isDir  <- doesDirectoryExist path
         isFile <- doesFileExist path
         if isFile && matches entry
           then pure path
           else if isDir
                  then findInstalledFile path sought `catchIO` search rest
                  else search rest

       matches entry
         | sought == "libHSlbBF" = sought `isPrefixOf` entry && ".a" `isSuffixOf` entry
         | True                  = entry == sought

-- | Return the first successful filesystem search result.
firstSuccessful :: [IO a] -> IO a
firstSuccessful = foldr catchIO (fail "Unable to locate the installed libBF package")

-- | Recover from an expected filesystem-search failure with another search.
catchIO :: IO a -> IO a -> IO a
catchIO action fallback = action `catch` \(_ :: IOException) -> fallback

-- | Build generated Makefiles with strict warnings, resolving LibBF from the
-- Cabal store when needed. Keep compiler overrides and GMP discovery intact.
-- Append @SBV_C_TEST_FLAGS@ to both compilation and linking commands, allowing
-- optimization and sanitizer runs to cover generated programs and library callers.
generatedMakeOptions :: FilePath -> IO [String]
generatedMakeOptions dir = do
  makefile <- readFile (dir </> "Makefile")
  extraFlags <- maybe "" (" " ++) <$> lookupEnv "SBV_C_TEST_FLAGS"
  let flags = "CCFLAGS=-std=c11 -Wall -Werror -O2" ++ extraFlags
  if "-lbf" `isInfixOf` makefile
    then do (includeDir, archive) <- locateLibBF
            pure [flags ++ " -I\"" ++ includeDir ++ "\"", "SBV_LIBS=\"" ++ archive ++ "\" -lm ${GMP_LIBS}"]
    else pure [flags]