summaryrefslogtreecommitdiff
path: root/compiler/GHC/Utils/TmpFs.hs
diff options
context:
space:
mode:
Diffstat (limited to 'compiler/GHC/Utils/TmpFs.hs')
-rw-r--r--compiler/GHC/Utils/TmpFs.hs409
1 files changed, 409 insertions, 0 deletions
diff --git a/compiler/GHC/Utils/TmpFs.hs b/compiler/GHC/Utils/TmpFs.hs
new file mode 100644
index 0000000000..d108f55b3b
--- /dev/null
+++ b/compiler/GHC/Utils/TmpFs.hs
@@ -0,0 +1,409 @@
+{-# LANGUAGE CPP #-}
+
+-- | Temporary file-system management
+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
+
+-- | Temporary file-system
+data TmpFs = TmpFs
+ { tmp_dirs_to_clean :: IORef (Map FilePath FilePath)
+ -- ^ Maps system temporary directory (passed via settings or DynFlags) to
+ -- an actual temporary directory for this process.
+ --
+ -- It's a Map probably to support changing the system temporary directory
+ -- over time.
+ --
+ -- Shared with forked TmpFs.
+
+ , tmp_next_suffix :: IORef Int
+ -- ^ The next available suffix to uniquely name a temp file, updated
+ -- atomically.
+ --
+ -- Shared with forked TmpFs.
+
+ , tmp_files_to_clean :: IORef FilesToClean
+ -- ^ Files to clean (per session or per module)
+ --
+ -- Not shared with forked TmpFs.
+ }
+
+-- | A collection of files that must be deleted before ghc exits.
+data FilesToClean = FilesToClean
+ { ftcGhcSession :: !(Set FilePath)
+ -- ^ Files that will be deleted at the end of runGhc(T)
+
+ , ftcCurrentModule :: !(Set FilePath)
+ -- ^ Files that will be deleted the next time
+ -- 'cleanCurrentModuleTempFiles' is called, or otherwise at the end of
+ -- the session.
+ }
+
+-- | Used when a temp file is created. This determines which component Set of
+-- FilesToClean will get the temp file
+data TempFileLifetime
+ = TFL_CurrentModule
+ -- ^ A file with lifetime TFL_CurrentModule will be cleaned up at the
+ -- end of upweep_mod
+ | TFL_GhcSession
+ -- ^ A file with lifetime TFL_GhcSession will be cleaned up at the end of
+ -- runGhc(T)
+ deriving (Show)
+
+
+-- | An empty FilesToClean
+emptyFilesToClean :: FilesToClean
+emptyFilesToClean = FilesToClean Set.empty Set.empty
+
+-- | Merge two FilesToClean
+mergeFilesToClean :: FilesToClean -> FilesToClean -> FilesToClean
+mergeFilesToClean x y = FilesToClean
+ { ftcGhcSession = Set.union (ftcGhcSession x) (ftcGhcSession y)
+ , ftcCurrentModule = Set.union (ftcCurrentModule x) (ftcCurrentModule y)
+ }
+
+-- | Initialise an empty TmpFs
+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
+ }
+
+-- | Initialise an empty TmpFs sharing unique numbers and per-process temporary
+-- directories with the given TmpFs
+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
+ }
+
+-- | Merge the first TmpFs into the second.
+--
+-- The first TmpFs is returned emptied.
+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)
+
+-- | Delete all files in @tmp_files_to_clean@.
+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
+
+-- | Delete all files in @tmp_files_to_clean@. That have lifetime
+-- TFL_CurrentModule.
+-- If a file must be cleaned eventually, but must survive a
+-- cleanCurrentModuleTempFiles, ensure it has lifetime TFL_GhcSession.
+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
+
+-- | Ensure that new_files are cleaned on the next call of
+-- 'cleanTempFiles' or 'cleanCurrentModuleTempFiles', depending on lifetime.
+-- If any of new_files are already tracked, they will have their lifetime
+-- updated.
+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
+
+-- | Update the lifetime of files already being tracked. If any files are
+-- not being tracked they will be discarded.
+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
+
+-- Return a unique numeric temp file suffix
+newTempSuffix :: TmpFs -> IO Int
+newTempSuffix tmpfs =
+ atomicModifyIORef' (tmp_next_suffix tmpfs) $ \n -> (n+1,n)
+
+-- Find a temporary name that doesn't already exist.
+newTempName :: Logger -> TmpFs -> DynFlags -> TempFileLifetime -> Suffix -> IO FilePath
+newTempName logger tmpfs dflags lifetime extn
+ = do d <- getTempDir logger tmpfs dflags
+ findTempName (d </> "ghc_") -- See Note [Deterministic base name]
+ 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 -- clean it up later
+ 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
+ -- see mkTempDir below; this is wrong: -> consIORef (tmp_dirs_to_clean tmpfs) 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 -- See Note [Deterministic base name]
+ let libname = prefix ++ show n
+ filename = dir </> "lib" ++ libname <.> extn
+ b <- doesFileExist filename
+ if b then findTempName dir prefix
+ else do -- clean it up later
+ addFilesToClean tmpfs lifetime [filename]
+ return (filename, dir, libname)
+
+
+-- Return our temporary directory within tmp_dir, creating one if we
+-- don't have one yet.
+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
+
+ -- 1. Speculatively create our new directory.
+ createDirectory our_dir
+
+ -- 2. Update the tmp_dirs_to_clean mapping unless an entry already exists
+ -- (i.e. unless another thread beat us to it).
+ 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)
+
+ -- 3. If there was an existing entry, return it and delete the
+ -- directory we created. Otherwise return the directory we created.
+ 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
+
+{- Note [Deterministic base name]
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The filename of temporary files, especially the basename of C files, can end
+up in the output in some form, e.g. as part of linker debug information. In the
+interest of bit-wise exactly reproducible compilation (#4012), the basename of
+the temporary file no longer contains random information (it used to contain
+the process id).
+
+This is ok, as the temporary directory used contains the pid (see getTempDir).
+-}
+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
+ -- Flat out refuse to delete files that are likely to be source input
+ -- files (is there a worse bug than having a compiler delete your source
+ -- files?)
+ --
+ -- Deleting source files is a sign of a bug elsewhere, so prominently flag
+ -- the condition.
+ 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)
+-- relies on Int == Int32 on Windows
+foreign import ccall unsafe "_getpid" getProcessID :: IO Int
+#else
+getProcessID :: IO Int
+getProcessID = System.Posix.Internals.c_getpid >>= return . fromIntegral
+#endif
+
+-- The following three functions are from the `temporary` package.
+
+-- | Create and use a temporary directory in the system standard temporary
+-- directory.
+--
+-- Behaves exactly the same as 'withTempDirectory', except that the parent
+-- temporary directory will be that returned by 'getTemporaryDirectory'.
+withSystemTempDirectory :: String -- ^ Directory name template. See 'openTempFile'.
+ -> (FilePath -> IO a) -- ^ Callback that can use the directory
+ -> IO a
+withSystemTempDirectory template action =
+ getTemporaryDirectory >>= \tmpDir -> withTempDirectory tmpDir template action
+
+
+-- | Create and use a temporary directory.
+--
+-- Creates a new temporary directory inside the given directory, making use
+-- of the template. The temp directory is deleted after use. For example:
+--
+-- > withTempDirectory "src" "sdist." $ \tmpDir -> do ...
+--
+-- The @tmpDir@ will be a new subdirectory of the given directory, e.g.
+-- @src/sdist.342@.
+withTempDirectory :: FilePath -- ^ Temp directory to create the directory in
+ -> String -- ^ Directory name template. See 'openTempFile'.
+ -> (FilePath -> IO a) -- ^ Callback that can use the directory
+ -> 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