module GHC.Utils.TmpFs
( TmpFs
, initTmpFs
, forkTmpFsFrom
, mergeTmpFsInto
, FilesToClean(..)
, emptyFilesToClean
, TempFileLifetime(..)
, cleanTempDirs
, cleanTempFiles
, cleanCurrentModuleTempFiles
, addFilesToClean
, changeTempFilesLifetime
, newTempName
, newTempLibName
, newTempDir
, withSystemTempDirectory
, withTempDirectory
)
where
import GHC.Prelude
import GHC.Driver.Session
import GHC.Utils.Error
import GHC.Utils.Outputable
import GHC.Utils.Logger
import GHC.Utils.Misc
import GHC.Utils.Exception as Exception
import GHC.Driver.Phases
import Control.Monad
import Data.List (partition)
import qualified Data.Set as Set
import Data.Set (Set)
import qualified Data.Map as Map
import Data.Map (Map)
import Data.IORef
import System.Directory
import System.FilePath
import System.IO.Error
#if !defined(mingw32_HOST_OS)
import qualified System.Posix.Internals
#endif
data TmpFs = TmpFs
{ tmp_dirs_to_clean :: IORef (Map FilePath FilePath)
, tmp_next_suffix :: IORef Int
, tmp_files_to_clean :: IORef FilesToClean
}
data FilesToClean = FilesToClean
{ ftcGhcSession :: !(Set FilePath)
, ftcCurrentModule :: !(Set FilePath)
}
data TempFileLifetime
= TFL_CurrentModule
| TFL_GhcSession
deriving (Show)
emptyFilesToClean :: FilesToClean
emptyFilesToClean = FilesToClean Set.empty Set.empty
mergeFilesToClean :: FilesToClean -> FilesToClean -> FilesToClean
mergeFilesToClean x y = FilesToClean
{ ftcGhcSession = Set.union (ftcGhcSession x) (ftcGhcSession y)
, ftcCurrentModule = Set.union (ftcCurrentModule x) (ftcCurrentModule y)
}
initTmpFs :: IO TmpFs
initTmpFs = do
files <- newIORef emptyFilesToClean
dirs <- newIORef Map.empty
next <- newIORef 0
return $ TmpFs
{ tmp_files_to_clean = files
, tmp_dirs_to_clean = dirs
, tmp_next_suffix = next
}
forkTmpFsFrom :: TmpFs -> IO TmpFs
forkTmpFsFrom old = do
files <- newIORef emptyFilesToClean
return $ TmpFs
{ tmp_files_to_clean = files
, tmp_dirs_to_clean = tmp_dirs_to_clean old
, tmp_next_suffix = tmp_next_suffix old
}
mergeTmpFsInto :: TmpFs -> TmpFs -> IO ()
mergeTmpFsInto src dst = do
src_files <- atomicModifyIORef' (tmp_files_to_clean src) (\s -> (emptyFilesToClean, s))
atomicModifyIORef' (tmp_files_to_clean dst) (\s -> (mergeFilesToClean src_files s, ()))
cleanTempDirs :: Logger -> TmpFs -> DynFlags -> IO ()
cleanTempDirs logger tmpfs dflags
= unless (gopt Opt_KeepTmpFiles dflags)
$ mask_
$ do let ref = tmp_dirs_to_clean tmpfs
ds <- atomicModifyIORef' ref $ \ds -> (Map.empty, ds)
removeTmpDirs logger dflags (Map.elems ds)
cleanTempFiles :: Logger -> TmpFs -> DynFlags -> IO ()
cleanTempFiles logger tmpfs dflags
= unless (gopt Opt_KeepTmpFiles dflags)
$ mask_
$ do let ref = tmp_files_to_clean tmpfs
to_delete <- atomicModifyIORef' ref $
\FilesToClean
{ ftcCurrentModule = cm_files
, ftcGhcSession = gs_files
} -> ( emptyFilesToClean
, Set.toList cm_files ++ Set.toList gs_files)
removeTmpFiles logger dflags to_delete
cleanCurrentModuleTempFiles :: Logger -> TmpFs -> DynFlags -> IO ()
cleanCurrentModuleTempFiles logger tmpfs dflags
= unless (gopt Opt_KeepTmpFiles dflags)
$ mask_
$ do let ref = tmp_files_to_clean tmpfs
to_delete <- atomicModifyIORef' ref $
\ftc@FilesToClean{ftcCurrentModule = cm_files} ->
(ftc {ftcCurrentModule = Set.empty}, Set.toList cm_files)
removeTmpFiles logger dflags to_delete
addFilesToClean :: TmpFs -> TempFileLifetime -> [FilePath] -> IO ()
addFilesToClean tmpfs lifetime new_files = modifyIORef' (tmp_files_to_clean tmpfs) $
\FilesToClean
{ ftcCurrentModule = cm_files
, ftcGhcSession = gs_files
} -> case lifetime of
TFL_CurrentModule -> FilesToClean
{ ftcCurrentModule = cm_files `Set.union` new_files_set
, ftcGhcSession = gs_files `Set.difference` new_files_set
}
TFL_GhcSession -> FilesToClean
{ ftcCurrentModule = cm_files `Set.difference` new_files_set
, ftcGhcSession = gs_files `Set.union` new_files_set
}
where
new_files_set = Set.fromList new_files
changeTempFilesLifetime :: TmpFs -> TempFileLifetime -> [FilePath] -> IO ()
changeTempFilesLifetime tmpfs lifetime files = do
FilesToClean
{ ftcCurrentModule = cm_files
, ftcGhcSession = gs_files
} <- readIORef (tmp_files_to_clean tmpfs)
let old_set = case lifetime of
TFL_CurrentModule -> gs_files
TFL_GhcSession -> cm_files
existing_files = [f | f <- files, f `Set.member` old_set]
addFilesToClean tmpfs lifetime existing_files
newTempSuffix :: TmpFs -> IO Int
newTempSuffix tmpfs =
atomicModifyIORef' (tmp_next_suffix tmpfs) $ \n -> (n+1,n)
newTempName :: Logger -> TmpFs -> DynFlags -> TempFileLifetime -> Suffix -> IO FilePath
newTempName logger tmpfs dflags lifetime extn
= do d <- getTempDir logger tmpfs dflags
findTempName (d </> "ghc_")
where
findTempName :: FilePath -> IO FilePath
findTempName prefix
= do n <- newTempSuffix tmpfs
let filename = prefix ++ show n <.> extn
b <- doesFileExist filename
if b then findTempName prefix
else do
addFilesToClean tmpfs lifetime [filename]
return filename
newTempDir :: Logger -> TmpFs -> DynFlags -> IO FilePath
newTempDir logger tmpfs dflags
= do d <- getTempDir logger tmpfs dflags
findTempDir (d </> "ghc_")
where
findTempDir :: FilePath -> IO FilePath
findTempDir prefix
= do n <- newTempSuffix tmpfs
let filename = prefix ++ show n
b <- doesDirectoryExist filename
if b then findTempDir prefix
else do createDirectory filename
return filename
newTempLibName :: Logger -> TmpFs -> DynFlags -> TempFileLifetime -> Suffix
-> IO (FilePath, FilePath, String)
newTempLibName logger tmpfs dflags lifetime extn
= do d <- getTempDir logger tmpfs dflags
findTempName d ("ghc_")
where
findTempName :: FilePath -> String -> IO (FilePath, FilePath, String)
findTempName dir prefix
= do n <- newTempSuffix tmpfs
let libname = prefix ++ show n
filename = dir </> "lib" ++ libname <.> extn
b <- doesFileExist filename
if b then findTempName dir prefix
else do
addFilesToClean tmpfs lifetime [filename]
return (filename, dir, libname)
getTempDir :: Logger -> TmpFs -> DynFlags -> IO FilePath
getTempDir logger tmpfs dflags = do
mapping <- readIORef dir_ref
case Map.lookup tmp_dir mapping of
Nothing -> do
pid <- getProcessID
let prefix = tmp_dir </> "ghc" ++ show pid ++ "_"
mask_ $ mkTempDir prefix
Just dir -> return dir
where
tmp_dir = tmpDir dflags
dir_ref = tmp_dirs_to_clean tmpfs
mkTempDir :: FilePath -> IO FilePath
mkTempDir prefix = do
n <- newTempSuffix tmpfs
let our_dir = prefix ++ show n
createDirectory our_dir
their_dir <- atomicModifyIORef' dir_ref $ \mapping ->
case Map.lookup tmp_dir mapping of
Just dir -> (mapping, Just dir)
Nothing -> (Map.insert tmp_dir our_dir mapping, Nothing)
case their_dir of
Nothing -> do
debugTraceMsg logger dflags 2 $
text "Created temporary directory:" <+> text our_dir
return our_dir
Just dir -> do
removeDirectory our_dir
return dir
`catchIO` \e -> if isAlreadyExistsError e
then mkTempDir prefix else ioError e
removeTmpDirs :: Logger -> DynFlags -> [FilePath] -> IO ()
removeTmpDirs logger dflags ds
= traceCmd logger dflags "Deleting temp dirs"
("Deleting: " ++ unwords ds)
(mapM_ (removeWith logger dflags removeDirectory) ds)
removeTmpFiles :: Logger -> DynFlags -> [FilePath] -> IO ()
removeTmpFiles logger dflags fs
= warnNon $
traceCmd logger dflags "Deleting temp files"
("Deleting: " ++ unwords deletees)
(mapM_ (removeWith logger dflags removeFile) deletees)
where
warnNon act
| null non_deletees = act
| otherwise = do
putMsg logger dflags (text "WARNING - NOT deleting source files:"
<+> hsep (map text non_deletees))
act
(non_deletees, deletees) = partition isHaskellUserSrcFilename fs
removeWith :: Logger -> DynFlags -> (FilePath -> IO ()) -> FilePath -> IO ()
removeWith logger dflags remover f = remover f `catchIO`
(\e ->
let msg = if isDoesNotExistError e
then text "Warning: deleting non-existent" <+> text f
else text "Warning: exception raised when deleting"
<+> text f <> colon
$$ text (show e)
in debugTraceMsg logger dflags 2 msg
)
#if defined(mingw32_HOST_OS)
foreign import ccall unsafe "_getpid" getProcessID :: IO Int
#else
getProcessID :: IO Int
getProcessID = System.Posix.Internals.c_getpid >>= return . fromIntegral
#endif
withSystemTempDirectory :: String
-> (FilePath -> IO a)
-> IO a
withSystemTempDirectory template action =
getTemporaryDirectory >>= \tmpDir -> withTempDirectory tmpDir template action
withTempDirectory :: FilePath
-> String
-> (FilePath -> IO a)
-> IO a
withTempDirectory targetDir template =
Exception.bracket
(createTempDirectory targetDir template)
(ignoringIOErrors . removeDirectoryRecursive)
ignoringIOErrors :: IO () -> IO ()
ignoringIOErrors ioe = ioe `catchIO` const (return ())
createTempDirectory :: FilePath -> String -> IO FilePath
createTempDirectory dir template = do
pid <- getProcessID
findTempName pid
where findTempName x = do
let path = dir </> template ++ show x
createDirectory path
return path
`catchIO` \e -> if isAlreadyExistsError e
then findTempName (x+1) else ioError e