tricorder-0.5.0.0: src/Tricorder/Session.hs
module Tricorder.Session
( Session (..)
, loadSession
, inputSession
, show
)
where
import Atelier.Config (LoadedConfig, extractConfig)
import Atelier.Effects.FileSystem (FileSystem)
import Atelier.Effects.Input (Input, input, runInputEff)
import Atelier.Effects.Log (Log)
import Data.Default (Default (..))
import Effectful.Reader.Static (Reader, ask)
import Text.Regex.TDFA.Pattern (showPattern)
import Prelude hiding (show)
import Atelier.Effects.Log qualified as Log
import Data.Text qualified as T
import Prelude qualified as P
import Tricorder.Build.ByteSize (ByteSize)
import Tricorder.Runtime (ProjectRoot (..))
import Tricorder.Session.CabalFile (CabalFile)
import Tricorder.Session.CommandConfig (CommandConfig (..))
import Tricorder.Session.CommandTemplate
( CommandTemplate (..)
, hasPlaceholder
, targetPlaceholder
)
import Tricorder.Session.Config (Config (..))
import Tricorder.Session.GenerateWithHpack (GenerateWithHpack (..))
import Tricorder.Session.Hooks (Hooks)
import Tricorder.Session.IdleTimeout (IdleTimeout (..))
import Tricorder.Session.Repl (resolveRepl)
import Tricorder.Session.ReplBuildDir (ReplBuildDir (..))
import Tricorder.Session.Stage.Build.Session (BuildSession (..))
import Tricorder.Session.Stage.Test.Config (TestConfig (..))
import Tricorder.Session.Stage.Test.Session (TestSession (..))
import Tricorder.Session.Target (definesCustomPrelude)
import Tricorder.Session.TestTarget (getTestTarget)
import Tricorder.Session.TestTimeout (TestTimeout (..))
import Tricorder.Session.Util (indent, showList)
import Tricorder.Session.WatchDirs (WatchDirs (..))
import Tricorder.Session.WatchExclusionPatterns
( WatchExclusionPatterns (..)
, resolveWatchExclusionPatterns
)
import Tricorder.Build.ByteSize qualified as ByteSize
import Tricorder.Session.Stage qualified as Stage
import Tricorder.Session.Stage.Build.Session qualified as BuildSession
import Tricorder.Session.Stage.Eval.Command qualified as EvalCommand
import Tricorder.Session.Stage.Eval.Session qualified as EvalSession
import Tricorder.Session.Stage.Test.Session qualified as TestSession
import Tricorder.Session.WatchDirs qualified as WatchDirs
data Session = Session
{ buildSession :: BuildSession
, testSession :: TestSession
, evalSession :: CommandTemplate 'Stage.Eval
, testMemoryLimit :: Maybe ByteSize
, watchDirs :: WatchDirs
, watchExclusionPatterns :: WatchExclusionPatterns
, replBuildDir :: ReplBuildDir
, testTimeout :: TestTimeout
, generateWithHpack :: GenerateWithHpack
, hooks :: Hooks
, idleTimeout :: IdleTimeout
}
deriving stock (Eq)
instance Default Session where
def =
Session
{ buildSession = def
, testSession = def
, evalSession = def
, testMemoryLimit = Nothing
, watchDirs = def
, watchExclusionPatterns = def
, replBuildDir = def
, testTimeout = def
, generateWithHpack = def
, hooks = def
, idleTimeout = def
}
loadSession
:: ( FileSystem :> es
, Input LoadedConfig :> es
, Input [CabalFile] :> es
, Log :> es
, Reader ProjectRoot :> es
)
=> Eff es Session
loadSession = do
projectRoot <- ask @ProjectRoot
loadedCfg <- input
Log.debug "Got config file"
projectFiles <- input
Log.debug "Got project files"
let cfgFile = extractConfig @"session" @Config loadedCfg
hooks = fromMaybe def cfgFile.hooks
warnDeprecatedConfig cfgFile
Log.debug "Warned about deprecated config stuff"
testMemoryLimit <- case cfgFile.testMemoryLimit of
Nothing -> pure Nothing
Just limit -> case ByteSize.fromText limit of
Nothing -> do
Log.err $ "Unable to parse test_memory_limit: " <> limit
pure Nothing
Just parsedLimit ->
pure $ Just parsedLimit
watchExclusionPatterns <-
case resolveWatchExclusionPatterns cfgFile.watchExclusionPatterns of
Left err -> do
Log.err
$ T.intercalate
"\n"
[ "Failed to parse watch exclusion patterns:"
, err
, "Defaulting to no exclusion patterns."
]
pure $ WatchExclusionPatterns []
Right pts -> pure pts
repl <- resolveRepl projectRoot
buildSession <- BuildSession.resolve cfgFile repl projectFiles
let testSession = TestSession.resolve repl buildSession.targets cfgFile
evalSession = EvalCommand.resolve repl cfgFile
effectiveTargets = buildSession.targets <> (getTestTarget <$> testSession.targets)
watchDirs = WatchDirs.resolve projectRoot projectFiles cfgFile effectiveTargets
when (not (null effectiveTargets) && all (definesCustomPrelude projectFiles) effectiveTargets)
$ Log.warn
"Every resolved target exposes a custom Prelude module. GHCi may \
\fail to start because the first target's Prelude will be loaded \
\before its package is ready. Consider adding a target that does \
\not define its own Prelude, or set an explicit command in your \
\tricorder configuration."
warnMissingTargetPlaceholder "test" cfgFile.test.commandConfig.commandTemplate
warnMissingTargetPlaceholder "eval" cfgFile.eval.commandTemplate
warnIgnoredextraAutoArguments
"build"
(cfgFile.build.commandTemplate <|> cfgFile.command)
cfgFile.build.extraAutoArguments
warnIgnoredextraAutoArguments
"test"
cfgFile.test.commandConfig.commandTemplate
cfgFile.test.commandConfig.extraAutoArguments
warnIgnoredextraAutoArguments "eval" cfgFile.eval.commandTemplate cfgFile.eval.extraAutoArguments
pure
$ Session
{ buildSession
, testSession
, evalSession
, watchDirs
, watchExclusionPatterns
, testMemoryLimit
, replBuildDir = ReplBuildDir cfgFile.replBuildDir
, testTimeout = TestTimeout cfgFile.testTimeout
, generateWithHpack = GenerateWithHpack cfgFile.generateWithHpack
, hooks
, idleTimeout = IdleTimeout $ fromIntegral cfgFile.idleTimeoutSeconds
}
inputSession
:: ( FileSystem :> es
, Input LoadedConfig :> es
, Input [CabalFile] :> es
, Log :> es
, Reader ProjectRoot :> es
)
=> Eff (Input Session : es) a -> Eff es a
inputSession = runInputEff loadSession
-- | Warn once per session load for each deprecated top-level config key
-- still in use, regardless of whether its replacement also takes
-- precedence.
warnDeprecatedConfig :: (Log :> es) => Config -> Eff es ()
warnDeprecatedConfig cfgFile = do
whenJust cfgFile.command
$ const
$ Log.warn "session.command is deprecated; use session.build.command_template instead."
unless (null cfgFile.targets)
$ Log.warn "session.targets is deprecated; use session.build.targets instead."
whenJust cfgFile.testTargets
$ const
$ Log.warn "session.test_targets is deprecated; use session.test.targets instead."
-- | Warn when a section's @extra_auto_arguments@ is set alongside a custom
-- @command_template@, since 'Tricorder.Session.Config.extraAutoArguments'
-- would otherwise be silently ignored.
warnIgnoredextraAutoArguments :: (Log :> es) => Text -> Maybe Text -> [Text] -> Eff es ()
warnIgnoredextraAutoArguments section customTemplate extraAutoArguments =
when (isJust customTemplate && not (null extraAutoArguments))
$ Log.warn
$ "session."
<> section
<> ".command_template is set; session."
<> section
<> ".extra_auto_arguments is ignored (it only applies to Tricorder's \
\automatically resolved command)."
-- | Warn when a custom @test@\/@eval@ @command_template@ has no @{target}@
-- placeholder — without it, every invocation (one per test target or
-- module) silently runs the exact same command. @build@ isn't checked this
-- way: a missing @{targets}@ there mirrors past custom-@command@ behavior,
-- more likely deliberate than an oversight.
warnMissingTargetPlaceholder :: (Log :> es) => Text -> Maybe Text -> Eff es ()
warnMissingTargetPlaceholder section customTemplate =
case customTemplate of
Just tpl
| not (hasPlaceholder targetPlaceholder tpl) ->
Log.warn
$ "session."
<> section
<> ".command_template has no {target} placeholder — every "
<> section
<> " invocation will run the same command."
_ -> pure ()
show :: Session -> Text
show session =
T.intercalate
"\n"
[ "Loaded session"
, "Build configuration:"
, indent $ BuildSession.show session.buildSession
, "Test configuration:"
, indent $ TestSession.show session.testSession
, "Eval comments configuration:"
, indent $ EvalSession.show session.evalSession
, "Watch dirs:"
, indent $ showList toText session.watchDirs.getWatchDirs
, "Watch exclusion patterns:"
, indent
$ showList
(toText . showPattern . fst)
session.watchExclusionPatterns.getWatchExclusionPatterns
, "Repl build dir: " <> toText session.replBuildDir.getReplBuildDir
, -- TODO: Remove " seconds" when TestTimeout is converted to a proper time unit.
"Test timeout: " <> P.show session.testTimeout.getTestTimeout <> " seconds"
, "Test memory limit: " <> P.show session.testMemoryLimit
, "Generate with hpack: " <> P.show session.generateWithHpack.getGenerateWithHpack
]