{-# LANGUAGE Safe #-}
module Cli.CompileMetadata (
CategoryIdentifier(..),
CompileMetadata(..),
ObjectFile(..),
RecompileMetadata(..),
allowedExtraTypes,
createCachePath,
eraseCachedData,
findSourceFiles,
fixPath,
getCachedPath,
getCacheRelativePath,
getIncludePathsForDeps,
getNamespacesForDeps,
getObjectFilesForDeps,
getObjectFileResolver,
getRealPathsForDeps,
getRequiresFromDeps,
getSourceFilesForDeps,
isCategoryObjectFile,
isPathConfigured,
isPathUpToDate,
loadPrivateDeps,
loadPublicDeps,
loadMetadata,
mergeObjectFiles,
resolveCategoryDeps,
resolveObjectDeps,
sortCompiledFiles,
tryLoadRecompile,
writeCachedFile,
writeMetadata,
writeRecompile,
) where
import Control.Applicative ((<|>))
import Control.Monad (when)
import Data.List (nub,isSuffixOf)
import Data.Maybe (isJust)
import System.Directory
import System.Environment
import System.Exit (exitFailure)
import System.FilePath
import System.IO
import qualified Data.Map as Map
import qualified Data.Set as Set
import Cli.CompileOptions (CompileMode)
import CompilerCxx.Category (CxxOutput(..))
import Types.TypeCategory
import Types.TypeInstance
data CompileMetadata =
CompileMetadata {
cmPath :: String,
cmNamespace :: String,
cmPublicDeps :: [String],
cmPrivateDeps :: [String],
cmExtraRequires :: [CategoryIdentifier],
cmCategories :: [String],
cmSubdirs :: [String],
cmPublicFiles :: [String],
cmPrivateFiles :: [String],
cmTestFiles :: [String],
cmHxxFiles :: [String],
cmCxxFiles :: [String],
cmObjectFiles :: [ObjectFile]
}
deriving (Show,Read)
data ObjectFile =
CategoryObjectFile {
cofCategory :: CategoryIdentifier,
cofRequires :: [CategoryIdentifier],
cofFiles :: [String]
} |
OtherObjectFile {
oofFile :: String
}
deriving (Show,Read)
data CategoryIdentifier =
CategoryIdentifier {
ciPath :: String,
ciCategory :: String,
ciNamespace :: String
} |
UnresolvedCategory {
ucCategory :: String
}
deriving (Eq,Ord,Show,Read)
mergeObjectFiles :: ObjectFile -> ObjectFile -> ObjectFile
mergeObjectFiles (CategoryObjectFile c rs1 fs1) (CategoryObjectFile _ rs2 fs2) =
CategoryObjectFile c (rs1 ++ rs2) (fs1 ++ fs2)
mergeObjectFiles o _ = o
isCategoryObjectFile :: ObjectFile -> Bool
isCategoryObjectFile (CategoryObjectFile _ _ _) = True
isCategoryObjectFile (OtherObjectFile _) = False
data RecompileMetadata =
RecompileMetadata {
rmRoot :: String,
rmPath :: String,
rmPublicDeps :: [String],
rmPrivateDeps :: [String],
rmExtraFiles :: [String],
rmExtraPaths :: [String],
rmExtraRequires :: [String],
rmMode :: CompileMode,
rmOutputName :: String
}
deriving (Show,Read)
cachedDataPath = ".zeolite-cache"
recompileFilename = ".zeolite-module"
metadataFilename = "metadata.txt"
allowedExtraTypes = [".hpp",".cpp",".h",".cc",".a",".o"]
loadMetadata :: String -> IO CompileMetadata
loadMetadata p = do
let f = p </> cachedDataPath </> metadataFilename
isFile <- doesFileExist p
when isFile $ do
hPutStrLn stderr $ "Path \"" ++ p ++ "\" is not a directory."
exitFailure
isDir <- doesDirectoryExist p
when (not isDir) $ do
hPutStrLn stderr $ "Path \"" ++ p ++ "\" does not exist."
exitFailure
filePresent <- doesFileExist f
when (not filePresent) $ do
hPutStrLn stderr $ "Module \"" ++ p ++ "\" has not been compiled yet."
exitFailure
c <- readFile f
m <- check $ (reads c :: [(CompileMetadata,String)])
return m where
check [(cm,"")] = return cm
check [(cm,"\n")] = return cm
check _ = do
hPutStrLn stderr $ "Could not parse metadata from \"" ++ p ++ "\"; please recompile."
exitFailure
tryLoadMetadata :: String -> IO (Maybe CompileMetadata)
tryLoadMetadata p = tryLoadData $ (p </> cachedDataPath </> metadataFilename)
tryLoadRecompile :: String -> IO (Maybe RecompileMetadata)
tryLoadRecompile p = tryLoadData $ (p </> recompileFilename)
tryLoadData :: Read a => String -> IO (Maybe a)
tryLoadData f = do
filePresent <- doesFileExist f
if not filePresent
then return Nothing
else do
c <- readFile f
check (reads c) where
check [(cm,"")] = return (Just cm)
check [(cm,"\n")] = return (Just cm)
check _ = return Nothing
isPathUpToDate :: String -> IO Bool
isPathUpToDate p = do
m <- tryLoadMetadata p
case m of
Nothing -> return False
Just m'-> do
(fr,_) <- loadDepsCommon True (\m -> cmPublicDeps m ++ cmPrivateDeps m) [p]
return fr
isPathConfigured :: String -> IO Bool
isPathConfigured p = tryLoadRecompile p >>= return . isJust
writeMetadata :: String -> CompileMetadata -> IO ()
writeMetadata p m = do
p' <- canonicalizePath p
hPutStrLn stderr $ "Writing metadata for \"" ++ p' ++ "\"."
writeCachedFile p' "" metadataFilename (show m ++ "\n")
writeRecompile :: String -> RecompileMetadata -> IO ()
writeRecompile p m = do
p' <- canonicalizePath p
hPutStrLn stderr $ "Updating config for \"" ++ p' ++ "\"."
writeFile (p </> recompileFilename) (show m ++ "\n")
eraseCachedData :: String -> IO ()
eraseCachedData p = do
let d = p </> cachedDataPath
dirExists <- doesDirectoryExist d
when dirExists $ removeDirectoryRecursive d
createCachePath :: String -> IO ()
createCachePath p = do
let f = p </> cachedDataPath
exists <- doesDirectoryExist f
when (not exists) $ createDirectoryIfMissing False f
writeCachedFile :: String -> String -> String -> String -> IO ()
writeCachedFile p ns f c = do
createCachePath p
createDirectoryIfMissing False $ p </> cachedDataPath </> ns
writeFile (getCachedPath p ns f) c
getCachedPath :: String -> String -> String -> String
getCachedPath p ns f = fixPath $ p </> cachedDataPath </> ns </> f
getCacheRelativePath :: String -> String
getCacheRelativePath f = ".." </> f
findSourceFiles :: String -> String -> IO ([String],[String],[String])
findSourceFiles p0 p = do
let absolute = p0 </> p
isFile <- doesFileExist absolute
when isFile $ do
hPutStrLn stderr $ "Path \"" ++ absolute ++ "\" is not a directory."
exitFailure
isDir <- doesDirectoryExist absolute
when (not isDir) $ do
hPutStrLn stderr $ "Path \"" ++ absolute ++ "\" does not exist."
exitFailure
ds <- getDirectoryContents absolute >>= return . map (p </>)
let ps = filter (isSuffixOf ".0rp") ds
let xs = filter (isSuffixOf ".0rx") ds
let ts = filter (isSuffixOf ".0rt") ds
return (ps,xs,ts)
getRealPathsForDeps :: [CompileMetadata] -> [String]
getRealPathsForDeps = map cmPath
getSourceFilesForDeps :: [CompileMetadata] -> [String]
getSourceFilesForDeps = concat . map extract where
extract m = map (cmPath m </>) (cmPublicFiles m)
getRequiresFromDeps :: [CompileMetadata] -> [CategoryIdentifier]
getRequiresFromDeps = concat . map cmExtraRequires
getNamespacesForDeps :: [CompileMetadata] -> [String]
getNamespacesForDeps = filter (not . null) . map cmNamespace
getIncludePathsForDeps :: [CompileMetadata] -> [String]
getIncludePathsForDeps = concat . map cmSubdirs
getObjectFilesForDeps :: [CompileMetadata] -> [ObjectFile]
getObjectFilesForDeps = concat . map cmObjectFiles
loadPublicDeps :: [String] -> IO (Bool,[CompileMetadata])
loadPublicDeps = loadDepsCommon False cmPublicDeps
loadPrivateDeps :: [CompileMetadata] -> IO (Bool,[CompileMetadata])
loadPrivateDeps ms = do
(fr,new) <- loadDepsCommon False (\m -> cmPublicDeps m ++ cmPrivateDeps m) toFind
return (fr,ms ++ existing ++ new) where
paths = concat $ map (\m -> cmPublicDeps m ++ cmPrivateDeps m) ms
(existing,toFind) = foldl splitByExisting ([],[]) $ nub paths
byPath = Map.fromList $ map (\m -> (cmPath m,m)) ms
splitByExisting (es,fs) p =
case p `Map.lookup` byPath of
Just m -> (es ++ [m],fs)
Nothing -> (es,fs ++ [p])
loadDepsCommon :: Bool -> (CompileMetadata -> [String]) -> [String] -> IO (Bool,[CompileMetadata])
loadDepsCommon s f ps = fmap snd $ fixedPaths >>= collect (Set.empty,(True,[])) where
fixedPaths = sequence $ map canonicalizePath ps
collect xa@(pa,(fr,xs)) (p:ps)
| p `Set.member` pa = collect xa ps
| otherwise = do
when (not s) $ hPutStrLn stderr $ "Loading metadata for dependency \"" ++ p ++ "\"."
m <- loadMetadata p
fresh <- checkModuleFreshness p m
when (not s && not fresh) $
hPutStrLn stderr $ "Module \"" ++ p ++ "\" is out of date and should be recompiled."
collect (p `Set.insert` pa,(fresh && fr,xs ++ [m])) (ps ++ f m)
collect xa _ = return xa
fixPath :: String -> String
fixPath = foldl (</>) "" . process [] . map dropSlash . splitPath where
dropSlash "/" = "/"
dropSlash d
| isSuffixOf "/" d = reverse $ tail $ reverse d
| otherwise = d
process rs (".":ds) = process rs ds
process ("..":rs) ("..":ds) = process ("..":"..":rs) ds
process ("/":[]) ("..":ds) = process ("/":[]) ds
process (_:rs) ("..":ds) = process rs ds
process rs (d:ds) = process (d:rs) ds
process rs _ = reverse rs
sortCompiledFiles :: [String] -> ([String],[String],[String])
sortCompiledFiles = foldl split ([],[],[]) where
split fs@(hxx,cxx,os) f
| isSuffixOf ".hpp" f = (hxx++[f],cxx,os)
| isSuffixOf ".h" f = (hxx++[f],cxx,os)
| isSuffixOf ".cpp" f = (hxx,cxx++[f],os)
| isSuffixOf ".cc" f = (hxx,cxx++[f],os)
| isSuffixOf ".a" f = (hxx,cxx,os++[f])
| isSuffixOf ".o" f = (hxx,cxx,os++[f])
| otherwise = fs
checkModuleFreshness :: String -> CompileMetadata -> IO Bool
checkModuleFreshness p (CompileMetadata p2 _ is is2 _ _ _ ps xs ts hxx cxx _) = do
time <- getModificationTime $ getCachedPath p "" metadataFilename
(ps2,xs2,ts2) <- findSourceFiles p ""
let e1 = checkMissing ps ps2
let e2 = checkMissing xs xs2
let e3 = checkMissing ts ts2
rm <- check time (p </> recompileFilename)
f1 <- sequence $ map (\p2 -> check time $ getCachedPath p2 "" metadataFilename) $ is ++ is2
f2 <- sequence $ map (check time . (p2 </>)) $ ps ++ xs
f3 <- sequence $ map (check time . getCachedPath p2 "") $ hxx ++ cxx
let fresh = not $ any id $ [rm,e1,e2,e3] ++ f1 ++ f2 ++ f3
return fresh where
check time f = do
exists <- doesPathExist f
if not exists
then return True
else do
time2 <- getModificationTime f
return (time2 > time)
checkMissing s0 s1 = not $ null $ (Set.fromList s1) `Set.difference` (Set.fromList s0)
getObjectFileResolver :: [CategoryIdentifier] -> [ObjectFile] -> [Namespace] -> [CategoryName] -> [String]
getObjectFileResolver ce os ns ds = resolved ++ nonCategories where
categories = filter isCategoryObjectFile os
nonCategories = map oofFile $ filter (not . isCategoryObjectFile) os
categoryMap = Map.fromList $ map keyByCategory categories
keyByCategory o = ((ciCategory $ cofCategory o,ciNamespace $ cofCategory o),o)
objectMap = Map.fromList $ map keyBySpec categories
keyBySpec o = (cofCategory o,o)
directDeps = concat $ map (resolveDep . show) ds
directResolved = map cofCategory directDeps ++ ce
resolveDep d = unwrap $ foldl (<|>) Nothing allChecks <|> Just [] where
allChecks = map (\n -> (d,n) `Map.lookup` categoryMap >>= return . (:[])) (map show ns ++ [""])
unwrap (Just xs) = xs
unwrap _ = []
(_,_,resolved) = collectAll Set.empty Set.empty directResolved
collectAll ca fa [] = (ca,fa,[])
collectAll ca fa (c:cs)
| c `Set.member` ca = collectAll ca fa cs
| otherwise =
case c `Map.lookup` objectMap of
Nothing -> collectAll ca fa cs
Just (CategoryObjectFile _ ds fs) -> (ca',fa'',fs') where
(ca',fa',fs0) = collectAll (c `Set.insert` ca) fa (ds ++ cs)
fa'' = fa' `Set.union` (Set.fromList fs)
fs' = (filter (not . flip elem fa') fs) ++ fs0
resolveObjectDeps :: String -> [([String],CxxOutput)] -> [CompileMetadata] -> [ObjectFile]
resolveObjectDeps p os deps = resolvedCategories ++ nonCategories where
categories = filter (isJust . coCategory . snd) os
publicNamespaces = getNamespacesForDeps deps
nonCategories = map OtherObjectFile $ concat $ map fst $ filter (not . isJust . coCategory . snd) os
resolvedCategories = Map.elems $ Map.fromListWith mergeObjectFiles $ map resolveCategory categories
categoryMap = Map.fromList $ directCategories ++ depCategories
directCategories = map (keyByCategory . cxxToId) $ map snd categories
depCategories = map keyByCategory $ concat $ map categoriesToIds deps
categoriesToIds dep = map (\c -> CategoryIdentifier (cmPath dep) c (cmNamespace dep)) (cmCategories dep)
cxxToId (CxxOutput (Just c) _ ns _ _ _) = CategoryIdentifier p (show c) (show ns)
resolveCategory (fs,ca@(CxxOutput _ _ _ ns2 ds _)) =
(cxxToId ca,CategoryObjectFile (cxxToId ca) rs fs) where
rs = concat $ map (resolveDep categoryMap (map show ns2 ++ publicNamespaces) . show) ds
resolveCategoryDeps :: [String] -> [CompileMetadata] -> [CategoryIdentifier]
resolveCategoryDeps cs deps = resolvedCategories where
publicNamespaces = getNamespacesForDeps deps
resolvedCategories = concat $ map (resolveDep categoryMap publicNamespaces) cs
categoryMap = Map.fromList depCategories
depCategories = map (keyByCategory . cofCategory) $ filter isCategoryObjectFile $ concat $ map cmObjectFiles deps
keyByCategory :: CategoryIdentifier -> ((String,String),CategoryIdentifier)
keyByCategory c = ((ciCategory c,ciNamespace c),c)
resolveDep :: Map.Map (String,String) CategoryIdentifier -> [String] -> String -> [CategoryIdentifier]
resolveDep cm ns d = unwrap $ foldl (<|>) Nothing allChecks where
allChecks = map (\n -> (d,n) `Map.lookup` cm >>= return . (:[])) ns
unwrap (Just xs) = xs
unwrap _ = [UnresolvedCategory d]