packages feed

run-haskell-module-0.0.2: src/Language/Haskell/RunHaskellModule.hs

-- | Functions for running generated modules
--
{-# LANGUAGE LambdaCase           #-}
{-# LANGUAGE RecordWildCards      #-}
{-# LANGUAGE ScopedTypeVariables  #-}
module Language.Haskell.RunHaskellModule
    ( RunOptions(..)
    , compileHaskellModule
    , compileHaskellModule'
    , runHaskellModule
    , runHaskellModule'
    ) where


import           Control.Exception
import           Control.Monad
import           Data.Char
import           Data.Default
import           Data.Semigroup((<>))
import           System.Environment
import           System.Exit
import           System.FilePath.Posix
import           System.IO
import           System.Process


data RunOptions = RunOptions
        { verbose            :: Bool
        , showStdout         :: Bool
        , compileArgs        :: [String]
        , additionalPackages :: [String]
        }


instance Default RunOptions where
    def = RunOptions { verbose            = False
                     , showStdout         = False
                     , compileArgs        = []
                     , additionalPackages = []
                     }


data GhcTool = Runner | Compiler


-- | Call specified process with args and print its output when it fails.
--
callProcess' :: RunOptions -> FilePath -> [String] -> IO ExitCode
callProcess' RunOptions{..} cmd args = do
    when verbose $ putStrLn $ "Run \"" ++ cmd ++ "\" with args: " ++ show args
    (_, pstdout, pstderr, p) <- createProcess ((proc cmd args) { std_out = if showStdout then Inherit else CreatePipe, std_err = CreatePipe })
    waitForProcess p >>= \case
        ExitSuccess -> do
            unless showStdout $ whenMaybe hClose pstdout
            whenMaybe hClose pstderr
            return ExitSuccess
        ExitFailure r -> do
            whenMaybe (dumpHandle stderr) pstdout
            whenMaybe (dumpHandle stderr) pstderr
            fail $ concat ["Running \"", cmd, "\" \"", show args, "\" has failed with \"", show r, "\""]
  where
    dumpHandle outhndl inhnd = hGetContents inhnd >>= hPutStr outhndl
    whenMaybe a m = maybe (return ()) a m


-- | Splits commandline-like strings into "words", i.e.
--   ```
--   -O0 --ghc-arg="-package scientific" --ghc-arg="-package xml-typelift"
--   ```
--   transformed into 3 "words":
--   ```
--   -O0
--   --ghc-arg="-package scientific"
--   --ghc-arg="-package xml-typelift"
--   ```
splitWithQuotes :: String -> [String]
splitWithQuotes [] = []
splitWithQuotes (ch:cs)
  | isSpace ch = splitWithQuotes $ dropWhile isSpace cs
  | otherwise = word : splitWithQuotes strrest
  where
    (word, strrest) = takeWordOrQuote (ch:cs)
    takeWordOrQuote :: String -> (String, String)
    takeWordOrQuote str = let (w', rest) = takeWordOrQuote' "" False str in (reverse w', rest)
      where
        takeWordOrQuote' acc _     ""         = (acc, "")
        takeWordOrQuote' acc True  ('"':"")   = (acc, "")
        takeWordOrQuote' acc True  ('"':c:rest)
          | isSpace c = ('"':acc, rest)
          | otherwise = takeWordOrQuote' ('"':acc) False (c:rest)
        takeWordOrQuote' acc True  (c  :rest) = takeWordOrQuote' (c:acc) True rest
        takeWordOrQuote' acc False ('"':rest) = takeWordOrQuote' ('"':acc) True rest
        takeWordOrQuote' acc False (c  :rest)
          | isSpace c = (acc, rest)
          | otherwise = takeWordOrQuote' (c:acc) False rest


findGhc :: RunOptions
        -> GhcTool
        -> IO (FilePath, [String]) -- ^ returns (exe, special tool arguments)
findGhc RunOptions{..} ghcTool = do
    when verbose $ do
        let showEnv env = lookupEnv env >>= (\e -> putStrLn $ ">>> " ++ show env ++ " = " ++ show e)
        showEnv "STACK_EXE"
        showEnv "CABAL_SANDBOX_CONFIG"
        showEnv "GHC_ENVIRONMENT"
        showEnv "GHC_PACKAGE_PATH"
        showEnv "HASKELL_DIST_DIR"
        showEnv "CI_GHC_ADDITIONAL_FLAGS"
        showEnv "CI_GHC_ADDITIONAL_PACKAGES"
        showEnv "CI_GHC_CABAL_STYLE"
        -- putStrLn "Environment: -----------"
        -- getEnvironment >>= (mapM_ $ \(env,val) -> putStrLn $ env ++ " = " ++ val)
        -- putStrLn "End of environment -----"
    stack    <- lookupEnv "STACK_EXE"
    oldCabal <- lookupEnv "CABAL_SANDBOX_CONFIG"
    newCabal <- lookupEnv "HASKELL_DIST_DIR"
    additionalFlags        <- (maybe [] splitWithQuotes)                    <$> lookupEnv "CI_GHC_ADDITIONAL_FLAGS"
    additionalPackagesList <- ((additionalPackages ++) . (maybe [] words))  <$> lookupEnv "CI_GHC_ADDITIONAL_PACKAGES"
    cabalStyle             <- (maybe "v2" id)                               <$> lookupEnv "CI_GHC_CABAL_STYLE"
    let cabalExec = cabalStyle ++ "-exec"
    let additionalPackagesArgs = map mkAdditionalPackagesArg additionalPackagesList

    let res@(exe, exeArgs') | Just stackExec <- stack    = (stackExec, [tool] ++ additionalFlags
                                                                   ++ (stackPackageArg <$> additionalPackagesList)
                                                                   ++ ["--"])
                            | Just _         <- oldCabal = ("cabal", ["exec", tool, "--"])
                            | Just _         <- newCabal = ("cabal", [cabalExec, tool, "--"] ++ additionalPackagesArgs)
                            | otherwise                  = (tool, [])
        exeArgs = case ghcTool of
                    Compiler -> exeArgs' ++ ["-O0"]
                    Runner   -> exeArgs'
    when verbose $ putStrLn $ "Use exe \"" ++ exe ++ "\", and additional arguments: " ++ show exeArgs
    return res
  where
    tool = case ghcTool of
               Runner   -> "runghc"
               Compiler -> "ghc"
    stackPackageArg arg = "--package=" ++ arg
    mkAdditionalPackagesArg arg = case ghcTool of
               Runner   -> "--ghc-arg=-package=" ++ arg
               Compiler ->           "-package=" ++ arg


passModuleToGhc :: RunOptions -> GhcTool -> FilePath -> [String] -> IO ExitCode
passModuleToGhc ro ghcTool moduleFilename args =
    handle (\(e::SomeException) -> do print e >> throw e) $ do
        (exe, exeArgs) <- findGhc ro ghcTool
        callProcess' ro exe (exeArgs ++ moduleFilename:args)


-- | Find ghc with cabal or stack and compile Haskell module with specified arguments and run options
--
compileHaskellModule' :: RunOptions -> FilePath -> [String] -> IO ExitCode
compileHaskellModule' ro moduleFilename args = passModuleToGhc ro Compiler moduleFilename args

-- | Find ghc with cabal or stack and compile Haskell module with specified arguments
--
compileHaskellModule :: FilePath -> [String] -> IO ExitCode
compileHaskellModule = compileHaskellModule' def

-- | Find ghc with cabal or stack and run Haskell module in specified file with arguments and run options
--
runHaskellModule' :: RunOptions -> FilePath -> [String] -> IO ExitCode
runHaskellModule' ro moduleFilename args = passModuleToGhc ro Runner moduleFilename args


-- | Find ghc with cabal or stack and run Haskell module in specified file with arguments
--
runHaskellModule :: FilePath -> [String] -> IO ExitCode
runHaskellModule = runHaskellModule' def