stack-2.9.3: src/Stack/Build.hs
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | Build the project.
module Stack.Build
( build
, buildLocalTargets
, loadPackage
, mkBaseConfigOpts
, queryBuildInfo
, splitObjsWarning
, CabalVersionException (..)
) where
import Data.Aeson ( Value (Object, Array), (.=), object )
import qualified Data.Aeson.Key as Key
import qualified Data.Aeson.KeyMap as KeyMap
import Data.List ( (\\), isPrefixOf )
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 Data.Text.Encoding ( decodeUtf8 )
import qualified Data.Text.IO as TIO
import Data.Text.Read ( decimal )
import qualified Data.Vector as V
import qualified Data.Yaml as Yaml
import qualified Distribution.PackageDescription as C
import Distribution.Types.Dependency ( depLibraries )
import Distribution.Version ( mkVersion )
import Path ( parent )
import Stack.Build.ConstructPlan
import Stack.Build.Execute
import Stack.Build.Installed
import Stack.Build.Source
import Stack.Package
import Stack.Prelude hiding ( loadPackage )
import Stack.Setup ( withNewLocalBuildTargets )
import Stack.Types.Build
import Stack.Types.Compiler ( compilerVersionText, getGhcVersion )
import Stack.Types.Config
import Stack.Types.NamedComponent
import Stack.Types.Package
import Stack.Types.SourceMap
import System.Terminal ( fixCodePage )
data CabalVersionException
= AllowNewerNotSupported Version
| CabalVersionNotSupported Version
deriving (Show, Typeable)
instance Exception CabalVersionException where
displayException (AllowNewerNotSupported cabalVer) = concat
[ "Error: [S-8503]\n"
, "'--allow-newer' requires Cabal version 1.22 or greater, but "
, "version "
, versionString cabalVer
, " was found."
]
displayException (CabalVersionNotSupported cabalVer) = concat
[ "Error: [S-5973]\n"
, "Stack no longer supports Cabal versions before 1.19.2, "
, "but version "
, versionString cabalVer
, " was found. To fix this, consider updating the resolver to lts-3.0 "
, "or later or to nightly-2015-05-05 or later."
]
data QueryException
= SelectorNotFound [Text]
| IndexOutOfRange [Text]
| NoNumericSelector [Text]
| CannotApplySelector Value [Text]
deriving (Show, Typeable)
instance Exception QueryException where
displayException (SelectorNotFound sels) =
err "[S-4419]" "Selector not found" sels
displayException (IndexOutOfRange sels) =
err "[S-8422]" "Index out of range" sels
displayException (NoNumericSelector sels) =
err "[S-4360]" "Encountered array and needed numeric selector" sels
displayException (CannotApplySelector value sels) =
err "[S-1711]" ("Cannot apply selector to " ++ show value) sels
-- | Helper function for 'QueryException' instance of 'Show'
err :: String -> String -> [Text] -> String
err msg code sels = "Error: " ++ code ++ "\n" ++ msg ++ ": " ++ show sels
-- | 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
allowNewer <- view $ configL.to configAllowNewer
cabalVer <- view cabalVersionL
-- https://github.com/haskell/cabal/issues/2023
when (allowNewer && cabalVer < mkVersion [1, 22]) $ throwM $
AllowNewerNotSupported cabalVer
-- Since --exact-configuration is always passed, some old cabal
-- versions can no longer be used. See the following link for why
-- it's 1.19.2:
-- https://github.com/haskell/cabal/blob/580fe6b6bf4e1648b2f66c1cb9da9f1f1378492c/cabal-install/Distribution/Client/Setup.hs#L592
when (cabalVer < mkVersion [1, 19, 2]) $ throwM $
CabalVersionNotSupported cabalVer
-- | See https://github.com/commercialhaskell/stack/issues/1198.
warnIfExecutablesWithSameNameCouldBeOverwritten
:: HasLogFunc 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 = "several executables with the same name:"
| otherwise = "executable"
exesText pkgs =
T.intercalate
", "
["'" <> T.pack (packageNameString p) <> ":" <> exe <> "'" | p <- pkgs]
(logWarn . display . T.unlines . concat)
[ [ "Building " <> exe_s <> " " <> exesText toBuild <> "." ]
, [ "Only one of them will be available via 'stack exec' or locally installed."
| length toBuild > 1
]
, [ "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 :: HasLogFunc env => BuildOpts -> RIO env ()
warnAboutSplitObjs bopts | boptsSplitObjs bopts = do
logWarn $ "Building with --split-objs is enabled. " <> fromString splitObjsWarning
warnAboutSplitObjs _ = pure ()
splitObjsWarning :: String
splitObjsWarning = unwords
[ "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
-- | Query information about the build and print the result to stdout in YAML format.
queryBuildInfo :: HasEnvConfig env
=> [Text] -- ^ selectors
-> RIO env ()
queryBuildInfo selectors0 =
rawBuildInfo
>>= select id selectors0
>>= liftIO . TIO.putStrLn . addGlobalHintsComment . decodeUtf8 . Yaml.encode
where
select _ [] value = pure value
select front (sel:sels) value =
case value of
Object o ->
case KeyMap.lookup (Key.fromText sel) o of
Nothing -> throwIO $ SelectorNotFound sels'
Just value' -> cont value'
Array v ->
case decimal sel of
Right (i, "")
| i >= 0 && i < V.length v -> cont $ v V.! i
| otherwise -> throwIO $ IndexOutOfRange sels'
_ -> throwIO $ NoNumericSelector sels'
_ -> throwIO $ CannotApplySelector value sels'
where
cont = select (front . (sel:)) sels
sels' = front [sel]
-- Include comments to indicate that this portion of the "stack
-- query" API is not necessarily stable.
addGlobalHintsComment
| null selectors0 = T.replace globalHintsLine ("\n" <> globalHintsComment <> globalHintsLine)
-- Append comment instead of pre-pending. The reasoning here is
-- that something *could* expect that the result of 'stack query
-- global-hints ghc-boot' is just a string literal. Seems easier
-- for to expect the first line of the output to be the literal.
| ["global-hints"] `isPrefixOf` selectors0 = (<> ("\n" <> globalHintsComment))
| otherwise = id
globalHintsLine = "\nglobal-hints:\n"
globalHintsComment = T.concat
[ "# Note: global-hints is experimental and may be renamed / removed in the future.\n"
, "# See https://github.com/commercialhaskell/stack/issues/3796"
]
-- | Get the raw build information object
rawBuildInfo :: HasEnvConfig env => RIO env Value
rawBuildInfo = do
locals <- projectLocalPackages
wantedCompiler <- view $ wantedCompilerVersionL.to (utf8BuilderToText . display)
actualCompiler <- view $ actualCompilerVersionL.to compilerVersionText
pure $ object
[ "locals" .= Object (KeyMap.fromList $ map localToPair locals)
, "compiler" .= object
[ "wanted" .= wantedCompiler
, "actual" .= actualCompiler
]
]
where
localToPair lp =
(Key.fromText $ T.pack $ packageNameString $ packageName p, value)
where
p = lpPackage lp
value = object
[ "version" .= CabalString (packageVersion p)
, "path" .= toFilePath (parent $ lpCabalFile lp)
]
checkComponentsBuildable :: MonadThrow m => [LocalPackage] -> m ()
checkComponentsBuildable lps =
unless (null unbuildable) $ throwM $ SomeTargetsNotBuildable unbuildable
where
unbuildable =
[ (packageName (lpPackage lp), c)
| lp <- lps
, c <- Set.toList (lpUnbuildable lp)
]
-- | Find if sublibrary dependency exist in each project
checkSubLibraryDependencies :: HasLogFunc env => [ProjectPackage] -> RIO env ()
checkSubLibraryDependencies proj = do
forM_ proj $ \p -> do
C.GenericPackageDescription _ _ _ lib subLibs foreignLibs exes tests benches <- liftIO $ cpGPD . ppCommon $ p
let dependencies = concatMap getDeps subLibs <>
concatMap getDeps foreignLibs <>
concatMap getDeps exes <>
concatMap getDeps tests <>
concatMap getDeps benches <>
maybe [] C.condTreeConstraints lib
libraries = concatMap (toList . depLibraries) dependencies
when (subLibDepExist libraries)
(logWarn "SubLibrary dependency is not supported, this will almost certainly fail")
where
getDeps (_, C.CondNode _ dep _) = dep
subLibDepExist lib =
any (\x ->
case x of
C.LSubLibName _ -> True
C.LMainLibName -> False
) lib