{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE ConstraintKinds   #-}
{-# LANGUAGE FlexibleContexts  #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections     #-}

module Stack.Dot
  ( dot
  , listDependencies
  , DotOpts (..)
  , DotPayload (..)
  , ListDepsOpts (..)
  , ListDepsFormat (..)
  , ListDepsFormatOpts (..)
  , resolveDependencies
  , printGraph
  , pruneGraph
  ) where

import           Data.Aeson
import qualified Data.ByteString.Lazy.Char8 as LBC8
import qualified Data.Foldable as F
import qualified Data.Sequence as Seq
import qualified Data.Set as Set
import qualified Data.Map as Map
import qualified Data.Text as Text
import qualified Data.Text.IO as Text
import qualified Data.Traversable as T
import           Distribution.License ( License (BSD3), licenseFromSPDX )
import qualified Distribution.PackageDescription as PD
import qualified Distribution.SPDX.License as SPDX
import           Distribution.Text ( display )
import           Distribution.Types.PackageName ( mkPackageName )
import qualified Path
import           RIO.Process ( HasProcessContext (..) )
import           Stack.Build ( loadPackage )
import           Stack.Build.Installed ( getInstalled, toInstallMap )
import           Stack.Build.Source
import           Stack.Build.Target( NeedTargets (..), parseTargets )
import           Stack.Constants
import           Stack.Package
import           Stack.Prelude hiding ( Display (..), pkgName, loadPackage )
import qualified Stack.Prelude ( pkgName )
import           Stack.Runners
import           Stack.SourceMap
import           Stack.Types.Build
import           Stack.Types.Compiler ( wantedToActual )
import           Stack.Types.Config
import           Stack.Types.GhcPkgId
import           Stack.Types.SourceMap

-- | Type representing exceptions thrown by functions exported by the
-- "Stack.Dot" module.
data DotException
  = DependencyNotFoundBug GhcPkgId
  | PackageNotFoundBug PackageName
  deriving (Show, Typeable)

instance Exception DotException where
  displayException (DependencyNotFoundBug depId) = bugReport "[S-7071]" $ concat
    [ "Expected to find "
    , ghcPkgIdString depId
    , " in global DB."
    ]
  displayException (PackageNotFoundBug pkgName) = bugReport "[S-7151]" $ concat
    [ "The '"
    , packageNameString pkgName
    , "' package was not found in any of the dependency sources."
    ]

-- | Options record for @stack dot@
data DotOpts = DotOpts
  { dotIncludeExternal :: !Bool
  -- ^ Include external dependencies
  , dotIncludeBase :: !Bool
  -- ^ Include dependencies on base
  , dotDependencyDepth :: !(Maybe Int)
  -- ^ Limit the depth of dependency resolution to (Just n) or continue until
  -- fixpoint
  , dotPrune :: !(Set PackageName)
  -- ^ Package names to prune from the graph
  , dotTargets :: [Text]
  -- ^ Stack TARGETs to trace dependencies for
  , dotFlags :: !(Map ApplyCLIFlag (Map FlagName Bool))
  -- ^ Flags to apply when calculating dependencies
  , dotTestTargets :: Bool
  -- ^ Like the "--test" flag for build, affects the meaning of 'dotTargets'.
  , dotBenchTargets :: Bool
  -- ^ Like the "--bench" flag for build, affects the meaning of 'dotTargets'.
  , dotGlobalHints :: Bool
  -- ^ Use global hints instead of relying on an actual GHC installation.
  }

data ListDepsFormatOpts = ListDepsFormatOpts
  { listDepsSep :: !Text
  -- ^ Separator between the package name and details.
  , listDepsLicense :: !Bool
  -- ^ Print dependency licenses instead of versions.
  }

data ListDepsFormat = ListDepsText ListDepsFormatOpts
                    | ListDepsTree ListDepsFormatOpts
                    | ListDepsJSON
                    | ListDepsConstraints

data ListDepsOpts = ListDepsOpts
  { listDepsFormat :: !ListDepsFormat
  -- ^ Format of printing dependencies
  , listDepsDotOpts :: !DotOpts
  -- ^ The normal dot options.
  }

-- | Visualize the project's dependencies as a graphviz graph
dot :: DotOpts -> RIO Runner ()
dot dotOpts = do
  (localNames, prunedGraph) <- createPrunedDependencyGraph dotOpts
  printGraph dotOpts localNames prunedGraph

-- | Information about a package in the dependency graph, when available.
data DotPayload = DotPayload
  { payloadVersion :: Maybe Version
  -- ^ The package version.
  , payloadLicense :: Maybe (Either SPDX.License License)
  -- ^ The license the package was released under.
  , payloadLocation :: Maybe PackageLocation
  -- ^ The location of the package.
  } deriving (Eq, Show)

-- | Create the dependency graph and also prune it as specified in the dot
-- options. Returns a set of local names and a map from package names to
-- dependencies.
createPrunedDependencyGraph
  :: DotOpts
  -> RIO Runner (Set PackageName, Map PackageName (Set PackageName, DotPayload))
createPrunedDependencyGraph dotOpts = withDotConfig dotOpts $ do
  localNames <- view $ buildConfigL.to (Map.keysSet . smwProject . bcSMWanted)
  logDebug "Creating dependency graph"
  resultGraph <- createDependencyGraph dotOpts
  let pkgsToPrune = if dotIncludeBase dotOpts
                      then dotPrune dotOpts
                      else Set.insert "base" (dotPrune dotOpts)
      prunedGraph = pruneGraph localNames pkgsToPrune resultGraph
  logDebug "Returning pruned dependency graph"
  pure (localNames, prunedGraph)

-- | Create the dependency graph, the result is a map from a package
-- name to a tuple of dependencies and payload if available. This
-- function mainly gathers the required arguments for
-- @resolveDependencies@.
createDependencyGraph
  :: DotOpts
  -> RIO DotConfig (Map PackageName (Set PackageName, DotPayload))
createDependencyGraph dotOpts = do
  sourceMap <- view sourceMapL
  locals <- for (toList $ smProject sourceMap) loadLocalPackage
  let graph = Map.fromList $ projectPackageDependencies dotOpts (filter lpWanted locals)
  globalDump <- view $ to dcGlobalDump
  -- TODO: Can there be multiple entries for wired-in-packages? If so,
  -- this will choose one arbitrarily..
  let globalDumpMap = Map.fromList $ map (\dp -> (Stack.Prelude.pkgName (dpPackageIdent dp), dp)) globalDump
      globalIdMap = Map.fromList $ map (\dp -> (dpGhcPkgId dp, dpPackageIdent dp)) globalDump
  let depLoader = createDepLoader sourceMap globalDumpMap globalIdMap loadPackageDeps
      loadPackageDeps name version loc flags ghcOptions cabalConfigOpts
        -- Skip packages that can't be loaded - see
        -- https://github.com/commercialhaskell/stack/issues/2967
        | name `elem` [mkPackageName "rts", mkPackageName "ghc"] =
            pure ( Set.empty
                 , DotPayload (Just version) (Just $ Right BSD3) Nothing )
        | otherwise =
            fmap (packageAllDeps &&& makePayload loc)
                 (loadPackage loc flags ghcOptions cabalConfigOpts)
  resolveDependencies (dotDependencyDepth dotOpts) graph depLoader
 where
  makePayload loc pkg = DotPayload (Just $ packageVersion pkg)
                                   (Just $ packageLicense pkg)
                                   (Just $ PLImmutable loc)

listDependencies
  :: ListDepsOpts
  -> RIO Runner ()
listDependencies opts = do
  let dotOpts = listDepsDotOpts opts
  (pkgs, resultGraph) <- createPrunedDependencyGraph dotOpts
  liftIO $ case listDepsFormat opts of
    ListDepsTree treeOpts ->
      Text.putStrLn "Packages"
      >> printTree treeOpts dotOpts 0 [] (treeRoots opts pkgs) resultGraph
    ListDepsJSON -> printJSON pkgs resultGraph
    ListDepsText textOpts ->
      void $ Map.traverseWithKey (go "" textOpts) (snd <$> resultGraph)
    ListDepsConstraints -> do
      let constraintOpts = ListDepsFormatOpts " ==" False
      Text.putStrLn "constraints:"
      void $ Map.traverseWithKey (go "  , " constraintOpts)
                                 (snd <$> resultGraph)
 where
  go prefix lineOpts name payload =
    Text.putStrLn $ prefix <> listDepsLine lineOpts name payload

data DependencyTree =
  DependencyTree (Set PackageName)
                 (Map PackageName (Set PackageName, DotPayload))

instance ToJSON DependencyTree where
  toJSON (DependencyTree _ dependencyMap) =
    toJSON $ foldToList dependencyToJSON dependencyMap

foldToList :: (k -> a -> b) -> Map k a -> [b]
foldToList f = Map.foldrWithKey (\k a bs -> bs ++ [f k a]) []

dependencyToJSON :: PackageName -> (Set PackageName, DotPayload) -> Value
dependencyToJSON pkg (deps, payload) =
  let fieldsAlwaysPresent = [ "name" .= packageNameString pkg
                            , "license" .= licenseText payload
                            , "version" .= versionText payload
                            , "dependencies" .= Set.map packageNameString deps
                            ]
      loc = catMaybes
              [("location" .=) . pkgLocToJSON <$> payloadLocation payload]
  in object $ fieldsAlwaysPresent ++ loc

pkgLocToJSON :: PackageLocation -> Value
pkgLocToJSON (PLMutable (ResolvedPath _ dir)) = object
  [ "type" .= ("project package" :: Text)
  , "url" .= ("file://" ++ Path.toFilePath dir)
  ]
pkgLocToJSON (PLImmutable (PLIHackage pkgid _ _)) = object
  [ "type" .= ("hackage" :: Text)
  , "url" .= ("https://hackage.haskell.org/package/" ++ display pkgid)
  ]
pkgLocToJSON (PLImmutable (PLIArchive archive _)) =
  let url = case archiveLocation archive of
              ALUrl u -> u
              ALFilePath (ResolvedPath _ path) ->
                Text.pack $ "file://" ++ Path.toFilePath path
  in object
       [ "type" .= ("archive" :: Text)
       , "url" .= url
       , "sha256" .= archiveHash archive
       , "size" .= archiveSize archive
       ]
pkgLocToJSON (PLImmutable (PLIRepo repo _)) = object
  [ "type" .= case repoType repo of
                RepoGit -> "git" :: Text
                RepoHg -> "hg" :: Text
  , "url" .= repoUrl repo
  , "commit" .= repoCommit repo
  , "subdir" .= repoSubdir repo
  ]

printJSON :: Set PackageName
          -> Map PackageName (Set PackageName, DotPayload)
          -> IO ()
printJSON pkgs dependencyMap =
  LBC8.putStrLn $ encode $ DependencyTree pkgs dependencyMap

treeRoots :: ListDepsOpts -> Set PackageName -> Set PackageName
treeRoots opts projectPackages' =
  let targets = dotTargets $ listDepsDotOpts opts
   in if null targets
        then projectPackages'
        else Set.fromList $ map (mkPackageName . Text.unpack) targets

printTree :: ListDepsFormatOpts
          -> DotOpts
          -> Int
          -> [Int]
          -> Set PackageName
          -> Map PackageName (Set PackageName, DotPayload)
          -> IO ()
printTree opts dotOpts depth remainingDepsCounts packages dependencyMap =
  F.sequence_ $ Seq.mapWithIndex go (toSeq packages)
 where
  toSeq = Seq.fromList . Set.toList
  go index name =
    let newDepsCounts = remainingDepsCounts ++ [Set.size packages - index - 1]
    in  case Map.lookup name dependencyMap of
          Just (deps, payload) -> do
            printTreeNode opts dotOpts depth newDepsCounts deps payload name
            if Just depth == dotDependencyDepth dotOpts
              then pure ()
              else printTree opts dotOpts (depth + 1) newDepsCounts deps
                     dependencyMap
          -- TODO: Define this behaviour, maybe pure an error?
          Nothing -> pure ()

printTreeNode :: ListDepsFormatOpts
              -> DotOpts
              -> Int
              -> [Int]
              -> Set PackageName
              -> DotPayload
              -> PackageName
              -> IO ()
printTreeNode opts dotOpts depth remainingDepsCounts deps payload name =
  let remainingDepth = fromMaybe 999 (dotDependencyDepth dotOpts) - depth
      hasDeps = not $ null deps
  in Text.putStrLn $
       treeNodePrefix "" remainingDepsCounts hasDeps remainingDepth <> " " <>
       listDepsLine opts name payload

treeNodePrefix :: Text -> [Int] -> Bool -> Int -> Text
treeNodePrefix t [] _ _      = t
treeNodePrefix t [0] True  0 = t <> "└──"
treeNodePrefix t [_] True  0 = t <> "├──"
treeNodePrefix t [0] True  _ = t <> "└─┬"
treeNodePrefix t [_] True  _ = t <> "├─┬"
treeNodePrefix t [0] False _ = t <> "└──"
treeNodePrefix t [_] False _ = t <> "├──"
treeNodePrefix t (0:ns) d remainingDepth = treeNodePrefix (t <> "  ") ns d remainingDepth
treeNodePrefix t (_:ns) d remainingDepth = treeNodePrefix (t <> "│ ") ns d remainingDepth

listDepsLine :: ListDepsFormatOpts -> PackageName -> DotPayload -> Text
listDepsLine opts name payload =
  Text.pack (packageNameString name) <> listDepsSep opts <>
  payloadText opts payload

payloadText :: ListDepsFormatOpts -> DotPayload -> Text
payloadText opts payload =
  if listDepsLicense opts
    then licenseText payload
    else versionText payload

licenseText :: DotPayload -> Text
licenseText payload =
  maybe "<unknown>" (Text.pack . display . either licenseFromSPDX id)
                    (payloadLicense payload)

versionText :: DotPayload -> Text
versionText payload =
  maybe "<unknown>" (Text.pack . display) (payloadVersion payload)

-- | @pruneGraph dontPrune toPrune graph@ prunes all packages in
-- @graph@ with a name in @toPrune@ and removes resulting orphans
-- unless they are in @dontPrune@
pruneGraph :: (F.Foldable f, F.Foldable g, Eq a)
           => f PackageName
           -> g PackageName
           -> Map PackageName (Set PackageName, a)
           -> Map PackageName (Set PackageName, a)
pruneGraph dontPrune names =
  pruneUnreachable dontPrune . Map.mapMaybeWithKey (\pkg (pkgDeps,x) ->
    if pkg `F.elem` names
      then Nothing
      else let filtered = Set.filter (\n -> n `F.notElem` names) pkgDeps
           in if Set.null filtered && not (Set.null pkgDeps)
                then Nothing
                else Just (filtered,x))

-- | Make sure that all unreachable nodes (orphans) are pruned
pruneUnreachable :: (Eq a, F.Foldable f)
                 => f PackageName
                 -> Map PackageName (Set PackageName, a)
                 -> Map PackageName (Set PackageName, a)
pruneUnreachable dontPrune = fixpoint prune
 where
  fixpoint :: Eq a => (a -> a) -> a -> a
  fixpoint f v = if f v == v then v else fixpoint f (f v)
  prune graph' = Map.filterWithKey (\k _ -> reachable k) graph'
   where
    reachable k = k `F.elem` dontPrune || k `Set.member` reachables
    reachables = F.fold (fst <$> graph')


-- | Resolve the dependency graph up to (Just depth) or until fixpoint is reached
resolveDependencies
  :: (Applicative m, Monad m)
  => Maybe Int
  -> Map PackageName (Set PackageName, DotPayload)
  -> (PackageName -> m (Set PackageName, DotPayload))
  -> m (Map PackageName (Set PackageName, DotPayload))
resolveDependencies (Just 0) graph _ = pure graph
resolveDependencies limit graph loadPackageDeps = do
  let values = Set.unions (fst <$> Map.elems graph)
      keys = Map.keysSet graph
      next = Set.difference values keys
  if Set.null next
     then pure graph
     else do
       x <- T.traverse (\name -> (name,) <$> loadPackageDeps name) (F.toList next)
       resolveDependencies (subtract 1 <$> limit)
                      (Map.unionWith unifier graph (Map.fromList x))
                      loadPackageDeps
 where
  unifier (pkgs1,v1) (pkgs2,_) = (Set.union pkgs1 pkgs2, v1)

-- | Given a SourceMap and a dependency loader, load the set of dependencies for
-- a package
createDepLoader
  :: SourceMap
  -> Map PackageName DumpPackage
  -> Map GhcPkgId PackageIdentifier
  -> (PackageName -> Version -> PackageLocationImmutable ->
      Map FlagName Bool -> [Text] -> [Text] ->
      RIO DotConfig (Set PackageName, DotPayload))
  -> PackageName
  -> RIO DotConfig (Set PackageName, DotPayload)
createDepLoader sourceMap globalDumpMap globalIdMap loadPackageDeps pkgName = do
  fromMaybe (throwIO $ PackageNotFoundBug pkgName)
    (projectPackageDeps <|> dependencyDeps <|> globalDeps)
 where
  projectPackageDeps = loadDeps <$> Map.lookup pkgName (smProject sourceMap)
   where
    loadDeps pp = do
      pkg <- loadCommonPackage (ppCommon pp)
      pure (packageAllDeps pkg, payloadFromLocal pkg Nothing)

  dependencyDeps =
    loadDeps <$> Map.lookup pkgName (smDeps sourceMap)
   where
    loadDeps DepPackage{dpLocation=PLMutable dir} = do
      pp <- mkProjectPackage YesPrintWarnings dir False
      pkg <- loadCommonPackage (ppCommon pp)
      pure (packageAllDeps pkg, payloadFromLocal pkg (Just $ PLMutable dir))

    loadDeps dp@DepPackage{dpLocation=PLImmutable loc} = do
      let common = dpCommon dp
      gpd <- liftIO $ cpGPD common
      let PackageIdentifier name version = PD.package $ PD.packageDescription gpd
          flags = cpFlags common
          ghcOptions = cpGhcOptions common
          cabalConfigOpts = cpCabalConfigOpts common
      assert
        (pkgName == name)
        (loadPackageDeps pkgName version loc flags ghcOptions cabalConfigOpts)

  -- If package is a global package, use info from ghc-pkg (#4324, #3084)
  globalDeps =
    pure . getDepsFromDump <$> Map.lookup pkgName globalDumpMap
   where
    getDepsFromDump dump = (Set.fromList deps, payloadFromDump dump)
     where
      deps = map ghcIdToPackageName (dpDepends dump)
      ghcIdToPackageName depId =
        maybe (impureThrow $ DependencyNotFoundBug depId)
              Stack.Prelude.pkgName
              (Map.lookup depId globalIdMap)

  payloadFromLocal pkg loc =
    DotPayload (Just $ packageVersion pkg)
               (Just $ packageLicense pkg)
               loc
  payloadFromDump dp =
    DotPayload (Just $ pkgVersion $ dpPackageIdent dp)
               (Right <$> dpLicense dp)
               Nothing

-- | Resolve the direct (depth 0) external dependencies of the given local
-- packages (assumed to come from project packages)
projectPackageDependencies
  :: DotOpts
  -> [LocalPackage]
  -> [(PackageName, (Set PackageName, DotPayload))]
projectPackageDependencies dotOpts locals =
  map (\lp -> let pkg = localPackageToPackage lp
                  pkgDir = Path.parent $ lpCabalFile lp
                  loc = PLMutable $ ResolvedPath (RelFilePath "N/A") pkgDir
              in  (packageName pkg, (deps pkg, lpPayload pkg loc)))
      locals
 where
  deps pkg = if dotIncludeExternal dotOpts
               then Set.delete (packageName pkg) (packageAllDeps pkg)
               else Set.intersection localNames (packageAllDeps pkg)
  localNames = Set.fromList $ map (packageName . lpPackage) locals
  lpPayload pkg loc =
    DotPayload (Just $ packageVersion pkg)
               (Just $ packageLicense pkg)
               (Just loc)

-- | Print a graphviz graph of the edges in the Map and highlight the given
-- local packages
printGraph :: (Applicative m, MonadIO m)
           => DotOpts
           -> Set PackageName -- ^ all locals
           -> Map PackageName (Set PackageName, DotPayload)
           -> m ()
printGraph dotOpts locals graph = do
  liftIO $ Text.putStrLn "strict digraph deps {"
  printLocalNodes dotOpts filteredLocals
  printLeaves graph
  void (Map.traverseWithKey printEdges (fst <$> graph))
  liftIO $ Text.putStrLn "}"
 where
  filteredLocals =
    Set.filter (\local' -> local' `Set.notMember` dotPrune dotOpts) locals

-- | Print the local nodes with a different style depending on options
printLocalNodes :: (F.Foldable t, MonadIO m)
                => DotOpts
                -> t PackageName
                -> m ()
printLocalNodes dotOpts locals =
  liftIO $ Text.putStrLn (Text.intercalate "\n" lpNodes)
 where
  applyStyle :: Text -> Text
  applyStyle n = if dotIncludeExternal dotOpts
                   then n <> " [style=dashed];"
                   else n <> " [style=solid];"
  lpNodes :: [Text]
  lpNodes = map (applyStyle . nodeName) (F.toList locals)

-- | Print nodes without dependencies
printLeaves :: MonadIO m
            => Map PackageName (Set PackageName, DotPayload)
            -> m ()
printLeaves = F.mapM_ printLeaf . Map.keysSet . Map.filter Set.null . fmap fst

-- | @printDedges p ps@ prints an edge from p to every ps
printEdges :: MonadIO m => PackageName -> Set PackageName -> m ()
printEdges package deps = F.forM_ deps (printEdge package)

-- | Print an edge between the two package names
printEdge :: MonadIO m => PackageName -> PackageName -> m ()
printEdge from to' =
  liftIO $ Text.putStrLn (Text.concat [ nodeName from
                                      , " -> "
                                      , nodeName to'
                                      , ";" ])

-- | Convert a package name to a graph node name.
nodeName :: PackageName -> Text
nodeName name = "\"" <> Text.pack (packageNameString name) <> "\""

-- | Print a node with no dependencies
printLeaf :: MonadIO m => PackageName -> m ()
printLeaf package = liftIO . Text.putStrLn . Text.concat $
  if isWiredIn package
    then ["{rank=max; ", nodeName package, " [shape=box]; };"]
    else ["{rank=max; ", nodeName package, "; };"]

-- | Check if the package is wired in (shipped with) ghc
isWiredIn :: PackageName -> Bool
isWiredIn = (`Set.member` wiredInPackages)

localPackageToPackage :: LocalPackage -> Package
localPackageToPackage lp =
  fromMaybe (lpPackage lp) (lpTestBench lp)

-- Plumbing for --test and --bench flags
withDotConfig
    :: DotOpts
    -> RIO DotConfig a
    -> RIO Runner a
withDotConfig opts inner =
  local (over globalOptsL modifyGO) $
    if dotGlobalHints opts
      then withConfig NoReexec $ withBuildConfig withGlobalHints
      else withConfig YesReexec withReal
 where
  withGlobalHints = do
    bconfig <- view buildConfigL
    globals <- globalsFromHints $ smwCompiler $ bcSMWanted bconfig
    fakeGhcPkgId <- parseGhcPkgId "ignored"
    actual <- either throwIO pure $
              wantedToActual $ smwCompiler $
              bcSMWanted bconfig
    let smActual = SMActual
          { smaCompiler = actual
          , smaProject = smwProject $ bcSMWanted bconfig
          , smaDeps = smwDeps $ bcSMWanted bconfig
          , smaGlobal = Map.mapWithKey toDump globals
          }
        toDump :: PackageName -> Version -> DumpPackage
        toDump name version = DumpPackage
          { dpGhcPkgId = fakeGhcPkgId
          , dpPackageIdent = PackageIdentifier name version
          , dpParentLibIdent = Nothing
          , dpLicense = Nothing
          , dpLibDirs = []
          , dpLibraries = []
          , dpHasExposedModules = True
          , dpExposedModules = mempty
          , dpDepends = []
          , dpHaddockInterfaces = []
          , dpHaddockHtml = Nothing
          , dpIsExposed = True
          }
        actualPkgs = Map.keysSet (smaDeps smActual) <>
                     Map.keysSet (smaProject smActual)
        prunedActual = smActual { smaGlobal = pruneGlobals (smaGlobal smActual) actualPkgs }
    targets <- parseTargets NeedTargets False boptsCLI prunedActual
    logDebug "Loading source map"
    sourceMap <- loadSourceMap targets boptsCLI smActual
    let dc = DotConfig
                { dcBuildConfig = bconfig
                , dcSourceMap = sourceMap
                , dcGlobalDump = toList $ smaGlobal smActual
                }
    logDebug "DotConfig fully loaded"
    runRIO dc inner

  withReal = withEnvConfig NeedTargets boptsCLI $ do
    envConfig <- ask
    let sourceMap = envConfigSourceMap envConfig
    installMap <- toInstallMap sourceMap
    (_, globalDump, _, _) <- getInstalled installMap
    let dc = DotConfig
          { dcBuildConfig = envConfigBuildConfig envConfig
          , dcSourceMap = sourceMap
          , dcGlobalDump = globalDump
          }
    runRIO dc inner

  boptsCLI = defaultBuildOptsCLI
    { boptsCLITargets = dotTargets opts
    , boptsCLIFlags = dotFlags opts
    }
  modifyGO =
    (if dotTestTargets opts
       then set (globalOptsBuildOptsMonoidL.buildOptsMonoidTestsL) (Just True)
       else id) .
    (if dotBenchTargets opts
       then set (globalOptsBuildOptsMonoidL.buildOptsMonoidBenchmarksL) (Just True)
       else id)

data DotConfig = DotConfig
  { dcBuildConfig :: !BuildConfig
  , dcSourceMap :: !SourceMap
  , dcGlobalDump :: ![DumpPackage]
  }
instance HasLogFunc DotConfig where
  logFuncL = runnerL.logFuncL
instance HasPantryConfig DotConfig where
  pantryConfigL = configL.pantryConfigL
instance HasTerm DotConfig where
  useColorL = runnerL.useColorL
  termWidthL = runnerL.termWidthL
instance HasStylesUpdate DotConfig where
  stylesUpdateL = runnerL.stylesUpdateL
instance HasGHCVariant DotConfig
instance HasPlatform DotConfig
instance HasRunner DotConfig where
  runnerL = configL.runnerL
instance HasProcessContext DotConfig where
  processContextL = runnerL.processContextL
instance HasConfig DotConfig
instance HasBuildConfig DotConfig where
  buildConfigL = lens dcBuildConfig (\x y -> x { dcBuildConfig = y })
instance HasSourceMap DotConfig where
  sourceMapL = lens dcSourceMap (\x y -> x { dcSourceMap = y })
