1 -----------------------------------------------------------------------------
2 -----------------------------------------------------------------------------
3 {-# LANGUAGE RecordWildCards #-}
4 -----------------------------------------------------------------------------
5 -----------------------------------------------------------------------------
6 {-# LANGUAGE ScopedTypeVariables #-}
9 -- Module : Distribution.Client.FetchUtils
10 -- Copyright : (c) David Himmelstrup 2005
14 -- Maintainer : cabal-devel@gmail.com
15 -- Stability : provisional
16 -- Portability : portable
18 -- Functions for fetching packages
19 module Distribution
.Client
.FetchUtils
20 ( -- * fetching packages
25 -- ** specifically for repo packages
26 , checkRepoTarballFetched
28 , verifyFetchedTarball
30 -- ** fetching packages asynchronously
32 , waitAsyncFetchPackage
35 -- * fetching other things
39 import Distribution
.Client
.Compat
.Prelude
42 import Distribution
.Client
.HttpUtils
47 , remoteRepoCheckHttps
50 import Distribution
.Client
.Types
52 import Distribution
.Client
.GlobalFlags
55 import Distribution
.Client
.Utils
59 import Distribution
.Package
64 import Distribution
.Simple
.Utils
71 import Distribution
.Verbosity
75 import Control
.Concurrent
.Async
76 import Control
.Concurrent
.MVar
77 import qualified Control
.Exception
.Safe
as Safe
78 import qualified Data
.Map
as Map
82 import System
.Directory
83 ( createDirectoryIfMissing
86 , getTemporaryDirectory
88 import System
.FilePath
92 import qualified System
.FilePath.Posix
as FilePath.Posix
101 import Distribution
.Client
.Errors
102 import qualified Hackage
.Security
.Client
as Sec
103 import qualified Hackage
.Security
.Util
.Checked
as Sec
104 import qualified Hackage
.Security
.Util
.Path
as Sec
106 -- ------------------------------------------------------------
108 -- * Actually fetch things
110 -- ------------------------------------------------------------
112 -- | Returns @True@ if the package has already been fetched
113 -- or does not need fetching.
114 isFetched
:: UnresolvedPkgLoc
-> IO Bool
115 isFetched loc
= case loc
of
116 LocalUnpackedPackage _dir
-> return True
117 LocalTarballPackage _file
-> return True
118 RemoteTarballPackage _uri local
-> return (isJust local
)
119 RepoTarballPackage repo pkgid _
-> doesFileExist (packageFile repo pkgid
)
120 RemoteSourceRepoPackage _ local
-> return (isJust local
)
122 -- | Checks if the package has already been fetched (or does not need
123 -- fetching) and if so returns evidence in the form of a 'PackageLocation'
124 -- with a resolved local file location.
127 -> IO (Maybe ResolvedPkgLoc
)
128 checkFetched loc
= case loc
of
129 LocalUnpackedPackage dir
->
130 return (Just
$ LocalUnpackedPackage dir
)
131 LocalTarballPackage file
->
132 return (Just
$ LocalTarballPackage file
)
133 RemoteTarballPackage uri
(Just file
) ->
134 return (Just
$ RemoteTarballPackage uri file
)
135 RepoTarballPackage repo pkgid
(Just file
) ->
136 return (Just
$ RepoTarballPackage repo pkgid file
)
137 RemoteSourceRepoPackage repo
(Just file
) ->
138 return (Just
$ RemoteSourceRepoPackage repo file
)
139 RemoteTarballPackage _uri Nothing
-> return Nothing
140 RemoteSourceRepoPackage _repo Nothing
-> return Nothing
141 RepoTarballPackage repo pkgid Nothing
->
143 (fmap (RepoTarballPackage repo pkgid
))
144 (checkRepoTarballFetched repo pkgid
)
146 -- | Like 'checkFetched' but for the specific case of a 'RepoTarballPackage'.
147 checkRepoTarballFetched
:: Repo
-> PackageId
-> IO (Maybe FilePath)
148 checkRepoTarballFetched repo pkgid
= do
149 let file
= packageFile repo pkgid
150 exists
<- doesFileExist file
152 then return (Just file
)
155 verifyFetchedTarball
:: Verbosity
-> RepoContext
-> Repo
-> PackageId
-> IO Bool
156 verifyFetchedTarball verbosity repoCtxt repo pkgid
=
157 let file
= packageFile repo pkgid
158 handleError
:: IO Bool -> IO Bool
162 Left e
-> warn verbosity
("Error verifying fetched tarball " ++ file
++ ", will redownload: " ++ show (e
:: SomeException
)) >> pure
False
165 exists
<- doesFileExist file
167 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.
169 -- a secure repo has hashes we can compare against to confirm this is the correct file.
171 repoContextWithSecureRepo repoCtxt repo
$ \repoSecure
->
172 Sec
.withIndex repoSecure
$ \callbacks
->
173 let warnAndFail s
= warn verbosity
("Fetched tarball " ++ file
++ " does not match server, will redownload: " ++ s
) >> return False
174 in -- the do block in parens is due to dealing with the checked exceptions mechanism.
176 fileInfo
<- Sec
.indexLookupFileInfo callbacks pkgid
177 sz
<- Sec
.FileLength
. fromInteger <$> getFileSize file
178 if sz
/= Sec
.fileInfoLength
(Sec
.trusted fileInfo
)
179 then warnAndFail
"file length mismatch"
181 res
<- Sec
.compareTrustedFileInfo
(Sec
.trusted fileInfo
) <$> Sec
.computeFileInfo
(Sec
.Path file
:: Sec
.Path Sec
.Absolute
)
184 else warnAndFail
"file hash mismatch"
186 `Sec
.catchChecked`
(\(e
:: Sec
.InvalidPackageException
) -> warnAndFail
(show e
))
187 `Sec
.catchChecked`
(\(e
:: Sec
.VerificationError
) -> warnAndFail
(show e
))
190 -- | Fetch a package if we don't have it already.
196 fetchPackage verbosity repoCtxt loc
= case loc
of
197 LocalUnpackedPackage dir
->
198 return (LocalUnpackedPackage dir
)
199 LocalTarballPackage file
->
200 return (LocalTarballPackage file
)
201 RemoteTarballPackage uri
(Just file
) ->
202 return (RemoteTarballPackage uri file
)
203 RepoTarballPackage repo pkgid
(Just file
) ->
204 return (RepoTarballPackage repo pkgid file
)
205 RemoteSourceRepoPackage repo
(Just dir
) ->
206 return (RemoteSourceRepoPackage repo dir
)
207 RemoteTarballPackage uri Nothing
-> do
208 path
<- downloadTarballPackage uri
209 return (RemoteTarballPackage uri path
)
210 RepoTarballPackage repo pkgid Nothing
-> do
211 local
<- fetchRepoTarball verbosity repoCtxt repo pkgid
212 return (RepoTarballPackage repo pkgid local
)
213 RemoteSourceRepoPackage _repo Nothing
->
214 dieWithException verbosity FetchPackageErr
216 downloadTarballPackage
:: URI
-> IO FilePath
217 downloadTarballPackage uri
= do
218 transport
<- repoContextGetTransport repoCtxt
219 transportCheckHttps verbosity transport uri
220 notice verbosity
("Downloading " ++ show uri
)
221 tmpdir
<- getTemporaryDirectory
222 (path
, hnd
) <- openTempFile tmpdir
"cabal-.tar.gz"
224 _
<- downloadURI transport verbosity uri path
227 -- | Fetch a repo package if we don't have it already.
228 fetchRepoTarball
:: Verbosity
-> RepoContext
-> Repo
-> PackageId
-> IO FilePath
229 fetchRepoTarball verbosity
' repoCtxt repo pkgid
= do
230 fetched
<- doesFileExist (packageFile repo pkgid
)
233 info verbosity
$ prettyShow pkgid
++ " has already been downloaded."
234 return (packageFile repo pkgid
)
236 progressMessage verbosity ProgressDownloading
(prettyShow pkgid
)
237 res
<- downloadRepoPackage
238 progressMessage verbosity ProgressDownloaded
(prettyShow pkgid
)
241 -- whether we download or not is non-deterministic
242 verbosity
= verboseUnmarkOutput verbosity
'
244 downloadRepoPackage
:: IO FilePath
245 downloadRepoPackage
= case repo
of
246 RepoLocalNoIndex
{} -> return (packageFile repo pkgid
)
248 transport
<- repoContextGetTransport repoCtxt
249 remoteRepoCheckHttps verbosity transport repoRemote
250 let uri
= packageURI repoRemote pkgid
251 dir
= packageDir repo pkgid
252 path
= packageFile repo pkgid
253 createDirectoryIfMissing
True dir
254 _
<- downloadURI transport verbosity uri path
256 RepoSecure
{} -> repoContextWithSecureRepo repoCtxt repo
$ \rep
-> do
257 let dir
= packageDir repo pkgid
258 path
= packageFile repo pkgid
259 createDirectoryIfMissing
True dir
260 Sec
.uncheckClientErrors
$ do
261 info verbosity
("Writing " ++ path
)
262 Sec
.downloadPackage
' rep pkgid path
265 -- | Downloads an index file to [config-dir/packages/serv-id] without
266 -- hackage-security. You probably don't want to call this directly;
267 -- use 'updateRepo' instead.
268 downloadIndex
:: HttpTransport
-> Verbosity
-> RemoteRepo
-> FilePath -> IO DownloadResult
269 downloadIndex transport verbosity remoteRepo cacheDir
= do
270 remoteRepoCheckHttps verbosity transport remoteRepo
272 (remoteRepoURI remoteRepo
)
274 uriPath
(remoteRepoURI remoteRepo
)
275 `
FilePath.Posix
.combine`
"00-index.tar.gz"
277 path
= cacheDir
</> "00-index" <.> "tar.gz"
278 createDirectoryIfMissing
True cacheDir
279 downloadURI transport verbosity uri path
281 -- ------------------------------------------------------------
283 -- * Async fetch wrapper utilities
285 -- ------------------------------------------------------------
290 (MVar
(Either SomeException ResolvedPkgLoc
))
292 -- | Fork off an async action to download the given packages (by location).
294 -- The downloads are initiated in order, so you can arrange for packages that
295 -- will likely be needed sooner to be earlier in the list.
297 -- The body action is passed a map from those packages (identified by their
298 -- location) to a completion var for that package. So the body action should
299 -- lookup the location and use 'waitAsyncFetchPackage' to get the result.
301 -- Synchronous exceptions raised by the download actions are delivered
302 -- via 'waitAsyncFetchPackage'.
306 -> [UnresolvedPkgLoc
]
307 -> (AsyncFetchMap
-> IO a
)
309 asyncFetchPackages verbosity repoCtxt pkglocs body
= do
310 -- TODO: [nice to have] use parallel downloads?
320 let fetchPackages
:: IO ()
322 for_ asyncDownloadVars
$ \(pkgloc
, var
) -> do
323 -- Suppress marking here, because 'withAsync' means
324 -- that we get nondeterministic interleaving.
325 -- It is essential that we don't catch async exceptions here,
326 -- specifically 'AsyncCancelled' thrown at us from 'concurrently'.
329 fetchPackage
(verboseUnmarkOutput verbosity
) repoCtxt pkgloc
335 (body
$ Map
.fromList asyncDownloadVars
)
338 -- | Expect to find a download in progress in the given 'AsyncFetchMap'
339 -- and wait on it to finish.
341 -- If the download failed with an exception then this will be thrown.
343 -- Note: This function is supposed to be idempotent, as our install plans
344 -- can now use the same tarball for many builds, e.g. different
345 -- components and/or qualified goals, and these all go through the
346 -- download phase so we end up using 'waitAsyncFetchPackage' twice on
347 -- the same package. C.f. #4461.
348 waitAsyncFetchPackage
353 waitAsyncFetchPackage verbosity downloadMap srcloc
=
354 case Map
.lookup srcloc downloadMap
of
356 debug verbosity
$ "Waiting for download of " ++ show srcloc
357 either throwIO
return =<< readMVar hnd
358 Nothing
-> fail "waitAsyncFetchPackage: package not being downloaded"
360 -- ------------------------------------------------------------
364 -- ------------------------------------------------------------
366 -- | Generate the full path to the locally cached copy of
367 -- the tarball for a given @PackageIdentifier@.
368 packageFile
:: Repo
-> PackageId
-> FilePath
369 packageFile repo pkgid
=
370 packageDir repo pkgid
374 -- | Generate the full path to the directory where the local cached copy of
375 -- the tarball for a given @PackageIdentifier@ is stored.
376 packageDir
:: Repo
-> PackageId
-> FilePath
377 packageDir
(RepoLocalNoIndex
(LocalRepo _ dir _
) _
) _pkgid
= dir
378 packageDir repo pkgid
=
380 </> prettyShow
(packageName pkgid
)
381 </> prettyShow
(packageVersion pkgid
)
383 -- | Generate the URI of the tarball for a given package.
384 packageURI
:: RemoteRepo
-> PackageId
-> URI
385 packageURI repo pkgid
386 | isOldHackageURI
(remoteRepoURI repo
) =
389 FilePath.Posix
.joinPath
390 [ uriPath
(remoteRepoURI repo
)
391 , prettyShow
(packageName pkgid
)
392 , prettyShow
(packageVersion pkgid
)
393 , prettyShow pkgid
<.> "tar.gz"
396 packageURI repo pkgid
=
399 FilePath.Posix
.joinPath
400 [ uriPath
(remoteRepoURI repo
)
402 , prettyShow pkgid
<.> "tar.gz"