stack-2.13.1: src/Stack/Build.hs
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
-- | Build the project.
module Stack.Build
( buildCmd
, build
, buildLocalTargets
, loadPackage
, mkBaseConfigOpts
, splitObjsWarning
) where
import Data.Attoparsec.Args ( EscapingMode (Escaping), parseArgs )
import Data.List ( (\\) )
import Data.List.Extra ( groupSort )
import qualified Data.List.NonEmpty as NE
import qualified Data.Map as Map
import qualified Data.Set as Set
import qualified Data.Text as T
import qualified Distribution.PackageDescription as C
import Distribution.Types.Dependency ( Dependency (..), depLibraries )
import Distribution.Version ( mkVersion )
import Stack.Build.ConstructPlan ( constructPlan )
import Stack.Build.Execute ( executePlan, preFetch, printPlan )
import Stack.Build.Installed ( getInstalled, toInstallMap )
import Stack.Build.Source ( localDependencies, projectLocalPackages )
import Stack.Build.Target ( NeedTargets (..) )
import Stack.FileWatch ( fileWatch, fileWatchPoll )
import Stack.Package ( resolvePackage )
import Stack.Prelude hiding ( loadPackage )
import Stack.Runners ( ShouldReexec (..), withConfig, withEnvConfig )
import Stack.Setup ( withNewLocalBuildTargets )
import Stack.Types.Build
( Plan (..), Task (..), TaskType (..), taskLocation )
import Stack.Types.Build.Exception
( BuildException (..), BuildPrettyException (..) )
import Stack.Types.BuildConfig ( HasBuildConfig, stackYamlL )
import Stack.Types.BuildOpts
( BuildCommand (..), BuildOpts (..), BuildOptsCLI (..)
, FileWatchOpts (..), buildOptsMonoidBenchmarksL
, buildOptsMonoidHaddockL, buildOptsMonoidInstallExesL
, buildOptsMonoidTestsL
)
import Stack.Types.Compiler ( getGhcVersion )
import Stack.Types.CompilerPaths ( cabalVersionL )
import Stack.Types.Config
( Config (..), HasConfig (..), buildOptsL
)
import Stack.Types.ConfigureOpts ( BaseConfigOpts (..) )
import Stack.Types.EnvConfig
( EnvConfig (..), HasEnvConfig (..), HasSourceMap
, actualCompilerVersionL, installationRootDeps
, installationRootLocal, packageDatabaseDeps
, packageDatabaseExtra, packageDatabaseLocal
)
import Stack.Types.GlobalOpts ( globalOptsBuildOptsMonoidL )
import Stack.Types.NamedComponent ( exeComponents )
import Stack.Types.Package
( InstallLocation (..), LocalPackage (..), Package (..)
, PackageConfig (..), lpFiles, lpFilesForComponents )
import Stack.Types.Platform ( HasPlatform (..) )
import Stack.Types.Runner ( Runner, globalOptsL )
import Stack.Types.SourceMap
( CommonPackage (..), ProjectPackage (..), SMTargets (..)
, SourceMap (..), Target (..) )
import System.Terminal ( fixCodePage )
newtype CabalVersionPrettyException
= CabalVersionNotSupported Version
deriving (Show, Typeable)
instance Pretty CabalVersionPrettyException where
pretty (CabalVersionNotSupported cabalVer) =
"[S-5973]"
<> line
<> fillSep
[ flow "Stack does not support Cabal versions before 1.24, but \
\version"
, fromString $ versionString cabalVer
, flow "was found. To fix this, consider updating the snapshot to"
, style Shell "lts-7.0"
, flow "or later or to"
, style Shell "nightly-2016-05-26"
, flow "or later."
]
instance Exception CabalVersionPrettyException
-- | Helper for build and install commands
buildCmd :: BuildOptsCLI -> RIO Runner ()
buildCmd opts = do
when (any (("-prof" `elem`) . fromRight [] . parseArgs Escaping) (boptsCLIGhcOptions opts)) $
prettyThrowIO GHCProfOptionInvalid
local (over globalOptsL modifyGO) $
case boptsCLIFileWatch opts of
FileWatchPoll -> fileWatchPoll (inner . Just)
FileWatch -> fileWatch (inner . Just)
NoFileWatch -> inner Nothing
where
inner ::
Maybe (Set (Path Abs File) -> IO ())
-> RIO Runner ()
inner setLocalFiles = withConfig YesReexec $ withEnvConfig NeedTargets opts $
Stack.Build.build setLocalFiles
-- Read the build command from the CLI and enable it to run
modifyGO =
case boptsCLICommand opts of
Test -> set (globalOptsBuildOptsMonoidL.buildOptsMonoidTestsL) (Just True)
Haddock ->
set (globalOptsBuildOptsMonoidL.buildOptsMonoidHaddockL) (Just True)
Bench ->
set (globalOptsBuildOptsMonoidL.buildOptsMonoidBenchmarksL) (Just True)
Install ->
set (globalOptsBuildOptsMonoidL.buildOptsMonoidInstallExesL) (Just True)
Build -> id -- Default case is just Build
-- | Build.
--
-- If a buildLock is passed there is an important contract here. That lock must
-- protect the snapshot, and it must be safe to unlock it if there are no further
-- modifications to the snapshot to be performed by this build.
build :: HasEnvConfig env
=> Maybe (Set (Path Abs File) -> IO ()) -- ^ callback after discovering all local files
-> RIO env ()
build msetLocalFiles = do
mcp <- view $ configL.to configModifyCodePage
ghcVersion <- view $ actualCompilerVersionL.to getGhcVersion
fixCodePage mcp ghcVersion $ do
bopts <- view buildOptsL
sourceMap <- view $ envConfigL.to envConfigSourceMap
locals <- projectLocalPackages
depsLocals <- localDependencies
let allLocals = locals <> depsLocals
checkSubLibraryDependencies (Map.elems $ smProject sourceMap)
boptsCli <- view $ envConfigL.to envConfigBuildOptsCLI
-- Set local files, necessary for file watching
stackYaml <- view stackYamlL
for_ msetLocalFiles $ \setLocalFiles -> do
files <-
if boptsCLIWatchAll boptsCli
then sequence [lpFiles lp | lp <- allLocals]
else forM allLocals $ \lp -> do
let pn = packageName (lpPackage lp)
case Map.lookup pn (smtTargets $ smTargets sourceMap) of
Nothing ->
pure Set.empty
Just (TargetAll _) ->
lpFiles lp
Just (TargetComps components) ->
lpFilesForComponents components lp
liftIO $ setLocalFiles $ Set.insert stackYaml $ Set.unions files
checkComponentsBuildable allLocals
installMap <- toInstallMap sourceMap
(installedMap, globalDumpPkgs, snapshotDumpPkgs, localDumpPkgs) <-
getInstalled installMap
baseConfigOpts <- mkBaseConfigOpts boptsCli
plan <- constructPlan baseConfigOpts localDumpPkgs loadPackage sourceMap installedMap (boptsCLIInitialBuildSteps boptsCli)
allowLocals <- view $ configL.to configAllowLocals
unless allowLocals $ case justLocals plan of
[] -> pure ()
localsIdents -> throwM $ LocalPackagesPresent localsIdents
checkCabalVersion
warnAboutSplitObjs bopts
warnIfExecutablesWithSameNameCouldBeOverwritten locals plan
when (boptsPreFetch bopts) $
preFetch plan
if boptsCLIDryrun boptsCli
then printPlan plan
else executePlan
boptsCli
baseConfigOpts
locals
globalDumpPkgs
snapshotDumpPkgs
localDumpPkgs
installedMap
(smtTargets $ smTargets sourceMap)
plan
buildLocalTargets ::
HasEnvConfig env
=> NonEmpty Text
-> RIO env (Either SomeException ())
buildLocalTargets targets =
tryAny $ withNewLocalBuildTargets (NE.toList targets) $ build Nothing
justLocals :: Plan -> [PackageIdentifier]
justLocals =
map taskProvides .
filter ((== Local) . taskLocation) .
Map.elems .
planTasks
checkCabalVersion :: HasEnvConfig env => RIO env ()
checkCabalVersion = do
cabalVer <- view cabalVersionL
when (cabalVer < mkVersion [1, 24]) $
prettyThrowM $ CabalVersionNotSupported cabalVer
-- | See https://github.com/commercialhaskell/stack/issues/1198.
warnIfExecutablesWithSameNameCouldBeOverwritten ::
HasTerm env
=> [LocalPackage]
-> Plan
-> RIO env ()
warnIfExecutablesWithSameNameCouldBeOverwritten locals plan = do
logDebug "Checking if we are going to build multiple executables with the same name"
forM_ (Map.toList warnings) $ \(exe, (toBuild, otherLocals)) -> do
let exe_s
| length toBuild > 1 = flow "several executables with the same name:"
| otherwise = "executable"
exesText pkgs =
fillSep $ punctuate
","
[ style
PkgComponent
(fromString $ packageNameString p <> ":" <> T.unpack exe)
| p <- pkgs
]
prettyWarnL $
[ "Building"
, exe_s
, exesText toBuild <> "."
]
<> [ fillSep
[ flow "Only one of them will be available via"
, style Shell "stack exec"
, flow "or locally installed."
]
| length toBuild > 1
]
<> [ fillSep
[ flow "Other executables with the same name might be overwritten:"
, exesText otherLocals <> "."
]
| not (null otherLocals)
]
where
-- Cases of several local packages having executables with the same name.
-- The Map entries have the following form:
--
-- executable name: ( package names for executables that are being built
-- , package names for other local packages that have an
-- executable with the same name
-- )
warnings :: Map Text ([PackageName],[PackageName])
warnings =
Map.mapMaybe
(\(pkgsToBuild,localPkgs) ->
case (pkgsToBuild,NE.toList localPkgs \\ NE.toList pkgsToBuild) of
(_ :| [],[]) ->
-- We want to build the executable of single local package
-- and there are no other local packages with an executable of
-- the same name. Nothing to warn about, ignore.
Nothing
(_,otherLocals) ->
-- We could be here for two reasons (or their combination):
-- 1) We are building two or more executables with the same
-- name that will end up overwriting each other.
-- 2) In addition to the executable(s) that we want to build
-- there are other local packages with an executable of the
-- same name that might get overwritten.
-- Both cases warrant a warning.
Just (NE.toList pkgsToBuild,otherLocals))
(Map.intersectionWith (,) exesToBuild localExes)
exesToBuild :: Map Text (NonEmpty PackageName)
exesToBuild =
collect
[ (exe,pkgName')
| (pkgName',task) <- Map.toList (planTasks plan)
, TTLocalMutable lp <- [taskType task]
, exe <- (Set.toList . exeComponents . lpComponents) lp
]
localExes :: Map Text (NonEmpty PackageName)
localExes =
collect
[ (exe,packageName pkg)
| pkg <- map lpPackage locals
, exe <- Set.toList (packageExes pkg)
]
collect :: Ord k => [(k,v)] -> Map k (NonEmpty v)
collect = Map.map NE.fromList . Map.fromDistinctAscList . groupSort
warnAboutSplitObjs :: HasTerm env => BuildOpts -> RIO env ()
warnAboutSplitObjs bopts | boptsSplitObjs bopts =
prettyWarnL
[ flow "Building with"
, style Shell "--split-objs"
, flow "is enabled."
, flow splitObjsWarning
]
warnAboutSplitObjs _ = pure ()
splitObjsWarning :: String
splitObjsWarning =
"Note that this feature is EXPERIMENTAL, and its behavior may be changed and \
\improved. You will need to clean your workdirs before use. If you want to \
\compile all dependencies with split-objs, you will need to delete the \
\snapshot (and all snapshots that could reference that snapshot)."
-- | Get the @BaseConfigOpts@ necessary for constructing configure options
mkBaseConfigOpts :: (HasEnvConfig env)
=> BuildOptsCLI -> RIO env BaseConfigOpts
mkBaseConfigOpts boptsCli = do
bopts <- view buildOptsL
snapDBPath <- packageDatabaseDeps
localDBPath <- packageDatabaseLocal
snapInstallRoot <- installationRootDeps
localInstallRoot <- installationRootLocal
packageExtraDBs <- packageDatabaseExtra
pure BaseConfigOpts
{ bcoSnapDB = snapDBPath
, bcoLocalDB = localDBPath
, bcoSnapInstallRoot = snapInstallRoot
, bcoLocalInstallRoot = localInstallRoot
, bcoBuildOpts = bopts
, bcoBuildOptsCLI = boptsCli
, bcoExtraDBs = packageExtraDBs
}
-- | Provide a function for loading package information from the package index
loadPackage ::
(HasBuildConfig env, HasSourceMap env)
=> PackageLocationImmutable
-> Map FlagName Bool
-> [Text] -- ^ GHC options
-> [Text] -- ^ Cabal configure options
-> RIO env Package
loadPackage loc flags ghcOptions cabalConfigOpts = do
compiler <- view actualCompilerVersionL
platform <- view platformL
let pkgConfig = PackageConfig
{ packageConfigEnableTests = False
, packageConfigEnableBenchmarks = False
, packageConfigFlags = flags
, packageConfigGhcOptions = ghcOptions
, packageConfigCabalConfigOpts = cabalConfigOpts
, packageConfigCompilerVersion = compiler
, packageConfigPlatform = platform
}
resolvePackage pkgConfig <$> loadCabalFileImmutable loc
checkComponentsBuildable :: MonadThrow m => [LocalPackage] -> m ()
checkComponentsBuildable lps =
unless (null unbuildable) $
prettyThrowM $ SomeTargetsNotBuildable unbuildable
where
unbuildable =
[ (packageName (lpPackage lp), c)
| lp <- lps
, c <- Set.toList (lpUnbuildable lp)
]
-- | Find if any sublibrary dependency (other than internal libraries) exists in
-- each project package.
checkSubLibraryDependencies :: HasTerm env => [ProjectPackage] -> RIO env ()
checkSubLibraryDependencies projectPackages =
forM_ projectPackages $ \projectPackage -> do
C.GenericPackageDescription pkgDesc _ _ lib subLibs foreignLibs exes tests benches <-
liftIO $ cpGPD . ppCommon $ projectPackage
let pName = pkgName . C.package $ pkgDesc
dependencies = concatMap getDeps subLibs <>
concatMap getDeps foreignLibs <>
concatMap getDeps exes <>
concatMap getDeps tests <>
concatMap getDeps benches <>
maybe [] C.condTreeConstraints lib
notInternal (Dependency pName' _ _) = pName' /= pName
publicDependencies = filter notInternal dependencies
publicLibraries = concatMap (toList . depLibraries) publicDependencies
when (subLibDepExist publicLibraries) $
prettyWarnS
"Sublibrary dependency is not supported, this will almost certainly \
\fail."
where
getDeps (_, C.CondNode _ dep _) = dep
subLibDepExist = any
( \case
C.LSubLibName _ -> True
C.LMainLibName -> False
)