{-# LANGUAGE GeneralizedNewtypeDeriving, DeriveDataTypeable, TemplateHaskell, ScopedTypeVariables, OverloadedStrings #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Distribution.HaskellSuite.Packages ( -- * Querying package databases -- | 'getInstalledPackages' and 'readPackagesInfo' can be used to get -- package information from package databases. -- -- They use the 'IsPackageDB' interface, so that you can use them with -- your own, custom databases. -- -- Use 'getInstalledPackages' to get all packages defined in a particular -- database, and 'readPackagesInfo' when you're searching for -- a particular set of packages in a set of databases. Packages , getInstalledPackages , readPackagesInfo -- * IsPackageDB class and friends , IsPackageDB(..) , MaybeInitDB(..) , maybeInitDB -- * StandardDB -- | 'StandardDB' is a simple `IsPackageDB` implementation which cover many -- (but not all) use cases. Please see the source code to see what -- assumptions it makes and whether they hold for your use case. , StandardDB(..) , IsDBName(..) -- * Relative paths in package databases -- | Traditionally, the paths in package databases are absolute. -- -- haskell-packages allows relative file paths in databases, which is -- useful in some cases (e.g. relocatable global package database). -- -- By default, 'readPackageDB' (for 'StandardDB') treats relative paths -- as being relative to the database path. -- -- However, Cabal still passes absolute file names, and by default -- 'writePackageDB' stores them verbatim. To change this, use -- 'makePkgInfoRelative' in your implementation of 'writePackageDB'. , makePkgInfoRelative , makePkgInfoAbsolute , mapPaths -- * Direct database manipulation -- | 'writeDB' and 'readDB' perform (de)serialization of a package -- database using a simple JSON encoding. You may use these to implement -- 'writePackageDB' and 'readPackageDB' for your own databases. , writeDB , readDB , initDB -- * Exceptions , PkgDBError(..) , PkgInfoError(..) ) where import Data.Aeson import Data.Aeson.TH import Data.Aeson.Types import Control.Applicative import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as LBS import Control.Exception as E import Control.Monad import Data.Typeable import Data.Tagged import Data.Proxy import qualified Data.Map as Map import Text.Printf import qualified Distribution.InstalledPackageInfo as Info import Distribution.Package import Distribution.Text import System.FilePath import System.Directory -- The following imports are needed only for JSON instances import Distribution.Simple.Compiler (PackageDB(..)) import Distribution.License (License(..)) import Distribution.ModuleName(ModuleName) import Distribution.Simple.Utils import Distribution.Verbosity -------------- -- Querying -- -------------- type Packages = [Info.InstalledPackageInfo] -- | Get all packages that are registered in a particular database -- -- If the database doesn't exist, the behaviour is determined by -- 'maybeInitDB'. getInstalledPackages :: forall db. IsPackageDB db => Proxy db -> PackageDB -> IO Packages getInstalledPackages _proxy dbspec = do mbDb <- locateDB dbspec maybe (return []) (readPackageDB $ maybeInitDB dbspec) (mbDb :: Maybe db) -- | Try to retrieve an 'InstalledPackageInfo' for each of -- 'UnitId's from a specified set of 'PackageDB's. -- -- May throw a 'PkgInfoNotFound' exception. -- -- If a database doesn't exist, the behaviour is determined by -- 'maybeInitDB'. readPackagesInfo :: IsPackageDB db => Proxy db -> [PackageDB] -> [UnitId] -> IO Packages readPackagesInfo proxyDb dbs pkgIds = do allPkgInfos <- concat <$> mapM (getInstalledPackages proxyDb) dbs let pkgMap = Map.fromList [ (Info.installedUnitId pkgInfo, pkgInfo) | pkgInfo <- allPkgInfos ] forM pkgIds $ \pkgId -> maybe (throwIO $ PkgInfoNotFound pkgId) return (Map.lookup pkgId pkgMap) --------------------------- -- IsPackageDB & friends -- --------------------------- -- | Package database class. -- -- @db@ will typically be a newtype-wrapped path to the database file, -- although more sophisticated setups are certainly possible. -- -- Consider using 'StandardDB' first, and implement your own database -- type if that isn't enough. class IsPackageDB db where -- | The name of the database. Used to construct some paths. dbName :: Tagged db String -- | Read a package database. -- -- If the database does not exist, then the first argument tells whether -- we should create and initialize it with an empty package list. In -- that case, if 'Don'tInitDB' is specified, a 'BadPkgDb' exception is -- thrown. readPackageDB :: MaybeInitDB -> db -> IO Packages -- | Write a package database writePackageDB :: db -> Packages -> IO () -- | Get the location of a global package database (if there's one) globalDB :: IO (Maybe db) -- | Create a db object given a database file path dbFromPath :: FilePath -> IO db -- Methods that have default implementations -- | Convert a package db specification to a db object locateDB :: PackageDB -> IO (Maybe db) locateDB GlobalPackageDB = globalDB locateDB UserPackageDB = Just <$> userDB locateDB (SpecificPackageDB p) = Just <$> dbFromPath p -- | The user database userDB :: IO db userDB = do let name = untag (dbName :: Tagged db String) path <- () <$> haskellPackagesDir <*> pure (name <.> "db") dbFromPath path -- | A flag which tells whether the library should create an empty package -- database if it doesn't exist yet data MaybeInitDB = InitDB | Don'tInitDB -- | This function determines whether a package database should be -- initialized if it doesn't exist yet. -- -- The rule is this: if it is a global or a user database, then initialize -- it; otherwise, don't. -- -- Rationale: if the database was specified by the user, she could have -- made a mistake in the path, and we'd rather report it. On the other -- hand, it is our responsibility to ensure that the user and global -- databases exist. maybeInitDB :: PackageDB -> MaybeInitDB maybeInitDB GlobalPackageDB = InitDB maybeInitDB UserPackageDB = InitDB maybeInitDB SpecificPackageDB {} = Don'tInitDB ---------------- -- StandardDB -- ---------------- class IsDBName name where getDBName :: Tagged name String data StandardDB name = StandardDB FilePath instance IsDBName name => IsPackageDB (StandardDB name) where dbName = retag (getDBName :: Tagged name String) readPackageDB init (StandardDB db) = map (makePkgInfoAbsolute (dropFileName db)) <$> readDB init db writePackageDB (StandardDB db) = writeDB db globalDB = return Nothing dbFromPath path = return $ StandardDB path --------------------------------- -- Absolute and relative paths -- --------------------------------- -- | Make all paths in the package info relative to the given base -- directory. makePkgInfoRelative :: FilePath -> Info.InstalledPackageInfo -> Info.InstalledPackageInfo makePkgInfoRelative base info = mapPaths (makeRelative base) info -- | Make all relative paths in the package info absolute, interpreting -- them relative to the given base directory. makePkgInfoAbsolute :: FilePath -> Info.InstalledPackageInfo -> Info.InstalledPackageInfo makePkgInfoAbsolute base info = flip mapPaths info $ \f -> if isRelative f then base f else f -- | Apply a given function to all file paths contained in the package info mapPaths :: (FilePath -> FilePath) -> (Info.InstalledPackageInfo -> Info.InstalledPackageInfo) mapPaths f info = info { Info.importDirs = map f (Info.importDirs info) , Info.libraryDirs = map f (Info.libraryDirs info) , Info.includeDirs = map f (Info.includeDirs info) , Info.frameworkDirs = map f (Info.frameworkDirs info) , Info.haddockInterfaces = map f (Info.haddockInterfaces info) , Info.haddockHTMLs = map f (Info.haddockHTMLs info) } ------------------------- -- Auxiliary functions -- ------------------------- writeDB :: FilePath -> Packages -> IO () writeDB path db = LBS.writeFile path $ encode db readDB :: MaybeInitDB -> FilePath -> IO Packages readDB maybeInit path = do maybeDoInitDB cts <- LBS.fromChunks . return <$> BS.readFile path `E.catch` \e -> throwIO $ PkgDBReadError path e maybe (throwIO $ BadPkgDB path) return $ decode' cts where maybeDoInitDB | InitDB <- maybeInit = initDB path | otherwise = return () -- | If the path does not exist, create an empty database there. Otherwise, -- do nothing. initDB :: FilePath -> IO () initDB path = do dbExists <- doesFileExist path unless dbExists $ do createDirectoryIfMissingVerbose silent True (dropFileName path) writeDB path [] haskellPackagesDir :: IO FilePath haskellPackagesDir = getAppUserDataDirectory "haskell-packages" ---------------- -- Exceptions -- ---------------- errPrefix :: String errPrefix = "haskell-suite package manager" data PkgDBError = BadPkgDB FilePath -- ^ package database could not be parsed or contains errors | PkgDBReadError FilePath IOException -- ^ package db file could not be read | PkgExists UnitId -- ^ attempt to register an already present package id | RegisterNullDB -- ^ attempt to register in the global db when it's not present deriving (Typeable) instance Show PkgDBError where show (BadPkgDB path) = printf "%s: bad package database at %s" errPrefix path show (PkgDBReadError path e) = printf "%s: package db at %s could not be read: %s" errPrefix path (show e) show (PkgExists pkgid) = printf "%s: package %s is already in the database" errPrefix (display pkgid) show (RegisterNullDB) = printf "%s: attempt to register in a null global db" errPrefix instance Exception PkgDBError data PkgInfoError = PkgInfoNotFound UnitId -- ^ requested package id could not be found in any of the package databases deriving Typeable instance Exception PkgInfoError instance Show PkgInfoError where show (PkgInfoNotFound pkgid) = printf "%s: package not found: %s" errPrefix (display pkgid) --------------------- -- Aeson instances -- --------------------- stdToJSON :: Text a => a -> Value stdToJSON = toJSON . display stdFromJSON :: Text a => Value -> Parser a stdFromJSON = maybe mzero return . simpleParse <=< parseJSON instance ToJSON License where toJSON = stdToJSON instance FromJSON License where parseJSON = stdFromJSON instance ToJSON ModuleName where toJSON = stdToJSON instance FromJSON ModuleName where parseJSON = stdFromJSON instance ToJSON PackageName where toJSON = stdToJSON instance FromJSON PackageName where parseJSON = stdFromJSON instance ToJSON PackageIdentifier where toJSON = stdToJSON instance FromJSON PackageIdentifier where parseJSON = stdFromJSON instance ToJSON UnitId where toJSON = stdToJSON instance FromJSON UnitId where parseJSON = stdFromJSON instance ToJSON AbiHash where toJSON = stdToJSON instance FromJSON AbiHash where parseJSON = stdFromJSON deriveJSON defaultOptions ''Info.OriginalModule deriveJSON defaultOptions ''Info.ExposedModule deriveJSON defaultOptions ''Info.InstalledPackageInfo