2 {-# LANGUAGE DataKinds #-}
3 {-# LANGUAGE ScopedTypeVariables #-}
5 module Distribution
.Client
.Utils
23 , makeRelativeCanonical
24 , filePathToByteString
25 , byteStringToFilePath
27 , canonicalizePathNoThrow
29 , existsAndIsMoreRecentThan
30 , tryReadAddSourcePackageDesc
31 , tryReadGenericPackageDesc
47 import Distribution
.Client
.Compat
.Prelude
50 import qualified Control
.Exception
as Exception
53 import qualified Control
.Exception
.Safe
as Safe
64 import qualified Data
.ByteString
.Lazy
as BS
69 import Distribution
.Client
.Errors
70 import Distribution
.Compat
.Environment
71 import Distribution
.Compat
.Time
(getModTime
)
72 import Distribution
.Simple
.Setup
(Flag
(..))
73 import Distribution
.Simple
.Utils
(dieWithException
, findPackageDesc
, noticeNoWrap
)
74 import Distribution
.Utils
.Path
82 , relativeSymbolicPath
84 , symbolicPathRelative_maybe
86 import Distribution
.Version
88 import System
.Directory
92 , getDirectoryContents
95 import qualified System
.Directory
as Directory
96 import System
.FilePath
104 import System
.IO.Unsafe
(unsafePerformIO
)
106 import Data
.Time
(utcToLocalTime
)
107 import Data
.Time
.Calendar
(toGregorian
)
108 import Data
.Time
.Clock
.POSIX
(getCurrentTime
)
109 import Data
.Time
.LocalTime
(getCurrentTimeZone
, localDay
)
110 import GHC
.Conc
.Sync
(getNumProcessors
)
111 import GHC
.IO.Encoding
112 ( TextEncoding
(TextEncoding
)
115 import GHC
.IO.Encoding
.Failure
116 ( CodingFailureMode
(TransliterateCodingFailure
)
119 #if defined
(mingw32_HOST_OS
) || MIN_VERSION_directory
(1,2,3)
120 import qualified System
.Directory
as Dir
121 import qualified System
.IO.Error
as IOError
123 import qualified Data
.Set
as Set
124 import Distribution
.Simple
.PackageDescription
(readGenericPackageDescription
)
125 import Distribution
.Types
.GenericPackageDescription
(GenericPackageDescription
)
127 -- | Generic merging utility. For sorted input lists this is a full outer join.
128 mergeBy
:: forall a b
. (a
-> b
-> Ordering) -> [a
] -> [b
] -> [MergeResult a b
]
131 merge
:: [a
] -> [b
] -> [MergeResult a b
]
132 merge
[] ys
= [OnlyInRight y | y
<- ys
]
133 merge xs
[] = [OnlyInLeft x | x
<- xs
]
134 merge
(x
: xs
) (y
: ys
) =
136 GT
-> OnlyInRight y
: merge
(x
: xs
) ys
137 EQ
-> InBoth x y
: merge xs ys
138 LT
-> OnlyInLeft x
: merge xs
(y
: ys
)
140 data MergeResult a b
= OnlyInLeft a | InBoth a b | OnlyInRight b
142 duplicates
:: Ord a
=> [a
] -> [[a
]]
143 duplicates
= duplicatesBy
compare
145 duplicatesBy
:: forall a
. (a
-> a
-> Ordering) -> [a
] -> [[a
]]
146 duplicatesBy cmp
= filter moreThanOne
. groupBy eq
. sortBy cmp
149 eq a b
= case cmp a b
of
152 moreThanOne
(_
: _
: _
) = True
153 moreThanOne _
= False
155 -- | Like 'removeFile', but does not throw an exception when the file does not
157 removeExistingFile
:: FilePath -> IO ()
158 removeExistingFile path
= do
159 exists
<- doesFileExist path
163 -- | A variant of 'withTempFile' that only gives us the file name, and while
164 -- it will clean up the file afterwards, it's lenient if the file is
169 -> (FilePath -> IO a
)
171 withTempFileName tmpDir template action
=
173 (openTempFile tmpDir template
)
174 (\(name
, _
) -> removeExistingFile name
)
175 (\(name
, h
) -> hClose h
>> action name
)
177 -- | Executes the action with an environment variable set to some
180 -- Warning: This operation is NOT thread-safe, because current
181 -- environment is a process-global concept.
182 withEnv
:: String -> String -> IO a
-> IO a
184 mb_old
<- lookupEnv k
186 m `Exception
.finally` setOrUnsetEnv k mb_old
188 -- | Executes the action with a list of environment variables and
189 -- corresponding overrides, where
191 -- * @'Just' v@ means \"set the environment variable's value to @v@\".
192 -- * 'Nothing' means \"unset the environment variable\".
194 -- Warning: This operation is NOT thread-safe, because current
195 -- environment is a process-global concept.
196 withEnvOverrides
:: [(String, Maybe FilePath)] -> IO a
-> IO a
197 withEnvOverrides overrides m
= do
198 mb_olds
<- traverse lookupEnv envVars
199 traverse_
(uncurry setOrUnsetEnv
) overrides
200 m `Exception
.finally`
zipWithM_ setOrUnsetEnv envVars mb_olds
203 envVars
= map fst overrides
205 setOrUnsetEnv
:: String -> Maybe String -> IO ()
206 setOrUnsetEnv var Nothing
= unsetEnv var
207 setOrUnsetEnv var
(Just val
) = setEnv var val
209 -- | Executes the action, increasing the PATH environment
212 -- Warning: This operation is NOT thread-safe, because the
213 -- environment variables are a process-global concept.
214 withExtraPathEnv
:: [FilePath] -> IO a
-> IO a
215 withExtraPathEnv paths m
= do
216 oldPathSplit
<- getSearchPath
217 let newPath
:: String
218 newPath
= mungePath
$ intercalate
[searchPathSeparator
] (paths
++ oldPathSplit
)
220 oldPath
= mungePath
$ intercalate
[searchPathSeparator
] oldPathSplit
221 -- TODO: This is a horrible hack to work around the fact that
222 -- setEnv can't take empty values as an argument
224 | p
== "" = "/dev/null"
226 setEnv
"PATH" newPath
227 m `Exception
.finally` setEnv
"PATH" oldPath
229 -- | Log directory change in 'make' compatible syntax
230 logDirChange
:: (String -> IO ()) -> Maybe FilePath -> IO a
-> IO a
231 logDirChange _ Nothing m
= m
232 logDirChange l
(Just d
) m
= do
233 l
$ "cabal: Entering directory '" ++ d
++ "'\n"
235 `Exception
.finally` l
("cabal: Leaving directory '" ++ d
++ "'\n")
237 -- The number of processors is not going to change during the duration of the
238 -- program, so unsafePerformIO is safe here.
239 numberOfProcessors
:: Int
240 numberOfProcessors
= unsafePerformIO getNumProcessors
242 -- | Determine the number of jobs to use given the value of the '-j' flag.
243 determineNumJobs
:: Flag
(Maybe Int) -> Int
244 determineNumJobs numJobsFlag
=
247 Flag Nothing
-> numberOfProcessors
250 -- | Given a relative path, make it absolute relative to the current
251 -- directory. Absolute paths are returned unmodified.
252 makeAbsoluteToCwd
:: FilePath -> IO FilePath
253 makeAbsoluteToCwd path
254 | isAbsolute path
= return path
256 cwd
<- Directory
.getCurrentDirectory
257 return $! cwd
</> path
259 -- | Given a path (relative or absolute), make it relative to the current
260 -- directory, including using @../..@ if necessary.
261 makeRelativeToCwd
:: FilePath -> IO FilePath
262 makeRelativeToCwd path
=
263 makeRelativeCanonical
<$> canonicalizePath path
<*> Directory
.getCurrentDirectory
265 -- | Given a path (relative or absolute), make it relative to the given
266 -- directory, including using @../..@ if necessary.
267 makeRelativeToDir
:: FilePath -> FilePath -> IO FilePath
268 makeRelativeToDir path dir
=
269 makeRelativeCanonical
<$> canonicalizePath path
<*> canonicalizePath dir
271 -- | makeRelativeToDir for SymbolicPath
272 makeRelativeToDirS
:: Maybe (SymbolicPath CWD
(Dir dir
)) -> SymbolicPath CWD to
-> IO (SymbolicPath dir to
)
273 makeRelativeToDirS Nothing s
= makeRelativeToDirS
(Just sameDirectory
) s
274 makeRelativeToDirS
(Just root
) p
=
275 case symbolicPathRelative_maybe p
of
276 -- TODO: Use AbsolutePath
277 Nothing
-> return $ makeSymbolicPath
(getSymbolicPath p
)
279 makeSymbolicPath
<$> makeRelativeToDir
(getSymbolicPath root
) (getSymbolicPath rel_path
)
281 -- | Given a canonical absolute path and canonical absolute dir, make the path
282 -- relative to the directory, including using @../..@ if necessary. Returns
283 -- the original absolute path if it is not on the same drive as the given dir.
284 makeRelativeCanonical
:: FilePath -> FilePath -> FilePath
285 makeRelativeCanonical path dir
286 | takeDrive path
/= takeDrive dir
= path
287 |
otherwise = go
(splitPath path
) (splitPath dir
)
289 go
(p
: ps
) (d
: ds
) | p
' == d
' = go ps ds
291 (p
', d
') = (dropTrailingPathSeparator p
, dropTrailingPathSeparator d
)
293 go ps ds
= joinPath
(replicate (length ds
) ".." ++ ps
)
295 -- | Convert a 'FilePath' to a lazy 'ByteString'. Each 'Char' is
296 -- encoded as a little-endian 'Word32'.
297 filePathToByteString
:: FilePath -> BS
.ByteString
298 filePathToByteString p
=
299 BS
.pack
$ foldr conv
[] codepts
302 codepts
= map (fromIntegral . ord) p
304 conv
:: Word32
-> [Word8
] -> [Word8
]
305 conv w32 rest
= b0
: b1
: b2
: b3
: rest
307 b0
= fromIntegral $ w32
308 b1
= fromIntegral $ w32 `shiftR`
8
309 b2
= fromIntegral $ w32 `shiftR`
16
310 b3
= fromIntegral $ w32 `shiftR`
24
312 -- | Reverse operation to 'filePathToByteString'.
313 byteStringToFilePath
:: BS
.ByteString
-> FilePath
314 byteStringToFilePath bs
315 | bslen `
mod`
4 /= 0 = unexpected
318 unexpected
= "Distribution.Client.Utils.byteStringToFilePath: unexpected"
323 |
otherwise = (chr . fromIntegral $ w32
) : go
(i
+ 4)
326 w32
= b0
.|
. (b1 `shiftL`
8) .|
. (b2 `shiftL`
16) .|
. (b3 `shiftL`
24)
327 b0
= fromIntegral $ BS
.index bs i
328 b1
= fromIntegral $ BS
.index bs
(i
+ 1)
329 b2
= fromIntegral $ BS
.index bs
(i
+ 2)
330 b3
= fromIntegral $ BS
.index bs
(i
+ 3)
332 -- | Workaround for the inconsistent behaviour of 'canonicalizePath'. Always
333 -- throws an error if the path refers to a non-existent file.
334 {- FOURMOLU_DISABLE -}
335 tryCanonicalizePath
:: FilePath -> IO FilePath
336 tryCanonicalizePath path
= do
337 ret
<- canonicalizePath path
338 #if defined
(mingw32_HOST_OS
) || MIN_VERSION_directory
(1,2,3)
339 exists
<- liftM2 (||
) (doesFileExist ret
) (Dir
.doesDirectoryExist ret
)
341 IOError.ioError $ IOError.mkIOError
IOError.doesNotExistErrorType
"canonicalizePath"
345 {- FOURMOLU_ENABLE -}
347 -- | A non-throwing wrapper for 'canonicalizePath'. If 'canonicalizePath' throws
348 -- an exception, returns the path argument unmodified.
349 canonicalizePathNoThrow
:: FilePath -> IO FilePath
350 canonicalizePathNoThrow path
= do
351 canonicalizePath path `catchIO`
(\_
-> return path
)
356 -- | Like Distribution.Simple.Utils.moreRecentFile, but uses getModTime instead
357 -- of getModificationTime for higher precision. We can't merge the two because
358 -- Distribution.Client.Time uses MIN_VERSION macros.
359 moreRecentFile
:: FilePath -> FilePath -> IO Bool
360 moreRecentFile a b
= do
361 exists
<- doesFileExist b
369 -- | Like 'moreRecentFile', but also checks that the first file exists.
370 existsAndIsMoreRecentThan
:: FilePath -> FilePath -> IO Bool
371 existsAndIsMoreRecentThan a b
= do
372 exists
<- doesFileExist a
375 else a `moreRecentFile` b
377 -- | Sets the handler for encoding errors to one that transliterates invalid
378 -- characters into one present in the encoding (i.e., \'?\').
379 -- This is opposed to the default behavior, which is to throw an exception on
380 -- error. This function will ignore file handles that have a Unicode encoding
381 -- set. It's a no-op for versions of `base` less than 4.4.
382 relaxEncodingErrors
:: Handle -> IO ()
383 relaxEncodingErrors handle
= do
384 maybeEncoding
<- hGetEncoding handle
385 case maybeEncoding
of
386 Just
(TextEncoding name decoder encoder
)
387 |
not ("UTF" `
isPrefixOf` name
) ->
388 let relax x
= x
{recover
= recoverEncode TransliterateCodingFailure
}
389 in hSetEncoding handle
(TextEncoding name decoder
(fmap relax encoder
))
393 -- | Like 'tryFindPackageDesc', but with error specific to add-source deps.
394 tryReadAddSourcePackageDesc
398 -> IO GenericPackageDescription
399 tryReadAddSourcePackageDesc verbosity depPath err
= do
400 let pkgDir
= makeSymbolicPath depPath
402 try_find_package_desc verbosity pkgDir
$
405 ++ "Failed to read cabal file of add-source dependency: "
407 readGenericPackageDescription verbosity
(Just pkgDir
) (relativeSymbolicPath pkgDescPath
)
409 -- | Try to read a @.cabal@ file, in directory @depPath@. Fails if one cannot be
410 -- found, with @err@ prefixing the error message. This function simply allows
411 -- us to give a more descriptive error than that provided by @findPackageDesc@.
412 tryReadGenericPackageDesc
414 -> SymbolicPath CWD
(Dir Pkg
)
416 -> IO GenericPackageDescription
417 tryReadGenericPackageDesc verbosity pkgDir err
= do
418 pkgDescPath
<- try_find_package_desc verbosity pkgDir err
419 readGenericPackageDescription verbosity
(Just pkgDir
) (relativeSymbolicPath pkgDescPath
)
421 -- | Internal helper function for 'tryReadAddSourcePackageDesc' and 'tryReadGenericPackageDesc'.
422 try_find_package_desc
424 -> SymbolicPath CWD
(Dir Pkg
)
426 -> IO (RelativePath Pkg File
)
427 try_find_package_desc verbosity pkgDir err
= do
428 errOrCabalFile
<- findPackageDesc
(Just pkgDir
)
429 case errOrCabalFile
of
430 Right file
-> return file
431 Left _
-> dieWithException verbosity
$ TryFindPackageDescErr err
433 -- | Phase of building a dependency. Represents current status of package
434 -- dependency processing. See #4040 for details.
436 = ProgressDownloading
444 progressMessage
:: Verbosity
-> ProgressPhase
-> String -> IO ()
445 progressMessage verbosity
phase subject
= do
446 noticeNoWrap verbosity
$ phaseStr
++ subject
++ "\n"
448 phaseStr
= case phase of
449 ProgressDownloading
->
451 ProgressDownloaded
->
459 ProgressInstalling
->
464 -- | Given a version, return an API-compatible (according to PVP) version range.
466 -- If the boolean argument denotes whether to use a desugared
467 -- representation (if 'True') or the new-style @^>=@-form (if
470 -- Example: @pvpize True (mkVersion [0,4,1])@ produces the version range @>= 0.4 && < 0.5@ (which is the
472 pvpize
:: Bool -> Version
-> VersionRange
473 pvpize
False v
= majorBoundVersion v
476 `intersectVersionRanges` earlierVersion
(incVersion
1 v
')
478 v
' = alterVersion
(take 2) v
480 -- | Increment the nth version component (counting from 0).
481 incVersion
:: Int -> Version
-> Version
482 incVersion n
= alterVersion
(incVersion
' n
)
484 incVersion
' 0 [] = [1]
485 incVersion
' 0 (v
: _
) = [v
+ 1]
486 incVersion
' m
[] = replicate m
0 ++ [1]
487 incVersion
' m
(v
: vs
) = v
: incVersion
' (m
- 1) vs
489 -- | Returns the current calendar year.
490 getCurrentYear
:: IO Integer
493 z
<- getCurrentTimeZone
494 let l
= utcToLocalTime z u
495 (y
, _
, _
) = toGregorian
$ localDay l
498 -- | From System.Directory.Extra
499 -- https://hackage.haskell.org/package/extra-1.7.9
500 listFilesInside
:: (FilePath -> IO Bool) -> FilePath -> IO [FilePath]
501 listFilesInside test dir
= ifNotM
(test
$ dropTrailingPathSeparator dir
) (pure
[]) $ do
502 (dirs
, files
) <- partitionM
doesDirectoryExist =<< listContents dir
503 rest
<- concatMapM
(listFilesInside test
) dirs
506 -- | From System.Directory.Extra
507 -- https://hackage.haskell.org/package/extra-1.7.9
508 listFilesRecursive
:: FilePath -> IO [FilePath]
509 listFilesRecursive
= listFilesInside
(const $ pure
True)
511 -- | From System.Directory.Extra
512 -- https://hackage.haskell.org/package/extra-1.7.9
513 listContents
:: FilePath -> IO [FilePath]
514 listContents dir
= do
515 xs
<- getDirectoryContents dir
516 pure
$ sort [dir
</> x | x
<- xs
, not $ all (== '.') x
]
518 -- | From Control.Monad.Extra
519 -- https://hackage.haskell.org/package/extra-1.7.9
520 ifM
:: Monad m
=> m
Bool -> m a
-> m a
-> m a
521 ifM b t f
= do b
' <- b
; if b
' then t
else f
523 -- | 'ifM' with swapped branches:
524 -- @ifNotM b t f = ifM (not <$> b) t f@
525 ifNotM
:: Monad m
=> m
Bool -> m a
-> m a
-> m a
528 -- | From Control.Monad.Extra
529 -- https://hackage.haskell.org/package/extra-1.7.9
530 concatMapM
:: Monad m
=> (a
-> m
[b
]) -> [a
] -> m
[b
]
531 {-# INLINE concatMapM #-}
532 concatMapM op
= foldr f
(pure
[])
534 f x xs
= do x
' <- op x
; if null x
' then xs
else do { xs
' <- xs
; pure
$ x
' ++ xs
' }
536 -- | From Control.Monad.Extra
537 -- https://hackage.haskell.org/package/extra-1.7.9
538 partitionM
:: Monad m
=> (a
-> m
Bool) -> [a
] -> m
([a
], [a
])
539 partitionM _
[] = pure
([], [])
540 partitionM f
(x
: xs
) = do
542 (as, bs
) <- partitionM f xs
543 pure
([x | res
] ++ as, [x |
not res
] ++ bs
)
545 safeRead
:: Read a
=> String -> Maybe a
547 |
[(x
, "")] <- reads s
= Just x
548 |
otherwise = Nothing
550 -- | @hasElem xs x = elem x xs@ except that @xs@ is turned into a 'Set' first.
551 -- Use underapplied to speed up subsequent lookups, e.g. @filter (hasElem xs) ys@.
552 -- Only amortized when used several times!
554 -- Time complexity \(O((n+m) \log(n))\) for \(m\) lookups in a list of length \(n\).
555 -- (Compare this to 'elem''s \(O(nm)\).)
557 -- This is [Agda.Utils.List.hasElem](https://hackage.haskell.org/package/Agda-2.6.2.2/docs/Agda-Utils-List.html#v:hasElem).
558 hasElem
:: Ord a
=> [a
] -> a
-> Bool
559 hasElem xs
= (`Set
.member` Set
.fromList xs
)
561 -- True if x occurs before y
562 occursOnlyOrBefore
:: Eq a
=> [a
] -> a
-> a
-> Bool
563 occursOnlyOrBefore xs x y
= case (elemIndex x xs
, elemIndex y xs
) of
564 (Just i
, Just j
) -> i
< j
568 giveRTSWarning
:: String -> String
569 giveRTSWarning
"run" =
570 "Your RTS options are applied to cabal, not the "
571 ++ "executable. Use '--' to separate cabal options from your "
572 ++ "executable options. For example, use 'cabal run -- +RTS -N "
573 ++ "to pass the '-N' RTS option to your executable."
574 giveRTSWarning
"test" =
575 "Some RTS options were found standalone, "
576 ++ "which affect cabal and not the binary. "
577 ++ "Please note that +RTS inside the --test-options argument "
578 ++ "suffices if your goal is to affect the tested binary. "
579 ++ "For example, use \"cabal test --test-options='+RTS -N'\" "
580 ++ "to pass the '-N' RTS option to your binary."
581 giveRTSWarning
"bench" =
582 "Some RTS options were found standalone, "
583 ++ "which affect cabal and not the binary. Please note "
584 ++ "that +RTS inside the --benchmark-options argument "
585 ++ "suffices if your goal is to affect the benchmarked "
586 ++ "binary. For example, use \"cabal test --benchmark-options="
587 ++ "'+RTS -N'\" to pass the '-N' RTS option to your binary."
589 "Your RTS options are applied to cabal, not the "