stack-2.9.3: src/Stack/Types/Package.hs
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
module Stack.Types.Package
( BuildInfoOpts (..)
, ExeName (..)
, FileCacheInfo (..)
, GetPackageOpts (..)
, InstallLocation (..)
, InstallMap
, Installed (..)
, InstalledPackageLocation (..)
, InstalledMap
, LocalPackage (..)
, MemoizedWith (..)
, Package (..)
, PackageConfig (..)
, PackageException (..)
, PackageLibraries (..)
, PackageSource (..)
, dotCabalCFilePath
, dotCabalGetPath
, dotCabalMain
, dotCabalMainPath
, dotCabalModule
, dotCabalModulePath
, installedPackageIdentifier
, installedVersion
, lpFiles
, lpFilesForComponents
, memoizeRefWith
, packageDefinedFlags
, packageIdent
, packageIdentifier
, psVersion
, runMemoizedWith
) where
import Stack.Prelude
import qualified RIO.Text as T
import Data.Aeson
( ToJSON (..), FromJSON (..), (.=), (.:), object, withObject
)
import qualified Data.Map as M
import qualified Data.Set as Set
import Distribution.CabalSpecVersion
import Distribution.Parsec ( PError (..), PWarning (..), showPos )
import qualified Distribution.SPDX.License as SPDX
import Distribution.License ( License )
import Distribution.ModuleName ( ModuleName )
import Distribution.PackageDescription
( TestSuiteInterface, BuildType )
import Distribution.System ( Platform (..) )
import Stack.Types.Compiler
import Stack.Types.Config
import Stack.Types.GhcPkgId
import Stack.Types.NamedComponent
import Stack.Types.SourceMap
import Stack.Types.Version
import Stack.Types.Dependency ( DepValue )
import Stack.Types.PackageFile
( GetPackageFiles (..), DotCabalDescriptor (..)
, DotCabalPath (..)
)
-- | Type representing exceptions thrown by functions exported by the
-- "Stack.Package" module.
data PackageException
= PackageInvalidCabalFile
!(Either PackageIdentifierRevision (Path Abs File))
!(Maybe Version)
![PError]
![PWarning]
| MismatchedCabalIdentifier !PackageIdentifierRevision !PackageIdentifier
| CabalFileNameParseFail FilePath
| CabalFileNameInvalidPackageName FilePath
| ComponentNotParsedBug
deriving (Show, Typeable)
instance Exception PackageException where
displayException (PackageInvalidCabalFile loc _mversion errs warnings) = concat
[ "Error: [S-8072]\n"
, "Unable to parse Cabal file "
, case loc of
Left pir -> "for " ++ T.unpack (utf8BuilderToText (display pir))
Right fp -> toFilePath fp
{-
Not actually needed, the errors will indicate if a newer version exists.
Also, it seems that this is set to Just the version even if we support it.
, case mversion of
Nothing -> ""
Just version -> "\nRequires newer Cabal file parser version: " ++
versionString version
-}
, "\n\n"
, unlines $ map
(\(PError pos msg) -> concat
[ "- "
, showPos pos
, ": "
, msg
])
errs
, unlines $ map
(\(PWarning _ pos msg) -> concat
[ "- "
, showPos pos
, ": "
, msg
])
warnings
]
displayException (MismatchedCabalIdentifier pir ident) = concat
[ "Error: [S-5394]\n"
, "Mismatched package identifier."
, "\nFound: "
, packageIdentifierString ident
, "\nExpected: "
, T.unpack $ utf8BuilderToText $ display pir
]
displayException (CabalFileNameParseFail fp) = concat
[ "Error: [S-2203]\n"
, "Invalid file path for Cabal file, must have a .cabal extension: "
, fp
]
displayException (CabalFileNameInvalidPackageName fp) = concat
[ "Error: [S-8854]\n"
, "Cabal file names must use valid package names followed by a .cabal \
\extension, the following is invalid: "
, fp
]
displayException ComponentNotParsedBug = bugReport "[S-4623]"
"Component names should always parse as directory names."
-- | Libraries in a package. Since Cabal 2.0, internal libraries are a
-- thing.
data PackageLibraries
= NoLibraries
| HasLibraries !(Set Text) -- ^ the foreign library names, sub libraries get built automatically without explicit component name passing
deriving (Show,Typeable)
-- | Name of an executable.
newtype ExeName = ExeName { unExeName :: Text }
deriving (Show, Eq, Ord, Hashable, IsString, Generic, NFData, Data, Typeable)
-- | Some package info.
data Package =
Package {packageName :: !PackageName -- ^ Name of the package.
,packageVersion :: !Version -- ^ Version of the package
,packageLicense :: !(Either SPDX.License License) -- ^ The license the package was released under.
,packageFiles :: !GetPackageFiles -- ^ Get all files of the package.
,packageDeps :: !(Map PackageName DepValue) -- ^ Packages that the package depends on, both as libraries and build tools.
,packageUnknownTools :: !(Set ExeName) -- ^ Build tools specified in the legacy manner (build-tools:) that failed the hard-coded lookup.
,packageAllDeps :: !(Set PackageName) -- ^ Original dependencies (not sieved).
,packageGhcOptions :: ![Text] -- ^ Ghc options used on package.
,packageCabalConfigOpts :: ![Text] -- ^ Additional options passed to ./Setup.hs configure
,packageFlags :: !(Map FlagName Bool) -- ^ Flags used on package.
,packageDefaultFlags :: !(Map FlagName Bool) -- ^ Defaults for unspecified flags.
,packageLibraries :: !PackageLibraries -- ^ does the package have a buildable library stanza?
,packageInternalLibraries :: !(Set Text) -- ^ names of internal libraries
,packageTests :: !(Map Text TestSuiteInterface) -- ^ names and interfaces of test suites
,packageBenchmarks :: !(Set Text) -- ^ names of benchmarks
,packageExes :: !(Set Text) -- ^ names of executables
,packageOpts :: !GetPackageOpts -- ^ Args to pass to GHC.
,packageHasExposedModules :: !Bool -- ^ Does the package have exposed modules?
,packageBuildType :: !BuildType -- ^ Package build-type.
,packageSetupDeps :: !(Maybe (Map PackageName VersionRange))
-- ^ If present: custom-setup dependencies
,packageCabalSpec :: !CabalSpecVersion -- ^ Cabal spec range
}
deriving (Show,Typeable)
packageIdent :: Package -> PackageIdentifier
packageIdent p = PackageIdentifier (packageName p) (packageVersion p)
packageIdentifier :: Package -> PackageIdentifier
packageIdentifier pkg =
PackageIdentifier (packageName pkg) (packageVersion pkg)
packageDefinedFlags :: Package -> Set FlagName
packageDefinedFlags = M.keysSet . packageDefaultFlags
type InstallMap = Map PackageName (InstallLocation, Version)
-- | Files that the package depends on, relative to package directory.
-- Argument is the location of the Cabal file
newtype GetPackageOpts = GetPackageOpts
{ getPackageOpts :: forall env. HasEnvConfig env
=> InstallMap
-> InstalledMap
-> [PackageName]
-> [PackageName]
-> Path Abs File
-> RIO env
(Map NamedComponent (Map ModuleName (Path Abs File))
,Map NamedComponent [DotCabalPath]
,Map NamedComponent BuildInfoOpts)
}
instance Show GetPackageOpts where
show _ = "<GetPackageOpts>"
-- | GHC options based on cabal information and ghc-options.
data BuildInfoOpts = BuildInfoOpts
{ bioOpts :: [String]
, bioOneWordOpts :: [String]
, bioPackageFlags :: [String]
-- ^ These options can safely have 'nubOrd' applied to them, as
-- there are no multi-word options (see
-- https://github.com/commercialhaskell/stack/issues/1255)
, bioCabalMacros :: Path Abs File
} deriving Show
-- | Package build configuration
data PackageConfig =
PackageConfig {packageConfigEnableTests :: !Bool -- ^ Are tests enabled?
,packageConfigEnableBenchmarks :: !Bool -- ^ Are benchmarks enabled?
,packageConfigFlags :: !(Map FlagName Bool) -- ^ Configured flags.
,packageConfigGhcOptions :: ![Text] -- ^ Configured ghc options.
,packageConfigCabalConfigOpts :: ![Text] -- ^ ./Setup.hs configure options
,packageConfigCompilerVersion :: ActualCompiler -- ^ GHC version
,packageConfigPlatform :: !Platform -- ^ host platform
}
deriving (Show,Typeable)
-- | Compares the package name.
instance Ord Package where
compare = on compare packageName
-- | Compares the package name.
instance Eq Package where
(==) = on (==) packageName
-- | Where the package's source is located: local directory or package index
data PackageSource
= PSFilePath LocalPackage
-- ^ Package which exist on the filesystem
| PSRemote PackageLocationImmutable Version FromSnapshot CommonPackage
-- ^ Package which is downloaded remotely.
instance Show PackageSource where
show (PSFilePath lp) = concat ["PSFilePath (", show lp, ")"]
show (PSRemote pli v fromSnapshot _) =
concat
[ "PSRemote"
, "(", show pli, ")"
, "(", show v, ")"
, show fromSnapshot
, "<CommonPackage>"
]
psVersion :: PackageSource -> Version
psVersion (PSFilePath lp) = packageVersion $ lpPackage lp
psVersion (PSRemote _ v _ _) = v
-- | Information on a locally available package of source code
data LocalPackage = LocalPackage
{ lpPackage :: !Package
-- ^ The @Package@ info itself, after resolution with package flags,
-- with tests and benchmarks disabled
, lpComponents :: !(Set NamedComponent)
-- ^ Components to build, not including the library component.
, lpUnbuildable :: !(Set NamedComponent)
-- ^ Components explicitly requested for build, that are marked
-- "buildable: false".
, lpWanted :: !Bool -- FIXME Should completely drop this "wanted" terminology, it's unclear
-- ^ Whether this package is wanted as a target.
, lpTestBench :: !(Maybe Package)
-- ^ This stores the 'Package' with tests and benchmarks enabled, if
-- either is asked for by the user.
, lpCabalFile :: !(Path Abs File)
-- ^ The Cabal file
, lpBuildHaddocks :: !Bool
, lpForceDirty :: !Bool
, lpDirtyFiles :: !(MemoizedWith EnvConfig (Maybe (Set FilePath)))
-- ^ Nothing == not dirty, Just == dirty. Note that the Set may be empty if
-- we forced the build to treat packages as dirty. Also, the Set may not
-- include all modified files.
, lpNewBuildCaches :: !(MemoizedWith EnvConfig (Map NamedComponent (Map FilePath FileCacheInfo)))
-- ^ current state of the files
, lpComponentFiles :: !(MemoizedWith EnvConfig (Map NamedComponent (Set (Path Abs File))))
-- ^ all files used by this package
}
deriving Show
newtype MemoizedWith env a = MemoizedWith { unMemoizedWith :: RIO env a }
deriving (Functor, Applicative, Monad)
memoizeRefWith :: MonadIO m => RIO env a -> m (MemoizedWith env a)
memoizeRefWith action = do
ref <- newIORef Nothing
pure $ MemoizedWith $ do
mres <- readIORef ref
res <-
case mres of
Just res -> pure res
Nothing -> do
res <- tryAny action
writeIORef ref $ Just res
pure res
either throwIO pure res
runMemoizedWith ::
(HasEnvConfig env, MonadReader env m, MonadIO m)
=> MemoizedWith EnvConfig a
-> m a
runMemoizedWith (MemoizedWith action) = do
envConfig <- view envConfigL
runRIO envConfig action
instance Show (MemoizedWith env a) where
show _ = "<<MemoizedWith>>"
lpFiles :: HasEnvConfig env => LocalPackage -> RIO env (Set.Set (Path Abs File))
lpFiles = runMemoizedWith . fmap (Set.unions . M.elems) . lpComponentFiles
lpFilesForComponents :: HasEnvConfig env
=> Set NamedComponent
-> LocalPackage
-> RIO env (Set.Set (Path Abs File))
lpFilesForComponents components lp = runMemoizedWith $ do
componentFiles <- lpComponentFiles lp
pure $ mconcat (M.elems (M.restrictKeys componentFiles components))
-- | A location to install a package into, either snapshot or local
data InstallLocation = Snap | Local
deriving (Show, Eq)
instance Semigroup InstallLocation where
Local <> _ = Local
_ <> Local = Local
Snap <> Snap = Snap
instance Monoid InstallLocation where
mempty = Snap
mappend = (<>)
data InstalledPackageLocation = InstalledTo InstallLocation | ExtraGlobal
deriving (Show, Eq)
newtype FileCacheInfo = FileCacheInfo
{ fciHash :: SHA256
}
deriving (Generic, Show, Eq, Typeable)
instance NFData FileCacheInfo
-- Provided for storing the BuildCache values in a file. But maybe
-- JSON/YAML isn't the right choice here, worth considering.
instance ToJSON FileCacheInfo where
toJSON (FileCacheInfo hash') = object
[ "hash" .= hash'
]
instance FromJSON FileCacheInfo where
parseJSON = withObject "FileCacheInfo" $ \o -> FileCacheInfo
<$> o .: "hash"
-- | Maybe get the module name from the .cabal descriptor.
dotCabalModule :: DotCabalDescriptor -> Maybe ModuleName
dotCabalModule (DotCabalModule m) = Just m
dotCabalModule _ = Nothing
-- | Maybe get the main name from the .cabal descriptor.
dotCabalMain :: DotCabalDescriptor -> Maybe FilePath
dotCabalMain (DotCabalMain m) = Just m
dotCabalMain _ = Nothing
-- | Get the module path.
dotCabalModulePath :: DotCabalPath -> Maybe (Path Abs File)
dotCabalModulePath (DotCabalModulePath fp) = Just fp
dotCabalModulePath _ = Nothing
-- | Get the main path.
dotCabalMainPath :: DotCabalPath -> Maybe (Path Abs File)
dotCabalMainPath (DotCabalMainPath fp) = Just fp
dotCabalMainPath _ = Nothing
-- | Get the c file path.
dotCabalCFilePath :: DotCabalPath -> Maybe (Path Abs File)
dotCabalCFilePath (DotCabalCFilePath fp) = Just fp
dotCabalCFilePath _ = Nothing
-- | Get the path.
dotCabalGetPath :: DotCabalPath -> Path Abs File
dotCabalGetPath dcp =
case dcp of
DotCabalModulePath fp -> fp
DotCabalMainPath fp -> fp
DotCabalFilePath fp -> fp
DotCabalCFilePath fp -> fp
type InstalledMap = Map PackageName (InstallLocation, Installed)
data Installed
= Library PackageIdentifier GhcPkgId (Maybe (Either SPDX.License License))
| Executable PackageIdentifier
deriving (Show, Eq)
installedPackageIdentifier :: Installed -> PackageIdentifier
installedPackageIdentifier (Library pid _ _) = pid
installedPackageIdentifier (Executable pid) = pid
-- | Get the installed Version.
installedVersion :: Installed -> Version
installedVersion i =
let PackageIdentifier _ version = installedPackageIdentifier i
in version