cabal-install-0.6.0: Distribution/Client/Fetch.hs
-----------------------------------------------------------------------------
-- |
-- Module : Distribution.Client.Fetch
-- Copyright : (c) David Himmelstrup 2005
-- License : BSD-like
--
-- Maintainer : lemmih@gmail.com
-- Stability : provisional
-- Portability : portable
--
--
-----------------------------------------------------------------------------
module Distribution.Client.Fetch (
-- * Commands
fetch,
-- * Utilities
fetchPackage,
isFetched,
downloadIndex,
) where
import Distribution.Client.Types
( UnresolvedDependency (..), AvailablePackage(..)
, AvailablePackageSource(..), AvailablePackageDb(..)
, Repo(..), RemoteRepo(..), LocalRepo(..) )
import Distribution.Client.Dependency
( resolveDependenciesWithProgress, packagesPreference
, PackagesInstalledPreference(..) )
import Distribution.Client.Dependency.Types
( foldProgress )
import Distribution.Client.IndexUtils as IndexUtils
( getAvailablePackages, disambiguateDependencies )
import qualified Distribution.Client.InstallPlan as InstallPlan
import Distribution.Client.HttpUtils (getHTTP, isOldHackageURI)
import Distribution.Package
( PackageIdentifier, packageName, packageVersion, Dependency(..) )
import qualified Distribution.Simple.PackageIndex as PackageIndex
import Distribution.Simple.Compiler
( Compiler(compilerId), PackageDB )
import Distribution.Simple.Program
( ProgramConfiguration )
import Distribution.Simple.Configure
( getInstalledPackages )
import Distribution.Simple.Utils
( die, notice, info, debug, setupMessage
, copyFileVerbose, writeFileAtomic )
import Distribution.System
( buildOS, buildArch )
import Distribution.Text
( display )
import Distribution.Verbosity
( Verbosity )
import Control.Monad
( when, filterM )
import System.Directory
( doesFileExist, createDirectoryIfMissing )
import System.FilePath
( (</>), (<.>) )
import qualified System.FilePath.Posix as FilePath.Posix
( combine, joinPath )
import Network.URI
( URI(uriPath, uriScheme) )
import Network.HTTP
( ConnError(..), Response(..) )
downloadURI :: Verbosity
-> FilePath -- ^ Where to put it
-> URI -- ^ What to download
-> IO (Maybe ConnError)
downloadURI verbosity path uri | uriScheme uri == "file:" = do
copyFileVerbose verbosity (uriPath uri) path
return Nothing
downloadURI verbosity path uri = do
eitherResult <- getHTTP verbosity uri
case eitherResult of
Left err -> return (Just err)
Right rsp
| rspCode rsp == (2,0,0)
-> writeFileAtomic path (rspBody rsp)
--FIXME: check the content-length header matches the body length.
--TODO: stream the download into the file rather than buffering the whole
-- thing in memory.
-- remember the ETag so we can not re-download if nothing changed.
>> return Nothing
| otherwise
-> return (Just (ErrorMisc ("Unsucessful HTTP code: " ++ show (rspCode rsp))))
-- Downloads a package to [config-dir/packages/package-id] and returns the path to the package.
downloadPackage :: Verbosity -> Repo -> PackageIdentifier -> IO String
downloadPackage _ repo@Repo{ repoKind = Right LocalRepo } pkgid =
return (packageFile repo pkgid)
downloadPackage verbosity repo@Repo{ repoKind = Left remoteRepo } pkgid = do
let uri = packageURI remoteRepo pkgid
dir = packageDir repo pkgid
path = packageFile repo pkgid
debug verbosity $ "GET " ++ show uri
createDirectoryIfMissing True dir
status <- downloadURI verbosity path uri
case status of
Just err -> die $ "Failed to download '" ++ display pkgid
++ "': " ++ show err
Nothing -> return path
-- Downloads an index file to [config-dir/packages/serv-id].
downloadIndex :: Verbosity -> RemoteRepo -> FilePath -> IO FilePath
downloadIndex verbosity repo cacheDir = do
let uri = (remoteRepoURI repo) {
uriPath = uriPath (remoteRepoURI repo)
`FilePath.Posix.combine` "00-index.tar.gz"
}
path = cacheDir </> "00-index" <.> "tar.gz"
createDirectoryIfMissing True cacheDir
mbError <- downloadURI verbosity path uri
case mbError of
Just err -> die $ "Failed to download index '" ++ show err ++ "'"
Nothing -> return path
-- |Returns @True@ if the package has already been fetched.
isFetched :: AvailablePackage -> IO Bool
isFetched (AvailablePackage pkgid _ source) = case source of
LocalUnpackedPackage -> return True
RepoTarballPackage repo -> doesFileExist (packageFile repo pkgid)
-- |Fetch a package if we don't have it already.
fetchPackage :: Verbosity -> Repo -> PackageIdentifier -> IO String
fetchPackage verbosity repo pkgid = do
fetched <- doesFileExist (packageFile repo pkgid)
if fetched
then do notice verbosity $ "'" ++ display pkgid ++ "' is cached."
return (packageFile repo pkgid)
else do setupMessage verbosity "Downloading" pkgid
downloadPackage verbosity repo pkgid
-- |Fetch a list of packages and their dependencies.
fetch :: Verbosity
-> PackageDB
-> [Repo]
-> Compiler
-> ProgramConfiguration
-> [UnresolvedDependency]
-> IO ()
fetch verbosity packageDB repos comp conf deps = do
installed <- getInstalledPackages verbosity comp packageDB conf
AvailablePackageDb available versionPref
<- getAvailablePackages verbosity repos
deps' <- IndexUtils.disambiguateDependencies available deps
let -- Hide the packages given on the command line so that the dep resolver
-- will decide that they need fetching, even if they're already
-- installed. Sicne we want to get the source packages of things we might
-- have installed (but not have the sources for).
installed' = fmap (hideGivenDeps deps') installed
hideGivenDeps pkgs index =
foldr PackageIndex.deletePackageName index
[ name | UnresolvedDependency (Dependency name _) _ <- pkgs ]
let progress = resolveDependenciesWithProgress
buildOS buildArch (compilerId comp)
installed' available
(packagesPreference PreferLatestForSelected versionPref)
deps'
notice verbosity "Resolving dependencies..."
maybePlan <- foldProgress (\message rest -> info verbosity message >> rest)
(return . Left) (return . Right) progress
case maybePlan of
Left message -> die message
Right pkgs -> do
ps <- filterM (fmap not . isFetched)
[ pkg | (InstallPlan.Configured
(InstallPlan.ConfiguredPackage pkg _ _))
<- InstallPlan.toList pkgs ]
when (null ps) $
notice verbosity $ "No packages need to be fetched. "
++ "All the requested packages are already cached."
sequence_
[ fetchPackage verbosity repo pkgid
| (AvailablePackage pkgid _ (RepoTarballPackage repo)) <- ps ]
-- |Generate the full path to the locally cached copy of
-- the tarball for a given @PackageIdentifer@.
packageFile :: Repo -> PackageIdentifier -> FilePath
packageFile repo pkgid = packageDir repo pkgid
</> display pkgid
<.> "tar.gz"
-- |Generate the full path to the directory where the local cached copy of
-- the tarball for a given @PackageIdentifer@ is stored.
packageDir :: Repo -> PackageIdentifier -> FilePath
packageDir repo pkgid = repoLocalDir repo
</> display (packageName pkgid)
</> display (packageVersion pkgid)
-- | Generate the URI of the tarball for a given package.
packageURI :: RemoteRepo -> PackageIdentifier -> URI
packageURI repo pkgid | isOldHackageURI (remoteRepoURI repo) =
(remoteRepoURI repo) {
uriPath = FilePath.Posix.joinPath
[uriPath (remoteRepoURI repo)
,display (packageName pkgid)
,display (packageVersion pkgid)
,display pkgid <.> "tar.gz"]
}
packageURI repo pkgid =
(remoteRepoURI repo) {
uriPath = FilePath.Posix.joinPath
[uriPath (remoteRepoURI repo)
,"packages"
,display pkgid
,"tarball"]
}