packages feed

stack-2.13.1: src/Stack/Package.hs

{-# LANGUAGE NoImplicitPrelude   #-}
{-# LANGUAGE DataKinds           #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE RecordWildCards     #-}

-- | Dealing with Cabal.

module Stack.Package
  ( readDotBuildinfo
  , resolvePackage
  , packageFromPackageDescription
  , Package (..)
  , PackageDescriptionPair (..)
  , GetPackageOpts (..)
  , PackageConfig (..)
  , buildLogPath
  , PackageException (..)
  , resolvePackageDescription
  , packageDependencies
  , applyForceCustomBuild
  ) where

import           Data.List ( unzip )
import qualified Data.Map.Strict as M
import qualified Data.Set as S
import qualified Data.Text as T
import           Distribution.CabalSpecVersion ( cabalSpecToVersionDigits )
import qualified Distribution.Compat.NonEmptySet as NES
import           Distribution.Compiler
                   ( CompilerFlavor (..), PerCompilerFlavor (..) )
import           Distribution.Package ( mkPackageName )
import           Distribution.PackageDescription
                   ( Benchmark (..), BuildInfo (..), BuildType (..)
                   , CondTree (..), Condition (..), ConfVar (..)
                   , Dependency (..), Executable (..), ForeignLib (..)
                   , GenericPackageDescription (..), HookedBuildInfo
                   , Library (..), PackageDescription (..), PackageFlag (..)
                   , SetupBuildInfo (..), TestSuite (..), allLanguages
                   , allLibraries, buildType, depPkgName, depVerRange
                   , libraryNameString, maybeToLibraryName, usedExtensions
                   )
import           Distribution.Pretty ( prettyShow )
import           Distribution.Simple.PackageDescription ( readHookedBuildInfo )
import           Distribution.System ( OS (..), Arch, Platform (..) )
import           Distribution.Text ( display )
import qualified Distribution.Types.CondTree as Cabal
import qualified Distribution.Types.ExeDependency as Cabal
import qualified Distribution.Types.LegacyExeDependency as Cabal
import qualified Distribution.Types.UnqualComponentName as Cabal
import           Distribution.Utils.Path ( getSymbolicPath )
import           Distribution.Verbosity ( silent )
import           Distribution.Version
                   ( anyVersion, mkVersion, orLaterVersion )
import           Path
                   ( (</>), parent, parseAbsDir, parseRelDir, parseRelFile
                   , stripProperPrefix
                   )
import           Path.Extra ( concatAndCollapseAbsDir, toFilePathNoTrailingSep )
import           Stack.Constants (relFileCabalMacrosH, relDirLogs)
import           Stack.Constants.Config ( distDirFromDir )
import           Stack.Prelude hiding ( Display (..) )
import           Stack.ComponentFile
                   ( buildDir, componentAutogenDir, componentBuildDir
                   , componentOutputDir, packageAutogenDir
                   )
import           Stack.Types.BuildConfig
                   ( HasBuildConfig (..), getProjectWorkDir )
import           Stack.Types.Compiler ( ActualCompiler (..), getGhcVersion )
import           Stack.Types.CompilerPaths ( cabalVersionL )
import           Stack.Types.Config ( Config (..), HasConfig (..) )
import           Stack.Types.EnvConfig ( HasEnvConfig )
import           Stack.Types.GhcPkgId ( ghcPkgIdString )
import           Stack.Types.NamedComponent
                   ( NamedComponent (..), internalLibComponents )
import           Stack.Types.Package
                   ( BuildInfoOpts (..), ExeName (..), GetPackageOpts (..)
                   , InstallMap, Installed (..), InstalledMap, Package (..)
                   , PackageConfig (..), PackageException (..)
                   , PackageLibraries (..), dotCabalCFilePath, packageIdentifier
                   )
import           Stack.Types.Version
                   ( VersionRange, intersectVersionRanges, withinRange )
import           System.FilePath ( replaceExtension )
import           Stack.Types.Dependency ( DepValue (..), DepType (..) )
import           Stack.Types.PackageFile ( DotCabalPath , GetPackageFiles (..) )
import           Stack.PackageFile ( getPackageFile )

-- | Read @<package>.buildinfo@ ancillary files produced by some Setup.hs hooks.
-- The file includes Cabal file syntax to be merged into the package description
-- derived from the package's Cabal file.
--
-- NOTE: not to be confused with BuildInfo, an Stack-internal datatype.
readDotBuildinfo :: MonadIO m => Path Abs File -> m HookedBuildInfo
readDotBuildinfo buildinfofp =
  liftIO $ readHookedBuildInfo silent (toFilePath buildinfofp)

-- | Resolve a parsed Cabal file into a 'Package', which contains all of the
-- info needed for Stack to build the 'Package' given the current configuration.
resolvePackage :: PackageConfig -> GenericPackageDescription -> Package
resolvePackage packageConfig gpkg =
  packageFromPackageDescription
    packageConfig
    (genPackageFlags gpkg)
    (resolvePackageDescription packageConfig gpkg)

packageFromPackageDescription ::
     PackageConfig
  -> [PackageFlag]
  -> PackageDescriptionPair
  -> Package
packageFromPackageDescription packageConfig pkgFlags (PackageDescriptionPair pkgNoMod pkg) =
  Package
  { packageName = name
  , packageVersion = pkgVersion pkgId
  , packageLicense = licenseRaw pkg
  , packageDeps = deps
  , packageFiles = pkgFiles
  , packageUnknownTools = unknownTools
  , packageGhcOptions = packageConfigGhcOptions packageConfig
  , packageCabalConfigOpts = packageConfigCabalConfigOpts packageConfig
  , packageFlags = packageConfigFlags packageConfig
  , packageDefaultFlags = M.fromList
      [(flagName flag, flagDefault flag) | flag <- pkgFlags]
  , packageAllDeps = M.keysSet deps
  , packageSubLibDeps = subLibDeps
  , packageLibraries =
      let mlib = do
            lib <- library pkg
            guard $ buildable $ libBuildInfo lib
            Just lib
       in
        case mlib of
          Nothing -> NoLibraries
          Just _ -> HasLibraries foreignLibNames
  , packageInternalLibraries = subLibNames
  , packageTests = M.fromList
      [ (T.pack (Cabal.unUnqualComponentName $ testName t), testInterface t)
      | t <- testSuites pkgNoMod
      , buildable (testBuildInfo t)
      ]
  , packageBenchmarks = S.fromList
      [ T.pack (Cabal.unUnqualComponentName $ benchmarkName b)
      | b <- benchmarks pkgNoMod
      , buildable (benchmarkBuildInfo b)
      ]
      -- Same comment about buildable applies here too.
  , packageExes = S.fromList
      [ T.pack (Cabal.unUnqualComponentName $ exeName biBuildInfo)
      | biBuildInfo <- executables pkg
      , buildable (buildInfo biBuildInfo)
      ]
  -- This is an action used to collect info needed for "stack ghci".
  -- This info isn't usually needed, so computation of it is deferred.
  , packageOpts = GetPackageOpts $
      \installMap installedMap omitPkgs addPkgs cabalfp -> do
        (componentsModules,componentFiles, _, _) <- getPackageFiles pkgFiles cabalfp
        let internals =
              S.toList $ internalLibComponents $ M.keysSet componentsModules
        excludedInternals <- mapM (parsePackageNameThrowing . T.unpack) internals
        mungedInternals <- mapM
          (parsePackageNameThrowing . T.unpack . toInternalPackageMungedName)
          internals
        componentsOpts <- generatePkgDescOpts
          installMap
          installedMap
          (excludedInternals ++ omitPkgs)
          (mungedInternals ++ addPkgs)
          cabalfp
          pkg
          componentFiles
        pure (componentsModules, componentFiles, componentsOpts)
  , packageHasExposedModules = maybe
      False
      (not . null . exposedModules)
      (library pkg)
  , packageBuildType = buildType pkg
  , packageSetupDeps = msetupDeps
  , packageCabalSpec = specVersion pkg
  }
 where
  extraLibNames = S.union subLibNames foreignLibNames

  subLibNames
    = S.fromList
    $ map (T.pack . Cabal.unUnqualComponentName)
    $ mapMaybe (libraryNameString . libName) -- this is a design bug in the
                                             -- Cabal API: this should
                                             -- statically be known to exist
    $ filter (buildable . libBuildInfo)
    $ subLibraries pkg

  foreignLibNames
    = S.fromList
    $ map (T.pack . Cabal.unUnqualComponentName . foreignLibName)
    $ filter (buildable . foreignLibBuildInfo)
    $ foreignLibs pkg

  toInternalPackageMungedName
    = T.pack . prettyShow . MungedPackageName (pkgName pkgId)
    . maybeToLibraryName . Just . Cabal.mkUnqualComponentName . T.unpack

  -- Gets all of the modules, files, build files, and data files that constitute
  -- the package. This is primarily used for dirtiness checking during build, as
  -- well as use by "stack ghci"
  pkgFiles = GetPackageFiles $ getPackageFile pkg
  pkgId = package pkg
  name = pkgName pkgId

  (unknownTools, knownTools) = packageDescTools pkg

  deps = M.filterWithKey (const . not . isMe) (M.unionsWith (<>)
    [ asLibrary <$> packageDependencies packageConfig pkg
    -- We include all custom-setup deps - if present - in the package deps
    -- themselves. Stack always works with the invariant that there will be a
    -- single installed package relating to a package name, and this applies at
    -- the setup dependency level as well.
    , asLibrary <$> fromMaybe M.empty msetupDeps
    , knownTools
    ])

  msetupDeps = fmap
    (M.fromList . map (depPkgName &&& depVerRange) . setupDepends)
    (setupBuildInfo pkg)

  subLibDeps = M.fromList $ concatMap
    (\(Dependency n vr libs) -> mapMaybe (getSubLibName n vr) (NES.toList libs))
    (concatMap targetBuildDepends (allBuildInfo' pkg))

  getSubLibName pn vr lib@(LSubLibName _) =
    Just (MungedPackageName pn lib, asLibrary vr)
  getSubLibName _ _ _ = Nothing

  asLibrary range = DepValue
    { dvVersionRange = range
    , dvType = AsLibrary
    }

  -- Is the package dependency mentioned here me: either the package name
  -- itself, or the name of one of the sub libraries
  isMe name' =  name' == name
             || fromString (packageNameString name') `S.member` extraLibNames

-- | Generate GHC options for the package's components, and a list of options
-- which apply generally to the package, not one specific component.
generatePkgDescOpts ::
     (HasEnvConfig env, MonadThrow m, MonadReader env m, MonadIO m)
  => InstallMap
  -> InstalledMap
  -> [PackageName]
     -- ^ Packages to omit from the "-package" / "-package-id" flags
  -> [PackageName]
     -- ^ Packages to add to the "-package" flags
  -> Path Abs File
  -> PackageDescription
  -> Map NamedComponent [DotCabalPath]
  -> m (Map NamedComponent BuildInfoOpts)
generatePkgDescOpts installMap installedMap omitPkgs addPkgs cabalfp pkg componentPaths = do
  config <- view configL
  cabalVer <- view cabalVersionL
  distDir <- distDirFromDir cabalDir
  let generate namedComponent binfo =
        ( namedComponent
        , generateBuildInfoOpts BioInput
            { biInstallMap = installMap
            , biInstalledMap = installedMap
            , biCabalDir = cabalDir
            , biDistDir = distDir
            , biOmitPackages = omitPkgs
            , biAddPackages = addPkgs
            , biBuildInfo = binfo
            , biDotCabalPaths =
                fromMaybe [] (M.lookup namedComponent componentPaths)
            , biConfigLibDirs = configExtraLibDirs config
            , biConfigIncludeDirs = configExtraIncludeDirs config
            , biComponentName = namedComponent
            , biCabalVersion = cabalVer
            }
        )
  pure
    ( M.fromList
        ( concat
            [ maybe
                []
                (pure . generate CLib . libBuildInfo)
                (library pkg)
            , mapMaybe
                (\sublib -> do
                  let maybeLib =
                        CInternalLib . T.pack . Cabal.unUnqualComponentName <$>
                          (libraryNameString . libName) sublib
                  flip generate  (libBuildInfo sublib) <$> maybeLib
                 )
                (subLibraries pkg)
            , fmap
                (\exe ->
                  generate
                    (CExe (T.pack (Cabal.unUnqualComponentName (exeName exe))))
                    (buildInfo exe)
                )
                (executables pkg)
            , fmap
                (\bench ->
                  generate
                    (CBench
                      (T.pack (Cabal.unUnqualComponentName (benchmarkName bench)))
                    )
                    (benchmarkBuildInfo bench)
                )
                (benchmarks pkg)
            , fmap
                (\test ->
                  generate
                    (CTest (T.pack (Cabal.unUnqualComponentName (testName test))))
                    (testBuildInfo test)
                )
                (testSuites pkg)
            ]
        )
    )
 where
  cabalDir = parent cabalfp

-- | Input to 'generateBuildInfoOpts'
data BioInput = BioInput
  { biInstallMap :: !InstallMap
  , biInstalledMap :: !InstalledMap
  , biCabalDir :: !(Path Abs Dir)
  , biDistDir :: !(Path Abs Dir)
  , biOmitPackages :: ![PackageName]
  , biAddPackages :: ![PackageName]
  , biBuildInfo :: !BuildInfo
  , biDotCabalPaths :: ![DotCabalPath]
  , biConfigLibDirs :: ![FilePath]
  , biConfigIncludeDirs :: ![FilePath]
  , biComponentName :: !NamedComponent
  , biCabalVersion :: !Version
  }

-- | Generate GHC options for the target. Since Cabal also figures out these
-- options, currently this is only used for invoking GHCI (via stack ghci).
generateBuildInfoOpts :: BioInput -> BuildInfoOpts
generateBuildInfoOpts BioInput {..} =
  BuildInfoOpts
    { bioOpts = ghcOpts ++ fmap ("-optP" <>) (cppOptions biBuildInfo)
    -- NOTE for future changes: Due to this use of nubOrd (and other uses
    -- downstream), these generated options must not rely on multiple
    -- argument sequences.  For example, ["--main-is", "Foo.hs", "--main-
    -- is", "Bar.hs"] would potentially break due to the duplicate
    -- "--main-is" being removed.
    --
    -- See https://github.com/commercialhaskell/stack/issues/1255
    , bioOneWordOpts = nubOrd $ concat
        [extOpts, srcOpts, includeOpts, libOpts, fworks, cObjectFiles]
    , bioPackageFlags = deps
    , bioCabalMacros = componentAutogen </> relFileCabalMacrosH
    }
 where
  cObjectFiles =
    mapMaybe (fmap toFilePath .
              makeObjectFilePathFromC biCabalDir biComponentName biDistDir)
             cfiles
  cfiles = mapMaybe dotCabalCFilePath biDotCabalPaths
  installVersion = snd
  -- Generates: -package=base -package=base16-bytestring-0.1.1.6 ...
  deps =
    concat
      [ case M.lookup name biInstalledMap of
          Just (_, Stack.Types.Package.Library _ident ipid _) ->
            ["-package-id=" <> ghcPkgIdString ipid]
          _ -> ["-package=" <> packageNameString name <>
            maybe "" -- This empty case applies to e.g. base.
              ((("-" <>) . versionString) . installVersion)
              (M.lookup name biInstallMap)]
      | name <- pkgs
      ]
  pkgs =
    biAddPackages ++
    [ name
    | Dependency name _ _ <- targetBuildDepends biBuildInfo
      -- TODO: cabal 3 introduced multiple public libraries in a single dependency
    , name `notElem` biOmitPackages
    ]
  PerCompilerFlavor ghcOpts _ = options biBuildInfo
  extOpts =
       map (("-X" ++) . display) (allLanguages biBuildInfo)
    <> map (("-X" ++) . display) (usedExtensions biBuildInfo)
  srcOpts =
    map (("-i" <>) . toFilePathNoTrailingSep)
      (concat
        [ [ componentBuildDir biCabalVersion biComponentName biDistDir ]
        , [ biCabalDir
          | null (hsSourceDirs biBuildInfo)
          ]
        , mapMaybe (toIncludeDir . getSymbolicPath) (hsSourceDirs biBuildInfo)
        , [ componentAutogen ]
        , maybeToList (packageAutogenDir biCabalVersion biDistDir)
        , [ componentOutputDir biComponentName biDistDir ]
        ]) ++
    [ "-stubdir=" ++ toFilePathNoTrailingSep (buildDir biDistDir) ]
  componentAutogen = componentAutogenDir biCabalVersion biComponentName biDistDir
  toIncludeDir "." = Just biCabalDir
  toIncludeDir relDir = concatAndCollapseAbsDir biCabalDir relDir
  includeOpts =
    map ("-I" <>) (biConfigIncludeDirs <> pkgIncludeOpts)
  pkgIncludeOpts =
    [ toFilePathNoTrailingSep absDir
    | dir <- includeDirs biBuildInfo
    , absDir <- handleDir dir
    ]
  libOpts =
    map ("-l" <>) (extraLibs biBuildInfo) <>
    map ("-L" <>) (biConfigLibDirs <> pkgLibDirs)
  pkgLibDirs =
    [ toFilePathNoTrailingSep absDir
    | dir <- extraLibDirs biBuildInfo
    , absDir <- handleDir dir
    ]
  handleDir dir = case (parseAbsDir dir, parseRelDir dir) of
    (Just ab, _       ) -> [ab]
    (_      , Just rel) -> [biCabalDir </> rel]
    (Nothing, Nothing ) -> []
  fworks = map ("-framework=" <>) (frameworks biBuildInfo)

-- | Make the .o path from the .c file path for a component. Example:
--
-- @
-- executable FOO
--   c-sources:        cbits/text_search.c
-- @
--
-- Produces
--
-- <dist-dir>/build/FOO/FOO-tmp/cbits/text_search.o
--
-- Example:
--
-- λ> makeObjectFilePathFromC
--     $(mkAbsDir "/Users/chris/Repos/hoogle")
--     CLib
--     $(mkAbsDir "/Users/chris/Repos/hoogle/.stack-work/Cabal-x.x.x/dist")
--     $(mkAbsFile "/Users/chris/Repos/hoogle/cbits/text_search.c")
-- Just "/Users/chris/Repos/hoogle/.stack-work/Cabal-x.x.x/dist/build/cbits/text_search.o"
-- λ> makeObjectFilePathFromC
--     $(mkAbsDir "/Users/chris/Repos/hoogle")
--     (CExe "hoogle")
--     $(mkAbsDir "/Users/chris/Repos/hoogle/.stack-work/Cabal-x.x.x/dist")
--     $(mkAbsFile "/Users/chris/Repos/hoogle/cbits/text_search.c")
-- Just "/Users/chris/Repos/hoogle/.stack-work/Cabal-x.x.x/dist/build/hoogle/hoogle-tmp/cbits/text_search.o"
-- λ>
makeObjectFilePathFromC ::
     MonadThrow m
  => Path Abs Dir      -- ^ The cabal directory.
  -> NamedComponent    -- ^ The name of the component.
  -> Path Abs Dir      -- ^ Dist directory.
  -> Path Abs File     -- ^ The path to the .c file.
  -> m (Path Abs File) -- ^ The path to the .o file for the component.
makeObjectFilePathFromC cabalDir namedComponent distDir cFilePath = do
  relCFilePath <- stripProperPrefix cabalDir cFilePath
  relOFilePath <-
    parseRelFile (replaceExtension (toFilePath relCFilePath) "o")
  pure (componentOutputDir namedComponent distDir </> relOFilePath)

-- | Get all dependencies of the package (buildable targets only).
--
-- Note that for Cabal versions 1.22 and earlier, there is a bug where Cabal
-- requires dependencies for non-buildable components to be present. We're going
-- to use GHC version as a proxy for Cabal library version in this case for
-- simplicity, so we'll check for GHC being 7.10 or earlier. This obviously
-- makes our function a lot more fun to write...
packageDependencies ::
     PackageConfig
  -> PackageDescription
  -> Map PackageName VersionRange
packageDependencies pkgConfig pkg' =
  M.fromListWith intersectVersionRanges $
  map (depPkgName &&& depVerRange) $
  concatMap targetBuildDepends (allBuildInfo' pkg) ++
  maybe [] setupDepends (setupBuildInfo pkg)
 where
  pkg
    | getGhcVersion (packageConfigCompilerVersion pkgConfig) >= mkVersion [8, 0] = pkg'
    -- Set all components to buildable. Only need to worry  library, exe, test,
    -- and bench, since others didn't exist in older Cabal versions
    | otherwise = pkg'
      { library =
          (\c -> c { libBuildInfo = go (libBuildInfo c) }) <$> library pkg'
      , executables =
          (\c -> c { buildInfo = go (buildInfo c) }) <$> executables pkg'
      , testSuites =
          if packageConfigEnableTests pkgConfig
            then (\c -> c { testBuildInfo = go (testBuildInfo c) }) <$>
                   testSuites pkg'
            else testSuites pkg'
      , benchmarks =
          if packageConfigEnableBenchmarks pkgConfig
            then (\c -> c { benchmarkBuildInfo = go (benchmarkBuildInfo c) }) <$>
                   benchmarks pkg'
            else benchmarks pkg'
      }

  go bi = bi { buildable = True }

-- | Get all dependencies of the package (buildable targets only).
--
-- This uses both the new 'buildToolDepends' and old 'buildTools' information.
packageDescTools ::
     PackageDescription
  -> (Set ExeName, Map PackageName DepValue)
packageDescTools pd =
  (S.fromList $ concat unknowns, M.fromListWith (<>) $ concat knowns)
 where
  (unknowns, knowns) = unzip $ map perBI $ allBuildInfo' pd

  perBI :: BuildInfo -> ([ExeName], [(PackageName, DepValue)])
  perBI bi =
    (unknownTools, tools)
   where
    (unknownTools, knownTools) = partitionEithers $ map go1 (buildTools bi)

    tools = mapMaybe go2 (knownTools ++ buildToolDepends bi)

    -- This is similar to desugarBuildTool from Cabal, however it
    -- uses our own hard-coded map which drops tools shipped with
    -- GHC (like hsc2hs), and includes some tools from Stackage.
    go1 :: Cabal.LegacyExeDependency -> Either ExeName Cabal.ExeDependency
    go1 (Cabal.LegacyExeDependency name range) =
      case M.lookup name hardCodedMap of
        Just pkgName ->
          Right $
            Cabal.ExeDependency pkgName (Cabal.mkUnqualComponentName name) range
        Nothing -> Left $ ExeName $ T.pack name

    go2 :: Cabal.ExeDependency -> Maybe (PackageName, DepValue)
    go2 (Cabal.ExeDependency pkg _name range)
      | pkg `S.member` preInstalledPackages = Nothing
      | otherwise = Just
          ( pkg
          , DepValue
              { dvVersionRange = range
              , dvType = AsBuildTool
              }
          )

-- | A hard-coded map for tool dependencies
hardCodedMap :: Map String PackageName
hardCodedMap = M.fromList
  [ ("alex", Distribution.Package.mkPackageName "alex")
  , ("happy", Distribution.Package.mkPackageName "happy")
  , ("cpphs", Distribution.Package.mkPackageName "cpphs")
  , ("greencard", Distribution.Package.mkPackageName "greencard")
  , ("c2hs", Distribution.Package.mkPackageName "c2hs")
  , ("hscolour", Distribution.Package.mkPackageName "hscolour")
  , ("hspec-discover", Distribution.Package.mkPackageName "hspec-discover")
  , ("hsx2hs", Distribution.Package.mkPackageName "hsx2hs")
  , ("gtk2hsC2hs", Distribution.Package.mkPackageName "gtk2hs-buildtools")
  , ("gtk2hsHookGenerator", Distribution.Package.mkPackageName "gtk2hs-buildtools")
  , ("gtk2hsTypeGen", Distribution.Package.mkPackageName "gtk2hs-buildtools")
  ]

-- | Executable-only packages which come pre-installed with GHC and do not need
-- to be built. Without this exception, we would either end up unnecessarily
-- rebuilding these packages, or failing because the packages do not appear in
-- the Stackage snapshot.
preInstalledPackages :: Set PackageName
preInstalledPackages = S.fromList
  [ mkPackageName "hsc2hs"
  , mkPackageName "haddock"
  ]

-- | Variant of 'allBuildInfo' from Cabal that, like versions before Cabal 2.2
-- only includes buildable components.
allBuildInfo' :: PackageDescription -> [BuildInfo]
allBuildInfo' pkg_descr = [ bi | lib <- allLibraries pkg_descr
                               , let bi = libBuildInfo lib
                               , buildable bi ]
                       ++ [ bi | flib <- foreignLibs pkg_descr
                               , let bi = foreignLibBuildInfo flib
                               , buildable bi ]
                       ++ [ bi | exe <- executables pkg_descr
                               , let bi = buildInfo exe
                               , buildable bi ]
                       ++ [ bi | tst <- testSuites pkg_descr
                               , let bi = testBuildInfo tst
                               , buildable bi ]
                       ++ [ bi | tst <- benchmarks pkg_descr
                               , let bi = benchmarkBuildInfo tst
                               , buildable bi ]

-- | A pair of package descriptions: one which modified the buildable values of
-- test suites and benchmarks depending on whether they are enabled, and one
-- which does not.
--
-- Fields are intentionally lazy, we may only need one or the other value.
--
-- Michael S Snoyman 2017-08-29: The very presence of this data type is terribly
-- ugly, it represents the fact that the Cabal 2.0 upgrade did _not_ go well.
-- Specifically, we used to have a field to indicate whether a component was
-- enabled in addition to buildable, but that's gone now, and this is an ugly
-- proxy. We should at some point clean up the mess of Package, LocalPackage,
-- etc, and probably pull in the definition of PackageDescription from Cabal
-- with our additionally needed metadata. But this is a good enough hack for the
-- moment. Odds are, you're reading this in the year 2024 and thinking "wtf?"
data PackageDescriptionPair = PackageDescriptionPair
  { pdpOrigBuildable :: PackageDescription
  , pdpModifiedBuildable :: PackageDescription
  }

-- | Evaluates the conditions of a 'GenericPackageDescription', yielding
-- a resolved 'PackageDescription'.
resolvePackageDescription ::
     PackageConfig
  -> GenericPackageDescription
  -> PackageDescriptionPair
resolvePackageDescription
  packageConfig
  ( GenericPackageDescription
      desc _ defaultFlags mlib subLibs foreignLibs' exes tests benches
  )
  =
  PackageDescriptionPair
    { pdpOrigBuildable = go False
    , pdpModifiedBuildable = go True
    }
 where
  go modBuildable = desc
    { library = fmap (resolveConditions rc updateLibDeps) mlib
    , subLibraries = map
        (\(n, v) -> (resolveConditions rc updateLibDeps v){libName=LSubLibName n})
        subLibs
    , foreignLibs = map
        (\(n, v) -> (resolveConditions rc updateForeignLibDeps v){foreignLibName=n})
        foreignLibs'
    , executables = map
        (\(n, v) -> (resolveConditions rc updateExeDeps v){exeName=n})
        exes
    , testSuites = map
        (\(n, v) -> (resolveConditions rc (updateTestDeps modBuildable) v){testName=n})
        tests
    , benchmarks = map
        (\(n, v) -> (resolveConditions rc (updateBenchmarkDeps modBuildable) v){benchmarkName=n})
        benches
    }

  flags = M.union (packageConfigFlags packageConfig) (flagMap defaultFlags)

  rc = mkResolveConditions
         (packageConfigCompilerVersion packageConfig)
         (packageConfigPlatform packageConfig)
         flags

  updateLibDeps lib deps = lib
    { libBuildInfo = (libBuildInfo lib) {targetBuildDepends = deps} }
  updateForeignLibDeps lib deps = lib
    { foreignLibBuildInfo =
        (foreignLibBuildInfo lib) {targetBuildDepends = deps}
    }
  updateExeDeps exe deps = exe
    { buildInfo = (buildInfo exe) {targetBuildDepends = deps} }

  -- Note that, prior to moving to Cabal 2.0, we would set testEnabled or
  -- benchmarkEnabled here. These fields no longer exist, so we modify buildable
  -- instead here. The only wrinkle in the Cabal 2.0 story is
  -- https://github.com/haskell/cabal/issues/1725, where older versions of Cabal
  -- (which may be used for actually building code) don't properly exclude
  -- build-depends for non-buildable components. Testing indicates that
  -- everything is working fine, and that this comment can be completely
  -- ignored. I'm leaving the comment anyway in case something breaks and you,
  -- poor reader, are investigating.
  updateTestDeps modBuildable test deps =
    let bi = testBuildInfo test
        bi' = bi
          { targetBuildDepends = deps
          , buildable =
                 buildable bi
              && (  not modBuildable
                 || packageConfigEnableTests packageConfig
                 )
          }
    in  test { testBuildInfo = bi' }
  updateBenchmarkDeps modBuildable benchmark deps =
    let bi = benchmarkBuildInfo benchmark
        bi' = bi
          { targetBuildDepends = deps
          , buildable =
                 buildable bi
              && (  not modBuildable
                 || packageConfigEnableBenchmarks packageConfig
                 )
          }
    in  benchmark { benchmarkBuildInfo = bi' }

-- | Make a map from a list of flag specifications.
--
-- What is @flagManual@ for?
flagMap :: [PackageFlag] -> Map FlagName Bool
flagMap = M.fromList . map pair
 where
  pair :: PackageFlag -> (FlagName, Bool)
  pair = flagName &&& flagDefault

data ResolveConditions = ResolveConditions
  { rcFlags :: Map FlagName Bool
  , rcCompilerVersion :: ActualCompiler
  , rcOS :: OS
  , rcArch :: Arch
  }

-- | Generic a @ResolveConditions@ using sensible defaults.
mkResolveConditions ::
     ActualCompiler -- ^ Compiler version
  -> Platform -- ^ installation target platform
  -> Map FlagName Bool -- ^ enabled flags
  -> ResolveConditions
mkResolveConditions compilerVersion (Platform arch os) flags = ResolveConditions
  { rcFlags = flags
  , rcCompilerVersion = compilerVersion
  , rcOS = os
  , rcArch = arch
  }

-- | Resolve the condition tree for the library.
resolveConditions ::
     (Semigroup target, Monoid target, Show target)
  => ResolveConditions
  -> (target -> cs -> target)
  -> CondTree ConfVar cs target
  -> target
resolveConditions rc addDeps (CondNode lib deps cs) = basic <> children
 where
  basic = addDeps lib deps
  children = mconcat (map apply cs)
   where
    apply (Cabal.CondBranch cond node mcs) =
      if condSatisfied cond
         then resolveConditions rc addDeps node
         else maybe mempty (resolveConditions rc addDeps) mcs
    condSatisfied c =
      case c of
        Var v -> varSatisfied v
        Lit b -> b
        CNot c' -> not (condSatisfied c')
        COr cx cy -> condSatisfied cx || condSatisfied cy
        CAnd cx cy -> condSatisfied cx && condSatisfied cy
    varSatisfied v =
      case v of
        OS os -> os == rcOS rc
        Arch arch -> arch == rcArch rc
        PackageFlag flag -> fromMaybe False $ M.lookup flag (rcFlags rc)
        -- NOTE:  ^^^^^ This should never happen, as all flags which are used
        -- must be declared. Defaulting to False.
        Impl flavor range ->
          case (flavor, rcCompilerVersion rc) of
            (GHC, ACGhc vghc) -> vghc `withinRange` range
            _ -> False

-- | Path for the package's build log.
buildLogPath ::
     (MonadReader env m, HasBuildConfig env, MonadThrow m)
  => Package
  -> Maybe String
  -> m (Path Abs File)
buildLogPath package' msuffix = do
  env <- ask
  let stack = getProjectWorkDir env
  fp <- parseRelFile $ concat $
    packageIdentifierString (packageIdentifier package') :
    maybe id (\suffix -> ("-" :) . (suffix :)) msuffix [".log"]
  pure $ stack </> relDirLogs </> fp

    {- FIXME
-- | Create a 'ProjectPackage' from a directory containing a package.
mkProjectPackage
  :: forall env. (HasPantryConfig env, HasLogFunc env, HasProcessContext env)
  => PrintWarnings
  -> ResolvedPath Dir
  -> RIO env ProjectPackage
mkProjectPackage printWarnings dir = do
  (gpd, name, cabalfp) <- loadCabalFilePath (resolvedAbsolute dir)
  pure ProjectPackage
    { ppCabalFP = cabalfp
    , ppGPD' = gpd printWarnings
    , ppResolvedDir = dir
    , ppName = name
    }

-- | Create a 'DepPackage' from a 'PackageLocation'
mkDepPackage
  :: forall env. (HasPantryConfig env, HasLogFunc env, HasProcessContext env)
  => PackageLocation
  -> RIO env DepPackage
mkDepPackage pl = do
  (name, gpdio) <-
    case pl of
      PLMutable dir -> do
        (gpdio, name, _cabalfp) <- loadCabalFilePath (resolvedAbsolute dir)
        pure (name, gpdio NoPrintWarnings)
      PLImmutable pli -> do
        PackageIdentifier name _ <- getPackageLocationIdent pli
        run <- askRunInIO
        pure (name, run $ loadCabalFileImmutable pli)
  pure DepPackage
    { dpGPD' = gpdio
    , dpLocation = pl
    , dpName = name
    }

    -}

-- | Force a package to be treated as a custom build type, see
-- <https://github.com/commercialhaskell/stack/issues/4488>
applyForceCustomBuild ::
     Version -- ^ global Cabal version
  -> Package
  -> Package
applyForceCustomBuild cabalVersion package
  | forceCustomBuild =
      package
        { packageBuildType = Custom
        , packageDeps =
            M.insertWith (<>) "Cabal" (DepValue cabalVersionRange AsLibrary) $
              packageDeps package
        , packageSetupDeps = Just $ M.fromList
            [ ("Cabal", cabalVersionRange)
            , ("base", anyVersion)
            ]
        }
  | otherwise = package
 where
  cabalVersionRange =
    orLaterVersion $ mkVersion $ cabalSpecToVersionDigits $
      packageCabalSpec package
  forceCustomBuild =
       packageBuildType package == Simple
    && not (cabalVersion `withinRange` cabalVersionRange)