stack-2.11.1: src/Stack/Types/Package.hs
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
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 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 qualified RIO.Text as T
import Stack.Prelude
import Stack.Types.Compiler ( ActualCompiler )
import Stack.Types.Dependency ( DepValue )
import Stack.Types.EnvConfig ( EnvConfig, HasEnvConfig (..) )
import Stack.Types.GhcPkgId ( GhcPkgId )
import Stack.Types.NamedComponent ( NamedComponent )
import Stack.Types.PackageFile
( GetPackageFiles (..), DotCabalDescriptor (..)
, DotCabalPath (..)
)
import Stack.Types.SourceMap ( CommonPackage, FromSnapshot )
import Stack.Types.Version ( VersionRange )
-- | 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 (Data, Eq, Generic, Hashable, IsString, NFData, Ord, Show, 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).
, packageSubLibDeps :: !(Map MungedPackageName DepValue)
-- ^ Original sub-library 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 (Applicative, Functor, 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 (Eq, Show)
instance Semigroup InstallLocation where
Local <> _ = Local
_ <> Local = Local
Snap <> Snap = Snap
instance Monoid InstallLocation where
mempty = Snap
mappend = (<>)
data InstalledPackageLocation
= InstalledTo InstallLocation | ExtraGlobal
deriving (Eq, Show)
newtype FileCacheInfo = FileCacheInfo
{ fciHash :: SHA256
}
deriving (Eq, Generic, Show, 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 (Eq, Show)
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