2 {-# LANGUAGE ScopedTypeVariables #-}
4 module Distribution
.Client
.Utils
22 , makeRelativeCanonical
23 , filePathToByteString
24 , byteStringToFilePath
26 , canonicalizePathNoThrow
28 , existsAndIsMoreRecentThan
29 , tryFindAddSourcePackageDesc
45 import Distribution
.Client
.Compat
.Prelude
48 import qualified Control
.Exception
as Exception
51 import qualified Control
.Exception
.Safe
as Safe
62 import qualified Data
.ByteString
.Lazy
as BS
67 import Distribution
.Compat
.Environment
68 import Distribution
.Compat
.Time
(getModTime
)
69 import Distribution
.Simple
.Setup
(Flag
(..))
70 import Distribution
.Simple
.Utils
(dieWithException
, findPackageDesc
, noticeNoWrap
)
71 import Distribution
.Version
72 import System
.Directory
77 , getDirectoryContents
81 import System
.FilePath
89 import System
.IO.Unsafe
(unsafePerformIO
)
91 import Data
.Time
(utcToLocalTime
)
92 import Data
.Time
.Calendar
(toGregorian
)
93 import Data
.Time
.Clock
.POSIX
(getCurrentTime
)
94 import Data
.Time
.LocalTime
(getCurrentTimeZone
, localDay
)
95 import GHC
.Conc
.Sync
(getNumProcessors
)
96 import GHC
.IO.Encoding
97 ( TextEncoding
(TextEncoding
)
100 import GHC
.IO.Encoding
.Failure
101 ( CodingFailureMode
(TransliterateCodingFailure
)
104 #if defined
(mingw32_HOST_OS
) || MIN_VERSION_directory
(1,2,3)
105 import qualified System
.Directory
as Dir
106 import qualified System
.IO.Error
as IOError
108 import qualified Data
.Set
as Set
109 import Distribution
.Client
.Errors
111 -- | Generic merging utility. For sorted input lists this is a full outer join.
112 mergeBy
:: forall a b
. (a
-> b
-> Ordering) -> [a
] -> [b
] -> [MergeResult a b
]
115 merge
:: [a
] -> [b
] -> [MergeResult a b
]
116 merge
[] ys
= [OnlyInRight y | y
<- ys
]
117 merge xs
[] = [OnlyInLeft x | x
<- xs
]
118 merge
(x
: xs
) (y
: ys
) =
120 GT
-> OnlyInRight y
: merge
(x
: xs
) ys
121 EQ
-> InBoth x y
: merge xs ys
122 LT
-> OnlyInLeft x
: merge xs
(y
: ys
)
124 data MergeResult a b
= OnlyInLeft a | InBoth a b | OnlyInRight b
126 duplicates
:: Ord a
=> [a
] -> [[a
]]
127 duplicates
= duplicatesBy
compare
129 duplicatesBy
:: forall a
. (a
-> a
-> Ordering) -> [a
] -> [[a
]]
130 duplicatesBy cmp
= filter moreThanOne
. groupBy eq
. sortBy cmp
133 eq a b
= case cmp a b
of
136 moreThanOne
(_
: _
: _
) = True
137 moreThanOne _
= False
139 -- | Like 'removeFile', but does not throw an exception when the file does not
141 removeExistingFile
:: FilePath -> IO ()
142 removeExistingFile path
= do
143 exists
<- doesFileExist path
147 -- | A variant of 'withTempFile' that only gives us the file name, and while
148 -- it will clean up the file afterwards, it's lenient if the file is
153 -> (FilePath -> IO a
)
155 withTempFileName tmpDir template action
=
157 (openTempFile tmpDir template
)
158 (\(name
, _
) -> removeExistingFile name
)
159 (\(name
, h
) -> hClose h
>> action name
)
161 -- | Executes the action in the specified directory.
163 -- Warning: This operation is NOT thread-safe, because current
164 -- working directory is a process-global concept.
165 inDir
:: Maybe FilePath -> IO a
-> IO a
167 inDir
(Just d
) m
= do
168 old
<- getCurrentDirectory
169 setCurrentDirectory d
170 m `Exception
.finally`
setCurrentDirectory old
172 -- | Executes the action with an environment variable set to some
175 -- Warning: This operation is NOT thread-safe, because current
176 -- environment is a process-global concept.
177 withEnv
:: String -> String -> IO a
-> IO a
179 mb_old
<- lookupEnv k
181 m `Exception
.finally` setOrUnsetEnv k mb_old
183 -- | Executes the action with a list of environment variables and
184 -- corresponding overrides, where
186 -- * @'Just' v@ means \"set the environment variable's value to @v@\".
187 -- * 'Nothing' means \"unset the environment variable\".
189 -- Warning: This operation is NOT thread-safe, because current
190 -- environment is a process-global concept.
191 withEnvOverrides
:: [(String, Maybe FilePath)] -> IO a
-> IO a
192 withEnvOverrides overrides m
= do
193 mb_olds
<- traverse lookupEnv envVars
194 traverse_
(uncurry setOrUnsetEnv
) overrides
195 m `Exception
.finally`
zipWithM_ setOrUnsetEnv envVars mb_olds
198 envVars
= map fst overrides
200 setOrUnsetEnv
:: String -> Maybe String -> IO ()
201 setOrUnsetEnv var Nothing
= unsetEnv var
202 setOrUnsetEnv var
(Just val
) = setEnv var val
204 -- | Executes the action, increasing the PATH environment
207 -- Warning: This operation is NOT thread-safe, because the
208 -- environment variables are a process-global concept.
209 withExtraPathEnv
:: [FilePath] -> IO a
-> IO a
210 withExtraPathEnv paths m
= do
211 oldPathSplit
<- getSearchPath
212 let newPath
:: String
213 newPath
= mungePath
$ intercalate
[searchPathSeparator
] (paths
++ oldPathSplit
)
215 oldPath
= mungePath
$ intercalate
[searchPathSeparator
] oldPathSplit
216 -- TODO: This is a horrible hack to work around the fact that
217 -- setEnv can't take empty values as an argument
219 | p
== "" = "/dev/null"
221 setEnv
"PATH" newPath
222 m `Exception
.finally` setEnv
"PATH" oldPath
224 -- | Log directory change in 'make' compatible syntax
225 logDirChange
:: (String -> IO ()) -> Maybe FilePath -> IO a
-> IO a
226 logDirChange _ Nothing m
= m
227 logDirChange l
(Just d
) m
= do
228 l
$ "cabal: Entering directory '" ++ d
++ "'\n"
230 `Exception
.finally` l
("cabal: Leaving directory '" ++ d
++ "'\n")
232 -- The number of processors is not going to change during the duration of the
233 -- program, so unsafePerformIO is safe here.
234 numberOfProcessors
:: Int
235 numberOfProcessors
= unsafePerformIO getNumProcessors
237 -- | Determine the number of jobs to use given the value of the '-j' flag.
238 determineNumJobs
:: Flag
(Maybe Int) -> Int
239 determineNumJobs numJobsFlag
=
242 Flag Nothing
-> numberOfProcessors
245 -- | Given a relative path, make it absolute relative to the current
246 -- directory. Absolute paths are returned unmodified.
247 makeAbsoluteToCwd
:: FilePath -> IO FilePath
248 makeAbsoluteToCwd path
249 | isAbsolute path
= return path
251 cwd
<- getCurrentDirectory
252 return $! cwd
</> path
254 -- | Given a path (relative or absolute), make it relative to the current
255 -- directory, including using @../..@ if necessary.
256 makeRelativeToCwd
:: FilePath -> IO FilePath
257 makeRelativeToCwd path
=
258 makeRelativeCanonical
<$> canonicalizePath path
<*> getCurrentDirectory
260 -- | Given a path (relative or absolute), make it relative to the given
261 -- directory, including using @../..@ if necessary.
262 makeRelativeToDir
:: FilePath -> FilePath -> IO FilePath
263 makeRelativeToDir path dir
=
264 makeRelativeCanonical
<$> canonicalizePath path
<*> canonicalizePath dir
266 -- | Given a canonical absolute path and canonical absolute dir, make the path
267 -- relative to the directory, including using @../..@ if necessary. Returns
268 -- the original absolute path if it is not on the same drive as the given dir.
269 makeRelativeCanonical
:: FilePath -> FilePath -> FilePath
270 makeRelativeCanonical path dir
271 | takeDrive path
/= takeDrive dir
= path
272 |
otherwise = go
(splitPath path
) (splitPath dir
)
274 go
(p
: ps
) (d
: ds
) | p
' == d
' = go ps ds
276 (p
', d
') = (dropTrailingPathSeparator p
, dropTrailingPathSeparator d
)
278 go ps ds
= joinPath
(replicate (length ds
) ".." ++ ps
)
280 -- | Convert a 'FilePath' to a lazy 'ByteString'. Each 'Char' is
281 -- encoded as a little-endian 'Word32'.
282 filePathToByteString
:: FilePath -> BS
.ByteString
283 filePathToByteString p
=
284 BS
.pack
$ foldr conv
[] codepts
287 codepts
= map (fromIntegral . ord) p
289 conv
:: Word32
-> [Word8
] -> [Word8
]
290 conv w32 rest
= b0
: b1
: b2
: b3
: rest
292 b0
= fromIntegral $ w32
293 b1
= fromIntegral $ w32 `shiftR`
8
294 b2
= fromIntegral $ w32 `shiftR`
16
295 b3
= fromIntegral $ w32 `shiftR`
24
297 -- | Reverse operation to 'filePathToByteString'.
298 byteStringToFilePath
:: BS
.ByteString
-> FilePath
299 byteStringToFilePath bs
300 | bslen `
mod`
4 /= 0 = unexpected
303 unexpected
= "Distribution.Client.Utils.byteStringToFilePath: unexpected"
308 |
otherwise = (chr . fromIntegral $ w32
) : go
(i
+ 4)
311 w32
= b0
.|
. (b1 `shiftL`
8) .|
. (b2 `shiftL`
16) .|
. (b3 `shiftL`
24)
312 b0
= fromIntegral $ BS
.index bs i
313 b1
= fromIntegral $ BS
.index bs
(i
+ 1)
314 b2
= fromIntegral $ BS
.index bs
(i
+ 2)
315 b3
= fromIntegral $ BS
.index bs
(i
+ 3)
317 -- | Workaround for the inconsistent behaviour of 'canonicalizePath'. Always
318 -- throws an error if the path refers to a non-existent file.
319 {- FOURMOLU_DISABLE -}
320 tryCanonicalizePath
:: FilePath -> IO FilePath
321 tryCanonicalizePath path
= do
322 ret
<- canonicalizePath path
323 #if defined
(mingw32_HOST_OS
) || MIN_VERSION_directory
(1,2,3)
324 exists
<- liftM2 (||
) (doesFileExist ret
) (Dir
.doesDirectoryExist ret
)
326 IOError.ioError $ IOError.mkIOError
IOError.doesNotExistErrorType
"canonicalizePath"
330 {- FOURMOLU_ENABLE -}
332 -- | A non-throwing wrapper for 'canonicalizePath'. If 'canonicalizePath' throws
333 -- an exception, returns the path argument unmodified.
334 canonicalizePathNoThrow
:: FilePath -> IO FilePath
335 canonicalizePathNoThrow path
= do
336 canonicalizePath path `catchIO`
(\_
-> return path
)
341 -- | Like Distribution.Simple.Utils.moreRecentFile, but uses getModTime instead
342 -- of getModificationTime for higher precision. We can't merge the two because
343 -- Distribution.Client.Time uses MIN_VERSION macros.
344 moreRecentFile
:: FilePath -> FilePath -> IO Bool
345 moreRecentFile a b
= do
346 exists
<- doesFileExist b
354 -- | Like 'moreRecentFile', but also checks that the first file exists.
355 existsAndIsMoreRecentThan
:: FilePath -> FilePath -> IO Bool
356 existsAndIsMoreRecentThan a b
= do
357 exists
<- doesFileExist a
360 else a `moreRecentFile` b
362 -- | Sets the handler for encoding errors to one that transliterates invalid
363 -- characters into one present in the encoding (i.e., \'?\').
364 -- This is opposed to the default behavior, which is to throw an exception on
365 -- error. This function will ignore file handles that have a Unicode encoding
366 -- set. It's a no-op for versions of `base` less than 4.4.
367 relaxEncodingErrors
:: Handle -> IO ()
368 relaxEncodingErrors handle
= do
369 maybeEncoding
<- hGetEncoding handle
370 case maybeEncoding
of
371 Just
(TextEncoding name decoder encoder
)
372 |
not ("UTF" `
isPrefixOf` name
) ->
373 let relax x
= x
{recover
= recoverEncode TransliterateCodingFailure
}
374 in hSetEncoding handle
(TextEncoding name decoder
(fmap relax encoder
))
378 -- | Like 'tryFindPackageDesc', but with error specific to add-source deps.
379 tryFindAddSourcePackageDesc
:: Verbosity
-> FilePath -> String -> IO FilePath
380 tryFindAddSourcePackageDesc verbosity depPath err
=
381 tryFindPackageDesc verbosity depPath
$
384 ++ "Failed to read cabal file of add-source dependency: "
387 -- | Try to find a @.cabal@ file, in directory @depPath@. Fails if one cannot be
388 -- found, with @err@ prefixing the error message. This function simply allows
389 -- us to give a more descriptive error than that provided by @findPackageDesc@.
390 tryFindPackageDesc
:: Verbosity
-> FilePath -> String -> IO FilePath
391 tryFindPackageDesc verbosity depPath err
= do
392 errOrCabalFile
<- findPackageDesc depPath
393 case errOrCabalFile
of
394 Right file
-> return file
395 Left _
-> dieWithException verbosity
$ TryFindPackageDescErr err
397 -- | Phase of building a dependency. Represents current status of package
398 -- dependency processing. See #4040 for details.
400 = ProgressDownloading
408 progressMessage
:: Verbosity
-> ProgressPhase
-> String -> IO ()
409 progressMessage verbosity
phase subject
= do
410 noticeNoWrap verbosity
$ phaseStr
++ subject
++ "\n"
412 phaseStr
= case phase of
413 ProgressDownloading
-> "Downloading "
414 ProgressDownloaded
-> "Downloaded "
415 ProgressStarting
-> "Starting "
416 ProgressBuilding
-> "Building "
417 ProgressHaddock
-> "Haddock "
418 ProgressInstalling
-> "Installing "
419 ProgressCompleted
-> "Completed "
421 -- | Given a version, return an API-compatible (according to PVP) version range.
423 -- If the boolean argument denotes whether to use a desugared
424 -- representation (if 'True') or the new-style @^>=@-form (if
427 -- Example: @pvpize True (mkVersion [0,4,1])@ produces the version range @>= 0.4 && < 0.5@ (which is the
429 pvpize
:: Bool -> Version
-> VersionRange
430 pvpize
False v
= majorBoundVersion v
433 `intersectVersionRanges` earlierVersion
(incVersion
1 v
')
435 v
' = alterVersion
(take 2) v
437 -- | Increment the nth version component (counting from 0).
438 incVersion
:: Int -> Version
-> Version
439 incVersion n
= alterVersion
(incVersion
' n
)
441 incVersion
' 0 [] = [1]
442 incVersion
' 0 (v
: _
) = [v
+ 1]
443 incVersion
' m
[] = replicate m
0 ++ [1]
444 incVersion
' m
(v
: vs
) = v
: incVersion
' (m
- 1) vs
446 -- | Returns the current calendar year.
447 getCurrentYear
:: IO Integer
450 z
<- getCurrentTimeZone
451 let l
= utcToLocalTime z u
452 (y
, _
, _
) = toGregorian
$ localDay l
455 -- | From System.Directory.Extra
456 -- https://hackage.haskell.org/package/extra-1.7.9
457 listFilesInside
:: (FilePath -> IO Bool) -> FilePath -> IO [FilePath]
458 listFilesInside test dir
= ifNotM
(test
$ dropTrailingPathSeparator dir
) (pure
[]) $ do
459 (dirs
, files
) <- partitionM
doesDirectoryExist =<< listContents dir
460 rest
<- concatMapM
(listFilesInside test
) dirs
463 -- | From System.Directory.Extra
464 -- https://hackage.haskell.org/package/extra-1.7.9
465 listFilesRecursive
:: FilePath -> IO [FilePath]
466 listFilesRecursive
= listFilesInside
(const $ pure
True)
468 -- | From System.Directory.Extra
469 -- https://hackage.haskell.org/package/extra-1.7.9
470 listContents
:: FilePath -> IO [FilePath]
471 listContents dir
= do
472 xs
<- getDirectoryContents dir
473 pure
$ sort [dir
</> x | x
<- xs
, not $ all (== '.') x
]
475 -- | From Control.Monad.Extra
476 -- https://hackage.haskell.org/package/extra-1.7.9
477 ifM
:: Monad m
=> m
Bool -> m a
-> m a
-> m a
478 ifM b t f
= do b
' <- b
; if b
' then t
else f
480 -- | 'ifM' with swapped branches:
481 -- @ifNotM b t f = ifM (not <$> b) t f@
482 ifNotM
:: Monad m
=> m
Bool -> m a
-> m a
-> m a
485 -- | From Control.Monad.Extra
486 -- https://hackage.haskell.org/package/extra-1.7.9
487 concatMapM
:: Monad m
=> (a
-> m
[b
]) -> [a
] -> m
[b
]
488 {-# INLINE concatMapM #-}
489 concatMapM op
= foldr f
(pure
[])
491 f x xs
= do x
' <- op x
; if null x
' then xs
else do { xs
' <- xs
; pure
$ x
' ++ xs
' }
493 -- | From Control.Monad.Extra
494 -- https://hackage.haskell.org/package/extra-1.7.9
495 partitionM
:: Monad m
=> (a
-> m
Bool) -> [a
] -> m
([a
], [a
])
496 partitionM _
[] = pure
([], [])
497 partitionM f
(x
: xs
) = do
499 (as, bs
) <- partitionM f xs
500 pure
([x | res
] ++ as, [x |
not res
] ++ bs
)
502 safeRead
:: Read a
=> String -> Maybe a
504 |
[(x
, "")] <- reads s
= Just x
505 |
otherwise = Nothing
507 -- | @hasElem xs x = elem x xs@ except that @xs@ is turned into a 'Set' first.
508 -- Use underapplied to speed up subsequent lookups, e.g. @filter (hasElem xs) ys@.
509 -- Only amortized when used several times!
511 -- Time complexity \(O((n+m) \log(n))\) for \(m\) lookups in a list of length \(n\).
512 -- (Compare this to 'elem''s \(O(nm)\).)
514 -- This is [Agda.Utils.List.hasElem](https://hackage.haskell.org/package/Agda-2.6.2.2/docs/Agda-Utils-List.html#v:hasElem).
515 hasElem
:: Ord a
=> [a
] -> a
-> Bool
516 hasElem xs
= (`Set
.member` Set
.fromList xs
)
518 -- True if x occurs before y
519 occursOnlyOrBefore
:: Eq a
=> [a
] -> a
-> a
-> Bool
520 occursOnlyOrBefore xs x y
= case (elemIndex x xs
, elemIndex y xs
) of
521 (Just i
, Just j
) -> i
< j
525 giveRTSWarning
:: String -> String
526 giveRTSWarning
"run" =
527 "Your RTS options are applied to cabal, not the "
528 ++ "executable. Use '--' to separate cabal options from your "
529 ++ "executable options. For example, use 'cabal run -- +RTS -N "
530 ++ "to pass the '-N' RTS option to your executable."
531 giveRTSWarning
"test" =
532 "Some RTS options were found standalone, "
533 ++ "which affect cabal and not the binary. "
534 ++ "Please note that +RTS inside the --test-options argument "
535 ++ "suffices if your goal is to affect the tested binary. "
536 ++ "For example, use \"cabal test --test-options='+RTS -N'\" "
537 ++ "to pass the '-N' RTS option to your binary."
538 giveRTSWarning
"bench" =
539 "Some RTS options were found standalone, "
540 ++ "which affect cabal and not the binary. Please note "
541 ++ "that +RTS inside the --benchmark-options argument "
542 ++ "suffices if your goal is to affect the benchmarked "
543 ++ "binary. For example, use \"cabal test --benchmark-options="
544 ++ "'+RTS -N'\" to pass the '-N' RTS option to your binary."
546 "Your RTS options are applied to cabal, not the "