cabal-install-3.12.1.0: src/Distribution/Client/FetchUtils.hs
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
{-# LANGUAGE RecordWildCards #-}
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
{-# LANGUAGE ScopedTypeVariables #-}
-- |
-- Module : Distribution.Client.FetchUtils
-- Copyright : (c) David Himmelstrup 2005
-- Duncan Coutts 2011
-- License : BSD-like
--
-- Maintainer : cabal-devel@gmail.com
-- Stability : provisional
-- Portability : portable
--
-- Functions for fetching packages
module Distribution.Client.FetchUtils
( -- * fetching packages
fetchPackage
, isFetched
, checkFetched
-- ** specifically for repo packages
, checkRepoTarballFetched
, fetchRepoTarball
, verifyFetchedTarballs
-- ** fetching packages asynchronously
, asyncFetchPackages
, waitAsyncFetchPackage
, AsyncFetchMap
-- * fetching other things
, downloadIndex
) where
import Distribution.Client.Compat.Prelude
import Prelude ()
import Distribution.Client.HttpUtils
( DownloadResult (..)
, HttpTransport (..)
, downloadURI
, isOldHackageURI
, remoteRepoCheckHttps
, transportCheckHttps
)
import Distribution.Client.Types
import Distribution.Client.GlobalFlags
( RepoContext (..)
)
import Distribution.Client.Utils
( ProgressPhase (..)
, progressMessage
)
import Distribution.Package
( PackageId
, packageName
, packageVersion
)
import Distribution.Simple.Utils
( debug
, dieWithException
, info
, notice
, warn
)
import Distribution.Verbosity
( verboseUnmarkOutput
)
import Control.Concurrent.Async
import Control.Concurrent.MVar
import qualified Control.Exception.Safe as Safe
import qualified Data.Map as Map
import Network.URI
( URI (uriPath)
)
import System.Directory
( createDirectoryIfMissing
, doesFileExist
, getFileSize
, getTemporaryDirectory
)
import System.FilePath
( (<.>)
, (</>)
)
import qualified System.FilePath.Posix as FilePath.Posix
( combine
, joinPath
)
import System.IO
( hClose
, openTempFile
)
import Control.Monad (forM)
import Distribution.Client.Errors
import qualified Hackage.Security.Client as Sec
import qualified Hackage.Security.Util.Checked as Sec
import qualified Hackage.Security.Util.Path as Sec
-- ------------------------------------------------------------
-- * Actually fetch things
-- ------------------------------------------------------------
-- | Returns @True@ if the package has already been fetched
-- or does not need fetching.
isFetched :: UnresolvedPkgLoc -> IO Bool
isFetched loc = case loc of
LocalUnpackedPackage _dir -> return True
LocalTarballPackage _file -> return True
RemoteTarballPackage _uri local -> return (isJust local)
RepoTarballPackage repo pkgid _ -> doesFileExist (packageFile repo pkgid)
RemoteSourceRepoPackage _ local -> return (isJust local)
-- | Checks if the package has already been fetched (or does not need
-- fetching) and if so returns evidence in the form of a 'PackageLocation'
-- with a resolved local file location.
checkFetched
:: UnresolvedPkgLoc
-> IO (Maybe ResolvedPkgLoc)
checkFetched loc = case loc of
LocalUnpackedPackage dir ->
return (Just $ LocalUnpackedPackage dir)
LocalTarballPackage file ->
return (Just $ LocalTarballPackage file)
RemoteTarballPackage uri (Just file) ->
return (Just $ RemoteTarballPackage uri file)
RepoTarballPackage repo pkgid (Just file) ->
return (Just $ RepoTarballPackage repo pkgid file)
RemoteSourceRepoPackage repo (Just file) ->
return (Just $ RemoteSourceRepoPackage repo file)
RemoteTarballPackage _uri Nothing -> return Nothing
RemoteSourceRepoPackage _repo Nothing -> return Nothing
RepoTarballPackage repo pkgid Nothing ->
fmap
(fmap (RepoTarballPackage repo pkgid))
(checkRepoTarballFetched repo pkgid)
-- | Like 'checkFetched' but for the specific case of a 'RepoTarballPackage'.
checkRepoTarballFetched :: Repo -> PackageId -> IO (Maybe FilePath)
checkRepoTarballFetched repo pkgid = do
let file = packageFile repo pkgid
exists <- doesFileExist file
if exists
then return (Just file)
else return Nothing
verifyFetchedTarballs
:: Verbosity
-> RepoContext
-> Repo
-> [PackageId]
-> IO
( [ Either
(Repo, PackageId) -- Verified
(Repo, PackageId) -- unverified)
]
)
verifyFetchedTarballs verbosity repoCtxt repo pkgids =
-- Establish the context once per repo (see #10110), this codepath is important
-- to be fast as it can happen when no other building happens.
let establishContext k =
case repo of
RepoSecure{} ->
repoContextWithSecureRepo repoCtxt repo $ \repoSecure ->
Sec.withIndex repoSecure $ \callbacks -> k (Just callbacks)
_ -> k Nothing
in do
establishContext $ \mCallbacks ->
forM pkgids $ \pkgid -> do
let file = packageFile repo pkgid
res <- verifyFetchedTarball verbosity file mCallbacks pkgid
return $ if res then Left (repo, pkgid) else Right (repo, pkgid)
verifyFetchedTarball :: Verbosity -> FilePath -> Maybe Sec.IndexCallbacks -> PackageId -> IO Bool
verifyFetchedTarball verbosity file mCallbacks pkgid =
let
handleError :: IO Bool -> IO Bool
handleError act = do
res <- Safe.try act
case res of
Left e -> warn verbosity ("Error verifying fetched tarball " ++ file ++ ", will redownload: " ++ show (e :: SomeException)) >> pure False
Right b -> pure b
in
handleError $ do
exists <- doesFileExist file
if not exists
then return True -- if the file does not exist, it vacuously passes validation, since it will be downloaded as necessary with what we will then check is a valid hash.
else case mCallbacks of
-- a secure repo has hashes we can compare against to confirm this is the correct file.
Just callbacks ->
let warnAndFail s = warn verbosity ("Fetched tarball " ++ file ++ " does not match server, will redownload: " ++ s) >> return False
in -- the do block in parens is due to dealing with the checked exceptions mechanism.
( do
fileInfo <- Sec.indexLookupFileInfo callbacks pkgid
sz <- Sec.FileLength . fromInteger <$> getFileSize file
if sz /= Sec.fileInfoLength (Sec.trusted fileInfo)
then warnAndFail "file length mismatch"
else do
res <- Sec.compareTrustedFileInfo (Sec.trusted fileInfo) <$> Sec.computeFileInfo (Sec.Path file :: Sec.Path Sec.Absolute)
if res
then pure True
else warnAndFail "file hash mismatch"
)
`Sec.catchChecked` (\(e :: Sec.InvalidPackageException) -> warnAndFail (show e))
`Sec.catchChecked` (\(e :: Sec.VerificationError) -> warnAndFail (show e))
_ -> pure True
-- | Fetch a package if we don't have it already.
fetchPackage
:: Verbosity
-> RepoContext
-> UnresolvedPkgLoc
-> IO ResolvedPkgLoc
fetchPackage verbosity repoCtxt loc = case loc of
LocalUnpackedPackage dir ->
return (LocalUnpackedPackage dir)
LocalTarballPackage file ->
return (LocalTarballPackage file)
RemoteTarballPackage uri (Just file) ->
return (RemoteTarballPackage uri file)
RepoTarballPackage repo pkgid (Just file) ->
return (RepoTarballPackage repo pkgid file)
RemoteSourceRepoPackage repo (Just dir) ->
return (RemoteSourceRepoPackage repo dir)
RemoteTarballPackage uri Nothing -> do
path <- downloadTarballPackage uri
return (RemoteTarballPackage uri path)
RepoTarballPackage repo pkgid Nothing -> do
local <- fetchRepoTarball verbosity repoCtxt repo pkgid
return (RepoTarballPackage repo pkgid local)
RemoteSourceRepoPackage _repo Nothing ->
dieWithException verbosity FetchPackageErr
where
downloadTarballPackage :: URI -> IO FilePath
downloadTarballPackage uri = do
transport <- repoContextGetTransport repoCtxt
transportCheckHttps verbosity transport uri
notice verbosity ("Downloading " ++ show uri)
tmpdir <- getTemporaryDirectory
(path, hnd) <- openTempFile tmpdir "cabal-.tar.gz"
hClose hnd
_ <- downloadURI transport verbosity uri path
return path
-- | Fetch a repo package if we don't have it already.
fetchRepoTarball :: Verbosity -> RepoContext -> Repo -> PackageId -> IO FilePath
fetchRepoTarball verbosity' repoCtxt repo pkgid = do
fetched <- doesFileExist (packageFile repo pkgid)
if fetched
then do
info verbosity $ prettyShow pkgid ++ " has already been downloaded."
return (packageFile repo pkgid)
else do
progressMessage verbosity ProgressDownloading (prettyShow pkgid)
res <- downloadRepoPackage
progressMessage verbosity ProgressDownloaded (prettyShow pkgid)
return res
where
-- whether we download or not is non-deterministic
verbosity = verboseUnmarkOutput verbosity'
downloadRepoPackage :: IO FilePath
downloadRepoPackage = case repo of
RepoLocalNoIndex{} -> return (packageFile repo pkgid)
RepoRemote{..} -> do
transport <- repoContextGetTransport repoCtxt
remoteRepoCheckHttps verbosity transport repoRemote
let uri = packageURI repoRemote pkgid
dir = packageDir repo pkgid
path = packageFile repo pkgid
createDirectoryIfMissing True dir
_ <- downloadURI transport verbosity uri path
return path
RepoSecure{} -> repoContextWithSecureRepo repoCtxt repo $ \rep -> do
let dir = packageDir repo pkgid
path = packageFile repo pkgid
createDirectoryIfMissing True dir
Sec.uncheckClientErrors $ do
info verbosity ("Writing " ++ path)
Sec.downloadPackage' rep pkgid path
return path
-- | Downloads an index file to [config-dir/packages/serv-id] without
-- hackage-security. You probably don't want to call this directly;
-- use 'updateRepo' instead.
downloadIndex :: HttpTransport -> Verbosity -> RemoteRepo -> FilePath -> IO DownloadResult
downloadIndex transport verbosity remoteRepo cacheDir = do
remoteRepoCheckHttps verbosity transport remoteRepo
let uri =
(remoteRepoURI remoteRepo)
{ uriPath =
uriPath (remoteRepoURI remoteRepo)
`FilePath.Posix.combine` "00-index.tar.gz"
}
path = cacheDir </> "00-index" <.> "tar.gz"
createDirectoryIfMissing True cacheDir
downloadURI transport verbosity uri path
-- ------------------------------------------------------------
-- * Async fetch wrapper utilities
-- ------------------------------------------------------------
type AsyncFetchMap =
Map
UnresolvedPkgLoc
(MVar (Either SomeException ResolvedPkgLoc))
-- | Fork off an async action to download the given packages (by location).
--
-- The downloads are initiated in order, so you can arrange for packages that
-- will likely be needed sooner to be earlier in the list.
--
-- The body action is passed a map from those packages (identified by their
-- location) to a completion var for that package. So the body action should
-- lookup the location and use 'waitAsyncFetchPackage' to get the result.
--
-- Synchronous exceptions raised by the download actions are delivered
-- via 'waitAsyncFetchPackage'.
asyncFetchPackages
:: Verbosity
-> RepoContext
-> [UnresolvedPkgLoc]
-> (AsyncFetchMap -> IO a)
-> IO a
asyncFetchPackages verbosity repoCtxt pkglocs body = do
-- TODO: [nice to have] use parallel downloads?
asyncDownloadVars <-
sequenceA
[ do
v <- newEmptyMVar
return (pkgloc, v)
| pkgloc <- pkglocs
]
let fetchPackages :: IO ()
fetchPackages =
for_ asyncDownloadVars $ \(pkgloc, var) -> do
-- Suppress marking here, because 'withAsync' means
-- that we get nondeterministic interleaving.
-- It is essential that we don't catch async exceptions here,
-- specifically 'AsyncCancelled' thrown at us from 'concurrently'.
result <-
Safe.try $
fetchPackage (verboseUnmarkOutput verbosity) repoCtxt pkgloc
putMVar var result
(_, res) <-
concurrently
fetchPackages
(body $ Map.fromList asyncDownloadVars)
pure res
-- | Expect to find a download in progress in the given 'AsyncFetchMap'
-- and wait on it to finish.
--
-- If the download failed with an exception then this will be thrown.
--
-- Note: This function is supposed to be idempotent, as our install plans
-- can now use the same tarball for many builds, e.g. different
-- components and/or qualified goals, and these all go through the
-- download phase so we end up using 'waitAsyncFetchPackage' twice on
-- the same package. C.f. #4461.
waitAsyncFetchPackage
:: Verbosity
-> AsyncFetchMap
-> UnresolvedPkgLoc
-> IO ResolvedPkgLoc
waitAsyncFetchPackage verbosity downloadMap srcloc =
case Map.lookup srcloc downloadMap of
Just hnd -> do
debug verbosity $ "Waiting for download of " ++ show srcloc
either throwIO return =<< readMVar hnd
Nothing -> fail "waitAsyncFetchPackage: package not being downloaded"
-- ------------------------------------------------------------
-- * Path utilities
-- ------------------------------------------------------------
-- | Generate the full path to the locally cached copy of
-- the tarball for a given @PackageIdentifier@.
packageFile :: Repo -> PackageId -> FilePath
packageFile repo pkgid =
packageDir repo pkgid
</> prettyShow pkgid
<.> "tar.gz"
-- | Generate the full path to the directory where the local cached copy of
-- the tarball for a given @PackageIdentifier@ is stored.
packageDir :: Repo -> PackageId -> FilePath
packageDir (RepoLocalNoIndex (LocalRepo _ dir _) _) _pkgid = dir
packageDir repo pkgid =
repoLocalDir repo
</> prettyShow (packageName pkgid)
</> prettyShow (packageVersion pkgid)
-- | Generate the URI of the tarball for a given package.
packageURI :: RemoteRepo -> PackageId -> URI
packageURI repo pkgid
| isOldHackageURI (remoteRepoURI repo) =
(remoteRepoURI repo)
{ uriPath =
FilePath.Posix.joinPath
[ uriPath (remoteRepoURI repo)
, prettyShow (packageName pkgid)
, prettyShow (packageVersion pkgid)
, prettyShow pkgid <.> "tar.gz"
]
}
packageURI repo pkgid =
(remoteRepoURI repo)
{ uriPath =
FilePath.Posix.joinPath
[ uriPath (remoteRepoURI repo)
, "package"
, prettyShow pkgid <.> "tar.gz"
]
}