Add “Ignore warning” option to cabal check
[cabal.git] / cabal-install / src / Distribution / Client / Utils.hs
blob59158ffd2a5dd06e63a910bbe56497b435581c91
1 {-# LANGUAGE CPP #-}
2 {-# LANGUAGE ScopedTypeVariables #-}
4 module Distribution.Client.Utils
5 ( MergeResult (..)
6 , mergeBy
7 , duplicates
8 , duplicatesBy
9 , readMaybe
10 , inDir
11 , withEnv
12 , withEnvOverrides
13 , logDirChange
14 , withExtraPathEnv
15 , determineNumJobs
16 , numberOfProcessors
17 , removeExistingFile
18 , withTempFileName
19 , makeAbsoluteToCwd
20 , makeRelativeToCwd
21 , makeRelativeToDir
22 , makeRelativeCanonical
23 , filePathToByteString
24 , byteStringToFilePath
25 , tryCanonicalizePath
26 , canonicalizePathNoThrow
27 , moreRecentFile
28 , existsAndIsMoreRecentThan
29 , tryFindAddSourcePackageDesc
30 , tryFindPackageDesc
31 , findOpenProgramLocation
32 , relaxEncodingErrors
33 , ProgressPhase (..)
34 , progressMessage
35 , pvpize
36 , incVersion
37 , getCurrentYear
38 , listFilesRecursive
39 , listFilesInside
40 , safeRead
41 , hasElem
42 , occursOnlyOrBefore
43 , giveRTSWarning
44 ) where
46 import Distribution.Client.Compat.Prelude
47 import Prelude ()
49 import qualified Control.Exception as Exception
50 ( finally
52 import qualified Control.Exception.Safe as Safe
53 ( bracket
55 import Control.Monad
56 ( zipWithM_
58 import Data.Bits
59 ( shiftL
60 , shiftR
61 , (.|.)
63 import qualified Data.ByteString.Lazy as BS
64 import Data.List
65 ( elemIndex
66 , groupBy
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
75 ( canonicalizePath
76 , doesDirectoryExist
77 , doesFileExist
78 , findExecutable
79 , getCurrentDirectory
80 , getDirectoryContents
81 , removeFile
82 , setCurrentDirectory
84 import System.FilePath
85 import System.IO
86 ( Handle
87 , hClose
88 , hGetEncoding
89 , hSetEncoding
90 , openTempFile
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)
101 , recover
103 import GHC.IO.Encoding.Failure
104 ( CodingFailureMode (TransliterateCodingFailure)
105 , recoverEncode
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
110 #endif
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]
116 mergeBy cmp = merge
117 where
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) =
122 case x `cmp` y of
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
134 where
135 eq :: a -> a -> Bool
136 eq a b = case cmp a b of
137 EQ -> True
138 _ -> False
139 moreThanOne (_ : _ : _) = True
140 moreThanOne _ = False
142 -- | Like 'removeFile', but does not throw an exception when the file does not
143 -- exist.
144 removeExistingFile :: FilePath -> IO ()
145 removeExistingFile path = do
146 exists <- doesFileExist path
147 when exists $
148 removeFile 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
152 -- moved\/deleted.
153 withTempFileName
154 :: FilePath
155 -> String
156 -> (FilePath -> IO a)
157 -> IO a
158 withTempFileName tmpDir template action =
159 Safe.bracket
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
169 inDir Nothing m = m
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
176 -- value.
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
181 withEnv k v m = do
182 mb_old <- lookupEnv k
183 setEnv k v
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
199 where
200 envVars :: [String]
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
208 -- in some way
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)
217 oldPath :: String
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
221 mungePath p
222 | p == "" = "/dev/null"
223 | otherwise = p
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 =
243 case numJobsFlag of
244 NoFlag -> 1
245 Flag Nothing -> numberOfProcessors
246 Flag (Just n) -> n
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
253 | otherwise = do
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)
276 where
277 go (p : ps) (d : ds) | p' == d' = go ps ds
278 where
279 (p', d') = (dropTrailingPathSeparator p, dropTrailingPathSeparator d)
280 go [] [] = "./"
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
288 where
289 codepts :: [Word32]
290 codepts = map (fromIntegral . ord) p
292 conv :: Word32 -> [Word8] -> [Word8]
293 conv w32 rest = b0 : b1 : b2 : b3 : rest
294 where
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
304 | otherwise = go 0
305 where
306 unexpected = "Distribution.Client.Utils.byteStringToFilePath: unexpected"
307 bslen = BS.length bs
309 go i
310 | i == bslen = []
311 | otherwise = (chr . fromIntegral $ w32) : go (i + 4)
312 where
313 w32 :: Word32
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)
328 unless exists $
329 IOError.ioError $ IOError.mkIOError IOError.doesNotExistErrorType "canonicalizePath"
330 Nothing (Just ret)
331 #endif
332 return ret
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)
341 --------------------
342 -- Modification time
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
350 if not exists
351 then return True
352 else do
353 tb <- getModTime b
354 ta <- getModTime a
355 return (ta > tb)
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
361 if not exists
362 then return False
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))
378 _ ->
379 return ()
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 $
386 ++ "\n"
387 ++ "Failed to read cabal file of add-source dependency: "
388 ++ depPath
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) =
403 locate name = do
404 exe <- findExecutable name
405 case exe of
406 Just s -> pure (Right s)
407 Nothing -> pure (Left ("Couldn't find file-opener program `" <> name <> "`"))
408 xdg = locate "xdg-open"
410 case os of
411 Windows -> pure (Right "start")
412 OSX -> locate "open"
413 Linux -> xdg
414 FreeBSD -> xdg
415 OpenBSD -> xdg
416 NetBSD -> xdg
417 DragonFly -> xdg
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.
422 data ProgressPhase
423 = ProgressDownloading
424 | ProgressDownloaded
425 | ProgressStarting
426 | ProgressBuilding
427 | ProgressHaddock
428 | ProgressInstalling
429 | ProgressCompleted
431 progressMessage :: Verbosity -> ProgressPhase -> String -> IO ()
432 progressMessage verbosity phase subject = do
433 noticeNoWrap verbosity $ phaseStr ++ subject ++ "\n"
434 where
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
448 -- 'False').
450 -- Example: @pvpize True (mkVersion [0,4,1])@ produces the version range @>= 0.4 && < 0.5@ (which is the
451 -- same as @0.4.*@).
452 pvpize :: Bool -> Version -> VersionRange
453 pvpize False v = majorBoundVersion v
454 pvpize True v =
455 orLaterVersion v'
456 `intersectVersionRanges` earlierVersion (incVersion 1 v')
457 where
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)
463 where
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
471 getCurrentYear = do
472 u <- getCurrentTime
473 z <- getCurrentTimeZone
474 let l = utcToLocalTime z u
475 (y, _, _) = toGregorian $ localDay l
476 return y
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
484 pure $ files ++ rest
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
506 ifNotM = flip . ifM
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 [])
513 where
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
521 res <- f x
522 (as, bs) <- partitionM f xs
523 pure ([x | res] ++ as, [x | not res] ++ bs)
525 safeRead :: Read a => String -> Maybe a
526 safeRead s
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
545 (Just _, _) -> True
546 _ -> False
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."
568 giveRTSWarning _ =
569 "Your RTS options are applied to cabal, not the "
570 ++ "binary."