2 {-# LANGUAGE ScopedTypeVariables #-}
4 module Distribution
.Client
.Utils
22 , makeRelativeCanonical
23 , filePathToByteString
24 , byteStringToFilePath
26 , canonicalizePathNoThrow
28 , existsAndIsMoreRecentThan
29 , tryFindAddSourcePackageDesc
31 , findOpenProgramLocation
46 import Distribution
.Client
.Compat
.Prelude
49 import qualified Control
.Exception
as Exception
52 import qualified Control
.Exception
.Safe
as Safe
63 import qualified Data
.ByteString
.Lazy
as BS
68 import Distribution
.Compat
.Environment
69 import Distribution
.Compat
.Time
(getModTime
)
70 import Distribution
.Simple
.Setup
(Flag
(..))
71 import Distribution
.Simple
.Utils
(dieWithException
, findPackageDesc
, noticeNoWrap
)
72 import Distribution
.System
(OS
(..), Platform
(..))
73 import Distribution
.Version
74 import System
.Directory
80 , getDirectoryContents
84 import System
.FilePath
92 import System
.IO.Unsafe
(unsafePerformIO
)
94 import Data
.Time
(utcToLocalTime
)
95 import Data
.Time
.Calendar
(toGregorian
)
96 import Data
.Time
.Clock
.POSIX
(getCurrentTime
)
97 import Data
.Time
.LocalTime
(getCurrentTimeZone
, localDay
)
98 import GHC
.Conc
.Sync
(getNumProcessors
)
99 import GHC
.IO.Encoding
100 ( TextEncoding
(TextEncoding
)
103 import GHC
.IO.Encoding
.Failure
104 ( CodingFailureMode
(TransliterateCodingFailure
)
107 #if defined
(mingw32_HOST_OS
) || MIN_VERSION_directory
(1,2,3)
108 import qualified System
.Directory
as Dir
109 import qualified System
.IO.Error
as IOError
111 import qualified Data
.Set
as Set
112 import Distribution
.Client
.Errors
114 -- | Generic merging utility. For sorted input lists this is a full outer join.
115 mergeBy
:: forall a b
. (a
-> b
-> Ordering) -> [a
] -> [b
] -> [MergeResult a b
]
118 merge
:: [a
] -> [b
] -> [MergeResult a b
]
119 merge
[] ys
= [OnlyInRight y | y
<- ys
]
120 merge xs
[] = [OnlyInLeft x | x
<- xs
]
121 merge
(x
: xs
) (y
: ys
) =
123 GT
-> OnlyInRight y
: merge
(x
: xs
) ys
124 EQ
-> InBoth x y
: merge xs ys
125 LT
-> OnlyInLeft x
: merge xs
(y
: ys
)
127 data MergeResult a b
= OnlyInLeft a | InBoth a b | OnlyInRight b
129 duplicates
:: Ord a
=> [a
] -> [[a
]]
130 duplicates
= duplicatesBy
compare
132 duplicatesBy
:: forall a
. (a
-> a
-> Ordering) -> [a
] -> [[a
]]
133 duplicatesBy cmp
= filter moreThanOne
. groupBy eq
. sortBy cmp
136 eq a b
= case cmp a b
of
139 moreThanOne
(_
: _
: _
) = True
140 moreThanOne _
= False
142 -- | Like 'removeFile', but does not throw an exception when the file does not
144 removeExistingFile
:: FilePath -> IO ()
145 removeExistingFile path
= do
146 exists
<- doesFileExist path
150 -- | A variant of 'withTempFile' that only gives us the file name, and while
151 -- it will clean up the file afterwards, it's lenient if the file is
156 -> (FilePath -> IO a
)
158 withTempFileName tmpDir template action
=
160 (openTempFile tmpDir template
)
161 (\(name
, _
) -> removeExistingFile name
)
162 (\(name
, h
) -> hClose h
>> action name
)
164 -- | Executes the action in the specified directory.
166 -- Warning: This operation is NOT thread-safe, because current
167 -- working directory is a process-global concept.
168 inDir
:: Maybe FilePath -> IO a
-> IO a
170 inDir
(Just d
) m
= do
171 old
<- getCurrentDirectory
172 setCurrentDirectory d
173 m `Exception
.finally`
setCurrentDirectory old
175 -- | Executes the action with an environment variable set to some
178 -- Warning: This operation is NOT thread-safe, because current
179 -- environment is a process-global concept.
180 withEnv
:: String -> String -> IO a
-> IO a
182 mb_old
<- lookupEnv k
184 m `Exception
.finally` setOrUnsetEnv k mb_old
186 -- | Executes the action with a list of environment variables and
187 -- corresponding overrides, where
189 -- * @'Just' v@ means \"set the environment variable's value to @v@\".
190 -- * 'Nothing' means \"unset the environment variable\".
192 -- Warning: This operation is NOT thread-safe, because current
193 -- environment is a process-global concept.
194 withEnvOverrides
:: [(String, Maybe FilePath)] -> IO a
-> IO a
195 withEnvOverrides overrides m
= do
196 mb_olds
<- traverse lookupEnv envVars
197 traverse_
(uncurry setOrUnsetEnv
) overrides
198 m `Exception
.finally`
zipWithM_ setOrUnsetEnv envVars mb_olds
201 envVars
= map fst overrides
203 setOrUnsetEnv
:: String -> Maybe String -> IO ()
204 setOrUnsetEnv var Nothing
= unsetEnv var
205 setOrUnsetEnv var
(Just val
) = setEnv var val
207 -- | Executes the action, increasing the PATH environment
210 -- Warning: This operation is NOT thread-safe, because the
211 -- environment variables are a process-global concept.
212 withExtraPathEnv
:: [FilePath] -> IO a
-> IO a
213 withExtraPathEnv paths m
= do
214 oldPathSplit
<- getSearchPath
215 let newPath
:: String
216 newPath
= mungePath
$ intercalate
[searchPathSeparator
] (paths
++ oldPathSplit
)
218 oldPath
= mungePath
$ intercalate
[searchPathSeparator
] oldPathSplit
219 -- TODO: This is a horrible hack to work around the fact that
220 -- setEnv can't take empty values as an argument
222 | p
== "" = "/dev/null"
224 setEnv
"PATH" newPath
225 m `Exception
.finally` setEnv
"PATH" oldPath
227 -- | Log directory change in 'make' compatible syntax
228 logDirChange
:: (String -> IO ()) -> Maybe FilePath -> IO a
-> IO a
229 logDirChange _ Nothing m
= m
230 logDirChange l
(Just d
) m
= do
231 l
$ "cabal: Entering directory '" ++ d
++ "'\n"
233 `Exception
.finally` l
("cabal: Leaving directory '" ++ d
++ "'\n")
235 -- The number of processors is not going to change during the duration of the
236 -- program, so unsafePerformIO is safe here.
237 numberOfProcessors
:: Int
238 numberOfProcessors
= unsafePerformIO getNumProcessors
240 -- | Determine the number of jobs to use given the value of the '-j' flag.
241 determineNumJobs
:: Flag
(Maybe Int) -> Int
242 determineNumJobs numJobsFlag
=
245 Flag Nothing
-> numberOfProcessors
248 -- | Given a relative path, make it absolute relative to the current
249 -- directory. Absolute paths are returned unmodified.
250 makeAbsoluteToCwd
:: FilePath -> IO FilePath
251 makeAbsoluteToCwd path
252 | isAbsolute path
= return path
254 cwd
<- getCurrentDirectory
255 return $! cwd
</> path
257 -- | Given a path (relative or absolute), make it relative to the current
258 -- directory, including using @../..@ if necessary.
259 makeRelativeToCwd
:: FilePath -> IO FilePath
260 makeRelativeToCwd path
=
261 makeRelativeCanonical
<$> canonicalizePath path
<*> getCurrentDirectory
263 -- | Given a path (relative or absolute), make it relative to the given
264 -- directory, including using @../..@ if necessary.
265 makeRelativeToDir
:: FilePath -> FilePath -> IO FilePath
266 makeRelativeToDir path dir
=
267 makeRelativeCanonical
<$> canonicalizePath path
<*> canonicalizePath dir
269 -- | Given a canonical absolute path and canonical absolute dir, make the path
270 -- relative to the directory, including using @../..@ if necessary. Returns
271 -- the original absolute path if it is not on the same drive as the given dir.
272 makeRelativeCanonical
:: FilePath -> FilePath -> FilePath
273 makeRelativeCanonical path dir
274 | takeDrive path
/= takeDrive dir
= path
275 |
otherwise = go
(splitPath path
) (splitPath dir
)
277 go
(p
: ps
) (d
: ds
) | p
' == d
' = go ps ds
279 (p
', d
') = (dropTrailingPathSeparator p
, dropTrailingPathSeparator d
)
281 go ps ds
= joinPath
(replicate (length ds
) ".." ++ ps
)
283 -- | Convert a 'FilePath' to a lazy 'ByteString'. Each 'Char' is
284 -- encoded as a little-endian 'Word32'.
285 filePathToByteString
:: FilePath -> BS
.ByteString
286 filePathToByteString p
=
287 BS
.pack
$ foldr conv
[] codepts
290 codepts
= map (fromIntegral . ord) p
292 conv
:: Word32
-> [Word8
] -> [Word8
]
293 conv w32 rest
= b0
: b1
: b2
: b3
: rest
295 b0
= fromIntegral $ w32
296 b1
= fromIntegral $ w32 `shiftR`
8
297 b2
= fromIntegral $ w32 `shiftR`
16
298 b3
= fromIntegral $ w32 `shiftR`
24
300 -- | Reverse operation to 'filePathToByteString'.
301 byteStringToFilePath
:: BS
.ByteString
-> FilePath
302 byteStringToFilePath bs
303 | bslen `
mod`
4 /= 0 = unexpected
306 unexpected
= "Distribution.Client.Utils.byteStringToFilePath: unexpected"
311 |
otherwise = (chr . fromIntegral $ w32
) : go
(i
+ 4)
314 w32
= b0
.|
. (b1 `shiftL`
8) .|
. (b2 `shiftL`
16) .|
. (b3 `shiftL`
24)
315 b0
= fromIntegral $ BS
.index bs i
316 b1
= fromIntegral $ BS
.index bs
(i
+ 1)
317 b2
= fromIntegral $ BS
.index bs
(i
+ 2)
318 b3
= fromIntegral $ BS
.index bs
(i
+ 3)
320 -- | Workaround for the inconsistent behaviour of 'canonicalizePath'. Always
321 -- throws an error if the path refers to a non-existent file.
322 {- FOURMOLU_DISABLE -}
323 tryCanonicalizePath
:: FilePath -> IO FilePath
324 tryCanonicalizePath path
= do
325 ret
<- canonicalizePath path
326 #if defined
(mingw32_HOST_OS
) || MIN_VERSION_directory
(1,2,3)
327 exists
<- liftM2 (||
) (doesFileExist ret
) (Dir
.doesDirectoryExist ret
)
329 IOError.ioError $ IOError.mkIOError
IOError.doesNotExistErrorType
"canonicalizePath"
333 {- FOURMOLU_ENABLE -}
335 -- | A non-throwing wrapper for 'canonicalizePath'. If 'canonicalizePath' throws
336 -- an exception, returns the path argument unmodified.
337 canonicalizePathNoThrow
:: FilePath -> IO FilePath
338 canonicalizePathNoThrow path
= do
339 canonicalizePath path `catchIO`
(\_
-> return path
)
344 -- | Like Distribution.Simple.Utils.moreRecentFile, but uses getModTime instead
345 -- of getModificationTime for higher precision. We can't merge the two because
346 -- Distribution.Client.Time uses MIN_VERSION macros.
347 moreRecentFile
:: FilePath -> FilePath -> IO Bool
348 moreRecentFile a b
= do
349 exists
<- doesFileExist b
357 -- | Like 'moreRecentFile', but also checks that the first file exists.
358 existsAndIsMoreRecentThan
:: FilePath -> FilePath -> IO Bool
359 existsAndIsMoreRecentThan a b
= do
360 exists
<- doesFileExist a
363 else a `moreRecentFile` b
365 -- | Sets the handler for encoding errors to one that transliterates invalid
366 -- characters into one present in the encoding (i.e., \'?\').
367 -- This is opposed to the default behavior, which is to throw an exception on
368 -- error. This function will ignore file handles that have a Unicode encoding
369 -- set. It's a no-op for versions of `base` less than 4.4.
370 relaxEncodingErrors
:: Handle -> IO ()
371 relaxEncodingErrors handle
= do
372 maybeEncoding
<- hGetEncoding handle
373 case maybeEncoding
of
374 Just
(TextEncoding name decoder encoder
)
375 |
not ("UTF" `
isPrefixOf` name
) ->
376 let relax x
= x
{recover
= recoverEncode TransliterateCodingFailure
}
377 in hSetEncoding handle
(TextEncoding name decoder
(fmap relax encoder
))
381 -- | Like 'tryFindPackageDesc', but with error specific to add-source deps.
382 tryFindAddSourcePackageDesc
:: Verbosity
-> FilePath -> String -> IO FilePath
383 tryFindAddSourcePackageDesc verbosity depPath err
=
384 tryFindPackageDesc verbosity depPath
$
387 ++ "Failed to read cabal file of add-source dependency: "
390 -- | Try to find a @.cabal@ file, in directory @depPath@. Fails if one cannot be
391 -- found, with @err@ prefixing the error message. This function simply allows
392 -- us to give a more descriptive error than that provided by @findPackageDesc@.
393 tryFindPackageDesc
:: Verbosity
-> FilePath -> String -> IO FilePath
394 tryFindPackageDesc verbosity depPath err
= do
395 errOrCabalFile
<- findPackageDesc depPath
396 case errOrCabalFile
of
397 Right file
-> return file
398 Left _
-> dieWithException verbosity
$ TryFindPackageDescErr err
400 findOpenProgramLocation
:: Platform
-> IO (Either String FilePath)
401 findOpenProgramLocation
(Platform _ os
) =
404 exe
<- findExecutable name
406 Just s
-> pure
(Right s
)
407 Nothing
-> pure
(Left
("Couldn't find file-opener program `" <> name
<> "`"))
408 xdg
= locate
"xdg-open"
411 Windows
-> pure
(Right
"start")
418 _
-> pure
(Left
("Couldn't determine file-opener program for " <> show os
))
420 -- | Phase of building a dependency. Represents current status of package
421 -- dependency processing. See #4040 for details.
423 = ProgressDownloading
431 progressMessage
:: Verbosity
-> ProgressPhase
-> String -> IO ()
432 progressMessage verbosity
phase subject
= do
433 noticeNoWrap verbosity
$ phaseStr
++ subject
++ "\n"
435 phaseStr
= case phase of
436 ProgressDownloading
-> "Downloading "
437 ProgressDownloaded
-> "Downloaded "
438 ProgressStarting
-> "Starting "
439 ProgressBuilding
-> "Building "
440 ProgressHaddock
-> "Haddock "
441 ProgressInstalling
-> "Installing "
442 ProgressCompleted
-> "Completed "
444 -- | Given a version, return an API-compatible (according to PVP) version range.
446 -- If the boolean argument denotes whether to use a desugared
447 -- representation (if 'True') or the new-style @^>=@-form (if
450 -- Example: @pvpize True (mkVersion [0,4,1])@ produces the version range @>= 0.4 && < 0.5@ (which is the
452 pvpize
:: Bool -> Version
-> VersionRange
453 pvpize
False v
= majorBoundVersion v
456 `intersectVersionRanges` earlierVersion
(incVersion
1 v
')
458 v
' = alterVersion
(take 2) v
460 -- | Increment the nth version component (counting from 0).
461 incVersion
:: Int -> Version
-> Version
462 incVersion n
= alterVersion
(incVersion
' n
)
464 incVersion
' 0 [] = [1]
465 incVersion
' 0 (v
: _
) = [v
+ 1]
466 incVersion
' m
[] = replicate m
0 ++ [1]
467 incVersion
' m
(v
: vs
) = v
: incVersion
' (m
- 1) vs
469 -- | Returns the current calendar year.
470 getCurrentYear
:: IO Integer
473 z
<- getCurrentTimeZone
474 let l
= utcToLocalTime z u
475 (y
, _
, _
) = toGregorian
$ localDay l
478 -- | From System.Directory.Extra
479 -- https://hackage.haskell.org/package/extra-1.7.9
480 listFilesInside
:: (FilePath -> IO Bool) -> FilePath -> IO [FilePath]
481 listFilesInside test dir
= ifNotM
(test
$ dropTrailingPathSeparator dir
) (pure
[]) $ do
482 (dirs
, files
) <- partitionM
doesDirectoryExist =<< listContents dir
483 rest
<- concatMapM
(listFilesInside test
) dirs
486 -- | From System.Directory.Extra
487 -- https://hackage.haskell.org/package/extra-1.7.9
488 listFilesRecursive
:: FilePath -> IO [FilePath]
489 listFilesRecursive
= listFilesInside
(const $ pure
True)
491 -- | From System.Directory.Extra
492 -- https://hackage.haskell.org/package/extra-1.7.9
493 listContents
:: FilePath -> IO [FilePath]
494 listContents dir
= do
495 xs
<- getDirectoryContents dir
496 pure
$ sort [dir
</> x | x
<- xs
, not $ all (== '.') x
]
498 -- | From Control.Monad.Extra
499 -- https://hackage.haskell.org/package/extra-1.7.9
500 ifM
:: Monad m
=> m
Bool -> m a
-> m a
-> m a
501 ifM b t f
= do b
' <- b
; if b
' then t
else f
503 -- | 'ifM' with swapped branches:
504 -- @ifNotM b t f = ifM (not <$> b) t f@
505 ifNotM
:: Monad m
=> m
Bool -> m a
-> m a
-> m a
508 -- | From Control.Monad.Extra
509 -- https://hackage.haskell.org/package/extra-1.7.9
510 concatMapM
:: Monad m
=> (a
-> m
[b
]) -> [a
] -> m
[b
]
511 {-# INLINE concatMapM #-}
512 concatMapM op
= foldr f
(pure
[])
514 f x xs
= do x
' <- op x
; if null x
' then xs
else do { xs
' <- xs
; pure
$ x
' ++ xs
' }
516 -- | From Control.Monad.Extra
517 -- https://hackage.haskell.org/package/extra-1.7.9
518 partitionM
:: Monad m
=> (a
-> m
Bool) -> [a
] -> m
([a
], [a
])
519 partitionM _
[] = pure
([], [])
520 partitionM f
(x
: xs
) = do
522 (as, bs
) <- partitionM f xs
523 pure
([x | res
] ++ as, [x |
not res
] ++ bs
)
525 safeRead
:: Read a
=> String -> Maybe a
527 |
[(x
, "")] <- reads s
= Just x
528 |
otherwise = Nothing
530 -- | @hasElem xs x = elem x xs@ except that @xs@ is turned into a 'Set' first.
531 -- Use underapplied to speed up subsequent lookups, e.g. @filter (hasElem xs) ys@.
532 -- Only amortized when used several times!
534 -- Time complexity \(O((n+m) \log(n))\) for \(m\) lookups in a list of length \(n\).
535 -- (Compare this to 'elem''s \(O(nm)\).)
537 -- This is [Agda.Utils.List.hasElem](https://hackage.haskell.org/package/Agda-2.6.2.2/docs/Agda-Utils-List.html#v:hasElem).
538 hasElem
:: Ord a
=> [a
] -> a
-> Bool
539 hasElem xs
= (`Set
.member` Set
.fromList xs
)
541 -- True if x occurs before y
542 occursOnlyOrBefore
:: Eq a
=> [a
] -> a
-> a
-> Bool
543 occursOnlyOrBefore xs x y
= case (elemIndex x xs
, elemIndex y xs
) of
544 (Just i
, Just j
) -> i
< j
548 giveRTSWarning
:: String -> String
549 giveRTSWarning
"run" =
550 "Your RTS options are applied to cabal, not the "
551 ++ "executable. Use '--' to separate cabal options from your "
552 ++ "executable options. For example, use 'cabal run -- +RTS -N "
553 ++ "to pass the '-N' RTS option to your executable."
554 giveRTSWarning
"test" =
555 "Some RTS options were found standalone, "
556 ++ "which affect cabal and not the binary. "
557 ++ "Please note that +RTS inside the --test-options argument "
558 ++ "suffices if your goal is to affect the tested binary. "
559 ++ "For example, use \"cabal test --test-options='+RTS -N'\" "
560 ++ "to pass the '-N' RTS option to your binary."
561 giveRTSWarning
"bench" =
562 "Some RTS options were found standalone, "
563 ++ "which affect cabal and not the binary. Please note "
564 ++ "that +RTS inside the --benchmark-options argument "
565 ++ "suffices if your goal is to affect the benchmarked "
566 ++ "binary. For example, use \"cabal test --benchmark-options="
567 ++ "'+RTS -N'\" to pass the '-N' RTS option to your binary."
569 "Your RTS options are applied to cabal, not the "