stack-0.0.2: src/Stack/Build/ConstructPlan.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
-- | Construct a @Plan@ for how to build
module Stack.Build.ConstructPlan
( constructPlan
) where
import Control.Exception.Lifted
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.RWS.Strict
import Control.Monad.Trans.Resource
import qualified Data.ByteString.Char8 as S8
import Data.Either
import Data.Function
import Data.List
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import qualified Data.Map.Strict as Map
import Data.Maybe
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Text (Text)
import Data.Text.Encoding (encodeUtf8)
import Distribution.Package (Dependency (..))
import Distribution.Version (anyVersion,
intersectVersionRanges)
import Prelude hiding (FilePath, pi, writeFile)
import Stack.Build.Cache
import Stack.Build.Installed
import Stack.Build.Source
import Stack.Build.Types
import Stack.BuildPlan
import Stack.Package
import Stack.Types
data PackageInfo
= PIOnlyInstalled Version Location Installed
| PIOnlySource PackageSource
| PIBoth PackageSource Installed
combineSourceInstalled :: PackageSource
-> (Version, Location, Installed)
-> PackageInfo
combineSourceInstalled ps (version, location, installed) =
assert (piiVersion ps == version) $
assert (piiLocation ps == location) $
case location of
-- Always trust something in the snapshot
Snap -> PIOnlyInstalled version location installed
Local -> PIBoth ps installed
type CombinedMap = Map PackageName PackageInfo
combineMap :: SourceMap -> InstalledMap -> CombinedMap
combineMap = Map.mergeWithKey
(\_ s i -> Just $ combineSourceInstalled s i)
(fmap PIOnlySource)
(fmap (\(v, l, i) -> PIOnlyInstalled v l i))
data AddDepRes
= ADRToInstall Task
| ADRFound Version Installed
deriving Show
type M = RWST
Ctx
( Map PackageName (Either ConstructPlanException Task) -- finals
, Map Text Location -- executable to be installed, and location where the binary is placed
)
(Map PackageName (Either ConstructPlanException AddDepRes))
IO
data Ctx = Ctx
{ mbp :: !MiniBuildPlan
, baseConfigOpts :: !BaseConfigOpts
, loadPackage :: !(PackageName -> Version -> Map FlagName Bool -> IO Package)
, combinedMap :: !CombinedMap
, toolToPackages :: !(Dependency -> Map PackageName VersionRange)
, ctxBuildConfig :: !BuildConfig
, callStack :: ![PackageName]
, extraToBuild :: !(Set PackageName)
}
instance HasStackRoot Ctx
instance HasPlatform Ctx
instance HasConfig Ctx
instance HasBuildConfig Ctx where
getBuildConfig = ctxBuildConfig
constructPlan :: forall env m.
(MonadThrow m, MonadReader env m, HasBuildConfig env, MonadIO m)
=> MiniBuildPlan
-> BaseConfigOpts
-> [LocalPackage]
-> Set PackageName -- ^ additional packages that must be built
-> Set GhcPkgId -- ^ locally registered
-> (PackageName -> Version -> Map FlagName Bool -> IO Package) -- ^ load upstream package
-> SourceMap
-> InstalledMap
-> m Plan
constructPlan mbp0 baseConfigOpts0 locals extraToBuild0 locallyRegistered loadPackage0 sourceMap installedMap = do
bconfig <- asks getBuildConfig
let inner = do
mapM_ addFinal $ filter lpWanted locals
mapM_ addDep $ Set.toList extraToBuild0
((), m, (efinals, installExes)) <- liftIO $ runRWST inner (ctx bconfig) M.empty
let toEither (_, Left e) = Left e
toEither (k, Right v) = Right (k, v)
(errlibs, adrs) = partitionEithers $ map toEither $ M.toList m
(errfinals, finals) = partitionEithers $ map toEither $ M.toList efinals
errs = errlibs ++ errfinals
if null errs
then do
let toTask (_, ADRFound _ _) = Nothing
toTask (name, ADRToInstall task) = Just (name, task)
tasks = M.fromList $ mapMaybe toTask adrs
return Plan
{ planTasks = tasks
, planFinals = M.fromList finals
, planUnregisterLocal = mkUnregisterLocal tasks locallyRegistered
, planInstallExes =
if boptsInstallExes $ bcoBuildOpts baseConfigOpts0
then installExes
else Map.empty
}
else throwM $ ConstructPlanExceptions errs
where
ctx bconfig = Ctx
{ mbp = mbp0
, baseConfigOpts = baseConfigOpts0
, loadPackage = loadPackage0
, combinedMap = combineMap sourceMap installedMap
, toolToPackages = \ (Dependency name _) ->
maybe Map.empty (Map.fromSet (\_ -> anyVersion)) $
Map.lookup (S8.pack . packageNameString . fromCabalPackageName $ name) toolMap
, ctxBuildConfig = bconfig
, callStack = []
, extraToBuild = extraToBuild0
}
toolMap = getToolMap mbp0
-- | Determine which packages to unregister based on the given tasks and
-- already registered local packages
mkUnregisterLocal :: Map PackageName Task -> Set GhcPkgId -> Set GhcPkgId
mkUnregisterLocal tasks locallyRegistered =
Set.filter toUnregister locallyRegistered
where
toUnregister gid =
case M.lookup name tasks of
Nothing -> False
Just _ -> True
where
ident = ghcPkgIdPackageIdentifier gid
name = packageIdentifierName ident
addFinal :: LocalPackage -> M ()
addFinal lp = do
void $ addDep $ packageName package
depsRes <- addPackageDeps package
res <- case depsRes of
Left e -> return $ Left e
Right (missing, present) -> do
ctx <- ask
return $ Right Task
{ taskProvides = PackageIdentifier
(packageName package)
(packageVersion package)
, taskConfigOpts = TaskConfigOpts missing $ \missing' ->
let allDeps = Set.union present missing'
in configureOpts
(baseConfigOpts ctx)
allDeps
True -- wanted
Local
(packageFlags package)
, taskPresent = present
, taskType = TTLocal lp
}
tell (Map.singleton (packageName package) res, mempty)
where
package = lpPackageFinal lp
addDep :: PackageName -> M (Either ConstructPlanException AddDepRes)
addDep name = do
m <- get
case Map.lookup name m of
Just res -> return res
Nothing -> do
res <- addDep' name
modify $ Map.insert name res
return res
addDep' :: PackageName -> M (Either ConstructPlanException AddDepRes)
addDep' name = do
ctx <- ask
if name `elem` callStack ctx
then return $ Left $ DependencyCycleDetected $ name : callStack ctx
else local
(\ctx' -> ctx' { callStack = name : callStack ctx' }) $ do
(addDep'' name)
addDep'' :: PackageName -> M (Either ConstructPlanException AddDepRes)
addDep'' name = do
ctx <- ask
case Map.lookup name $ combinedMap ctx of
-- TODO look up in the package index and see if there's a
-- recommendation available
Nothing -> return $ Left $ UnknownPackage name
Just (PIOnlyInstalled version loc installed) -> do
tellExecutablesUpstream name version loc Map.empty -- slightly hacky, no flags since they likely won't affect executable names
return $ Right $ ADRFound version installed
Just (PIOnlySource ps) -> do
tellExecutables name ps
installPackage name ps
Just (PIBoth ps installed) -> do
tellExecutables name ps
needInstall <- checkNeedInstall name ps installed
if needInstall
then installPackage name ps
else return $ Right $ ADRFound (piiVersion ps) installed
tellExecutables :: PackageName -> PackageSource -> M () -- TODO merge this with addFinal above?
tellExecutables _ (PSLocal lp)
| lpWanted lp = tellExecutablesPackage Local $ lpPackage lp
| otherwise = return ()
tellExecutables name (PSUpstream version loc flags) = do
tellExecutablesUpstream name version loc flags
tellExecutablesUpstream :: PackageName -> Version -> Location -> Map FlagName Bool -> M ()
tellExecutablesUpstream name version loc flags = do
ctx <- ask
when (name `Set.member` extraToBuild ctx) $ do
p <- liftIO $ loadPackage ctx name version flags
tellExecutablesPackage loc p
tellExecutablesPackage :: Location -> Package -> M ()
tellExecutablesPackage loc p =
tell (Map.empty, m)
where
m = Map.fromList $ map (, loc) $ Set.toList $ packageExes p
-- TODO There are a lot of duplicated computations below. I've kept that for
-- simplicity right now
installPackage :: PackageName -> PackageSource -> M (Either ConstructPlanException AddDepRes)
installPackage name ps = do
ctx <- ask
package <- psPackage name ps
depsRes <- addPackageDeps package
case depsRes of
Left e -> return $ Left e
Right (missing, present) -> do
return $ Right $ ADRToInstall Task
{ taskProvides = PackageIdentifier
(packageName package)
(packageVersion package)
, taskConfigOpts = TaskConfigOpts missing $ \missing' ->
let allDeps = Set.union present missing'
in configureOpts
(baseConfigOpts ctx)
allDeps
(psWanted ps)
(piiLocation ps)
(packageFlags package)
, taskPresent = present
, taskType =
case ps of
PSLocal lp -> TTLocal lp
PSUpstream _ loc _ -> TTUpstream package loc
}
checkNeedInstall :: PackageName -> PackageSource -> Installed -> M Bool
checkNeedInstall name ps installed = assert (piiLocation ps == Local) $ do
package <- psPackage name ps
depsRes <- addPackageDeps package
case depsRes of
Left _e -> return True -- installPackage will find the error again
Right (missing, present)
| Set.null missing -> checkDirtiness ps installed package present
| otherwise -> return True
addPackageDeps :: Package -> M (Either ConstructPlanException (Set PackageIdentifier, Set GhcPkgId))
addPackageDeps package = do
deps' <- packageDepsWithTools package
deps <- forM (Map.toList deps') $ \(depname, range) -> do
eres <- addDep depname
case eres of
Left e ->
let bd =
case e of
UnknownPackage _ -> NotInBuildPlan
_ -> Couldn'tResolveItsDependencies
in return $ Left (depname, (range, bd))
Right adr | not $ adrVersion adr `withinRange` range ->
return $ Left (depname, (range, DependencyMismatch $ adrVersion adr))
Right (ADRToInstall task) -> return $ Right
(Set.singleton $ taskProvides task, Set.empty)
Right (ADRFound _ (Executable _)) -> return $ Right
(Set.empty, Set.empty)
Right (ADRFound _ (Library gid)) -> return $ Right
(Set.empty, Set.singleton gid)
case partitionEithers deps of
([], pairs) -> return $ Right $ mconcat pairs
(errs, _) -> return $ Left $ DependencyPlanFailures
(PackageIdentifier
(packageName package)
(packageVersion package))
(Map.fromList errs)
where
adrVersion (ADRToInstall task) = packageIdentifierVersion $ taskProvides task
adrVersion (ADRFound v _) = v
checkDirtiness :: PackageSource
-> Installed
-> Package
-> Set GhcPkgId
-> M Bool
checkDirtiness ps installed package present = do
ctx <- ask
let configOpts = configureOpts
(baseConfigOpts ctx)
present
(psWanted ps)
(piiLocation ps) -- should be Local always
(packageFlags package)
configCache = ConfigCache
{ configCacheOpts = map encodeUtf8 configOpts
, configCacheDeps = present
}
moldOpts <- tryGetFlagCache installed
case moldOpts of
Nothing -> return True
Just oldOpts -> return $ oldOpts /= configCache || psDirty ps
psDirty :: PackageSource -> Bool
psDirty (PSLocal lp) = lpDirtyFiles lp
psDirty (PSUpstream _ _ _) = False -- files never change in an upstream package
psWanted :: PackageSource -> Bool
psWanted (PSLocal lp) = lpWanted lp
psWanted (PSUpstream _ _ _) = False
psPackage :: PackageName -> PackageSource -> M Package
psPackage _ (PSLocal lp) = return $ lpPackage lp
psPackage name (PSUpstream version _ flags) = do
ctx <- ask
liftIO $ loadPackage ctx name version flags
-- | Get all of the dependencies for a given package, including guessed build
-- tool dependencies.
packageDepsWithTools :: Package -> M (Map PackageName VersionRange)
packageDepsWithTools p = do
ctx <- ask
return $ Map.unionsWith intersectVersionRanges
$ packageDeps p
: map (toolToPackages ctx) (packageTools p)