{-# LANGUAGE DeriveDataTypeable #-} module Development.Cake3.Types where import Control.Applicative import Control.Monad (when) import Control.Monad.Writer (MonadWriter, WriterT(..), execWriterT, execWriter, tell) import Data.Maybe import Data.Monoid import Data.Data import Data.Typeable import Data.Foldable (Foldable(..), foldl', forM_) import qualified Data.List as L import Data.List hiding(foldr, foldl') import qualified Data.Map as M import Data.Map (Map) import qualified Data.Set as S import Data.Set (Set) import qualified System.FilePath as F import System.FilePath.Wrapper -- | The representation of Makefile variable. data Variable = Variable { vname :: String -- ^ The name of a variable , vval :: Maybe String -- ^ Nothing means that variable is defined elsewhere (eg. borrowed from the -- environment) } deriving(Show, Eq, Ord, Data, Typeable) -- | The representation a tool used by the Makefile's recipe. Typical example -- are 'gcc' or 'bison' data Tool = Tool { tname :: String -- ^ Name of tool. } deriving(Show, Eq, Ord, Data, Typeable) -- | Command represents OS command line and consists of a list of fragments. -- Each fragment is either text (may contain spaces) or FilePath (spaces should -- be escaped) type Command = [CommandPiece] data CommandPiece = CmdStr String | CmdFile File deriving (Show, Eq, Ord, Data, Typeable) return_text x = return [CmdStr x] return_file f = return [CmdFile f] data Flag = Phony | Intermediate deriving(Show,Eq,Ord, Data, Typeable) -- | Recipe answers to the question 'How to build the targets'. Internally, it -- contains sets of targets and prerequisites, as well as shell commands -- required to build former from latter data Recipe = Recipe { rtgt :: Set File -- ^ Targets , rsrc :: Set File -- ^ Prerequisites , rcmd :: [Command] -- ^ A list of shell commands , rvars :: Set Variable -- ^ A set of variables employed in the recipe. The target Makefile should -- notice changes in those variables and rebuild the targets , rtools :: Set Tool -- ^ A set of tools employed in the recipe. Make , rloc :: String -- ^ Location (probably, doesn't function) , rflags :: Set Flag -- ^ Set of flags (Makefile-specific) } deriving(Show, Eq, Ord, Data, Typeable) emptyRecipe :: String -> Recipe emptyRecipe loc = Recipe mempty mempty mempty mempty mempty loc mempty addPrerequisites :: Set File -> Recipe -> Recipe addPrerequisites p r = r { rsrc = p`mappend`(rsrc r)} addPrerequisite :: File -> Recipe -> Recipe addPrerequisite f = addPrerequisites (S.singleton f) type Target = Set File groupSet :: (Ord k, Ord x, Foldable t) => (x -> Set k) -> t x -> Map k (Set x) groupSet keys s = foldl' f' mempty s where f' m x = foldl' ins m (keys x) where ins m k = M.insertWith mappend k (S.singleton x) m groupRecipes :: (Foldable t) => t Recipe -> Map File (Set Recipe) groupRecipes = groupSet rtgt flattern :: [Set x] -> [x] flattern = concat . map S.toList applyPlacement' :: (Eq x) => [File] -> Map File x -> [x] applyPlacement' pl m = let placed = nub $ catMaybes $ L.map (\k -> M.lookup k m) pl all = L.map snd $ M.toList m in placed ++ (all \\ placed) filterRecipesByTools :: (Foldable t) => [Tool] -> t Recipe -> Set Recipe filterRecipesByTools ts rs = foldMap mp rs where mp r = (\match -> if match then S.singleton r else S.empty) $ or $ map (\t -> S.member t (rtools r)) ts filterRecipesByTargets :: (Foldable t, Foldable t2) => t2 File -> t Recipe -> Set Recipe filterRecipesByTargets ts rs = foldMap mp rs where mp r = (\(Any match) -> if match then S.singleton r else S.empty) $ foldMap (\t -> Any $ S.member t (rtgt r)) ts filterRecipesByToolsDeep :: [Tool] -> Set Recipe -> Set Recipe filterRecipesByToolsDeep ts rs = fdeep (queryPrereq ry) rn ry where ry = filterRecipesByTools ts rs rn = rs `S.difference` ry fdeep ts rn ry = let ry' = filterRecipesByTargets ts rn in if not $ S.null ry' then fdeep (queryPrereq ry') (rn `S.difference` ry') (ry `S.union` ry') else ry applyPlacement :: (Foldable t) => [File] -> t Recipe -> [Recipe] applyPlacement pl rs = flattern $ applyPlacement' pl (groupRecipes rs) transformRecipes :: (Applicative m) => (Recipe -> m (Set Recipe)) -> Set Recipe -> m (Set Recipe) transformRecipes f m = S.foldl' f' (pure mempty) m where f' a r = mappend <$> (f r) <*> a transformRecipesM_ :: (Monad m, Foldable t) => (Recipe -> m ()) -> t Recipe -> m () transformRecipesM_ f rs = foldl' (\a r -> a >> f r) (return mempty) rs queryVariables :: (Foldable t) => t Recipe -> Set Variable queryVariables rs = foldl' (\a r -> a`mappend`(rvars r)) mempty rs queryVariablesE :: (Foldable t) => t Recipe -> Either String (Set Variable) queryVariablesE rs = check where vs = queryVariables rs bads = M.filter (\s -> (S.size s) /= 1) (groupSet (\v -> S.singleton (vname v)) vs) check | (M.size bads) > 0 = Left "Some variables share same name" | otherwise = Right vs queryTargets :: (Foldable t) => t Recipe -> Set File queryTargets rs = foldl' (\a r -> a`mappend`(rtgt r)) mempty rs queryPrereq :: (Foldable t) => t Recipe -> Set File queryPrereq rs = foldl' (\a r -> a`mappend`(rsrc r)) mempty rs var :: String -> Maybe String -> Variable var n v = Variable n v intermediateFiles :: (Foldable t) => t Recipe -> Set File intermediateFiles rs = execWriter $ do forM_ rs $ \r -> do when (not $ Phony `S.member` (rflags r)) $ do tell (rtgt r) tool :: String -> Tool tool = Tool -- | Define the Makefile-level variable. Rules, referring to a variable, -- 'notice' it's changes. makevar :: String -- ^ Variable name -> String -- ^ Default value -> Variable makevar n v = var n (Just v) -- | Declare the variable defined elsewhere. Typycally, environment variables -- may be decalred with this functions. Variables are tracked by the cake3. -- Rules, referring to a variable, 'notice' it's changes. extvar :: String -> Variable extvar n = var n Nothing -- | Reref to special variable @$(MAKE)@ make = extvar "MAKE" data ModuleLocation = ModuleLocation { top2mod :: FilePath -- ^ Relative path from top-level dir to the module , mod2top :: FilePath -- ^ Relative path from module to the top-level dir } deriving (Data, Typeable, Show, Eq, Ord) toplevelModule = ModuleLocation "." "." -- | Simple wrapper for FilePath. The first type argument is a Hint, containing -- the path to the current module type File = FileT ModuleLocation FilePath -- | Converts string representation of Path into type-safe File. Internally, -- files are stored in a form of offset from module root directory, plus the -- path from top-level dir to module root and back (see @ModuleLocation@) -- -- TODO: rename to mkFile file' :: ModuleLocation -> FilePath -> File file' pl f = FileT pl f -- | Path to the module (a directory), which have originally declared the @file@ fileModule :: File -> File fileModule file@(FileT ml _) = file' ml "." -- | Adds './' before the path, marking it as relative dottify :: FilePath -> FilePath dottify = addDot . F.normalise where addDot "." = "." addDot p@('.':'.':_) = p addDot ('/':_) = error "dottify: error, trying to cast absolute path to relative" addDot p = "."p -- | Returns the path to the file, relative to the top-level directory (the -- place, where the target Makefile is located) topRel :: File -> FilePath topRel (FileT (ModuleLocation t2m m2t) path) = dottify $ t2m path -- | Converts path @x@ to the back-path, consisting of '..' directories wayback :: FilePath -> FilePath wayback x = F.joinPath $ map (const "..") $ filter (/= ".") $ F.splitDirectories $ F.takeDirectory x -- | Returns path from file @s@ to @t@, via the top-level directory route :: File -> File -> FilePath route s@(FileT myloc mypath) t@(FileT hisloc hispath) | myloc == hisloc = dottify $ (wayback mypath) hispath | otherwise = dottify $ (wayback mypath) (mod2top myloc) (top2mod hisloc) hispath -- | Convert File back to FilePath with escaped spaces escapeFile :: File -> FilePath escapeFile f = escapeFile' (topRel f) where escapeFile' [] = [] escapeFile' (' ':xs) = "\\ " ++ escapeFile' xs escapeFile' (x:xs) = (x:(escapeFile' xs))