1{-# LANGUAGE LambdaCase #-}2{-# LANGUAGE RecordWildCards #-}34-- | This module performs Makefile macro expansion.5module Mach.Eval6 ( TgtDef,7 MkDef,8 getPreqs,9 Cmd,10 cmdIgnore,11 cmdSilent,12 cmdExec,13 cmdShell,14 getCmds,15 phony,16 silent,17 ignore,18 defaultTarget,19 firstTarget,20 lookupRule,21 eval,22 Target,23 getName,24 getDef,25 )26where2728import Control.Applicative ((<|>))29import Control.Exception (throwIO)30import Control.Monad (foldM, unless)31import Data.Functor ((<&>))32import Data.List (elemIndices, isSuffixOf)33import qualified Data.Map as Map34import Data.Maybe (fromMaybe)35import Mach.Error (MakeErr (TargetErr), TargetError (MultipleDefines, NoTargetOrFile, UnexpectedPrereqs))36import Mach.Parser (parseMkFile)37import qualified Mach.Types as T38import Mach.Util (firstJustM, isSpecial, stripSuffix)39import System.Directory (doesPathExist)4041-- | Expanded target definition of a target rule or inference rule.42-- The same target definition may be used for multiple files. For43-- example, when a target rule is defined for multiple targets.44--45-- Refer to 'Target' for more information.46data TgtDef = TgtDef47 { -- | Prerequisites (expanded)48 getPreqs :: [String],49 -- | Commands50 getRawCmds :: [T.Token]51 }52 deriving (Show)5354-- | Expanded makefile definition.55data MkDef = MkDef56 { -- | Macros defined in this Makefile.57 assigns :: Env,58 -- | First "normal" target defined in the Makefile.59 firstTarget :: Maybe String,60 -- | Single suffix inference rules.61 singleSuffix :: [(String, TgtDef)],62 -- | Double suffix inference rules.63 doubleSuffix :: [(String, TgtDef)],64 -- | TgtDefs defined in this Makefile.65 targetDefs :: Map.Map String TgtDef66 }67 deriving (Show)6869getSpecialPreqs :: MkDef -> String -> Maybe [String]70getSpecialPreqs MkDef {targetDefs = targets} name =71 getPreqs <$> Map.lookup name targets7273-- | Return all suffixes (.SUFFIXES special target).74suffixes :: MkDef -> [String]75suffixes mkDef = fromMaybe [] (getSpecialPreqs mkDef ".SUFFIXES")7677phony :: MkDef -> [String]78phony mkDef = fromMaybe [] (getSpecialPreqs mkDef ".PHONY")7980-- | Returns all names of silent targets (.SILENT special target).81silent :: MkDef -> Maybe [String]82silent mkDef = getSpecialPreqs mkDef ".SILENT"8384-- | Returns all names of ignored targets (.IGNORE special target).85ignore :: MkDef -> Maybe [String]86ignore mkDef = getSpecialPreqs mkDef ".IGNORE"8788-- | Returns a target built from the default rule (if defined).89defaultTarget :: FilePath -> MkDef -> Either TargetError Target90defaultTarget name MkDef {targetDefs = targets} = do91 target <- case Map.lookup ".DEFAULT" targets of92 Nothing -> Left $ NoTargetOrFile name93 Just t -> pure t9495 unless (null $ getPreqs target) $96 Left UnexpectedPrereqs9798 pure $ Target name (TgtDef [] $ getRawCmds target)99100-- | Lookup a target definition, the definition may be inferred.101lookupRule :: MkDef -> String -> IO (Maybe Target)102lookupRule mk@MkDef {targetDefs = targets} name =103 lookupRule' $ Map.lookup name targets104 where105 lookupRule' :: Maybe TgtDef -> IO (Maybe Target)106 lookupRule' Nothing = infRule107 lookupRule' (Just t)108 | null (getRawCmds t) =109 infRule <&> \case110 Nothing -> Just (Target name t)111 -- mergeDef will never return Nothing as112 -- inference rules do not have any prerqs.113 Just x -> setDef x <$> mergeDef (getDef x) t114 | otherwise = pure $ Just (Target name t)115116 infRule :: IO (Maybe Target)117 infRule = suffixLookup mk name (suffixes mk)118119suffixLookup :: MkDef -> String -> [String] -> IO (Maybe Target)120suffixLookup _ _ [] = pure Nothing121suffixLookup mk@MkDef {singleSuffix = inf1, doubleSuffix = inf2} name (suffix : xs) = do122 target <-123 if '.' `elem` name124 then firstJustM (lookupDouble name) inf2125 else firstJustM (lookupSingle name) inf1126127 case target of128 Nothing -> suffixLookup mk name xs129 result -> pure result130 where131 maybeTarget :: TgtDef -> FilePath -> FilePath -> IO (Maybe Target)132 maybeTarget tgtDef tgtName srcName = do133 srcExists <- doesPathExist srcName134 pure $135 if srcExists136 then Just $ Inferred tgtName srcName (TgtDef [srcName] $ getRawCmds tgtDef)137 else Nothing138139 lookupSingle :: String -> (String, TgtDef) -> IO (Maybe Target)140 lookupSingle tgtName (ruleName, tgtDef) =141 if ruleName == suffix142 then maybeTarget tgtDef tgtName (tgtName ++ suffix)143 else pure Nothing144145 lookupDouble :: String -> (String, TgtDef) -> IO (Maybe Target)146 lookupDouble tgtName (ruleName, tgtDef) =147 let indices = elemIndices '.' ruleName148 baseName = stripSuffix tgtName149 (src, tgt) = splitAt (last indices) ruleName150 in if suffix `isSuffixOf` name && suffix `isSuffixOf` ruleName151 then maybeTarget tgtDef (baseName ++ tgt) (baseName ++ src)152 else pure Nothing153154-- A target that has prerequisites, but does not have any commands,155-- can be used to add to the prerequisite list for that target.156-- Returns nothing if the targets cannot be merged, i.e. if both157-- targets define commands.158mergeDef :: TgtDef -> TgtDef -> Maybe TgtDef159mergeDef (TgtDef p c) (TgtDef p' c')160 | null c || null c' = Just $ TgtDef (p ++ p') (c ++ c')161 | otherwise = Nothing162163mergeDefs :: Map.Map String TgtDef -> Map.Map String TgtDef -> Maybe (Map.Map String TgtDef)164mergeDefs old new =165 flip Map.union old166 <$> Map.traverseWithKey167 ( \k v -> case Map.lookup k old of168 Just v' -> mergeDef v' v169 Nothing -> Just v170 )171 new172173------------------------------------------------------------------------174175-- | A 'TgtDef' initialized for a specific target. The target176-- can either be build from a target rule or an inference rule.177data Target178 = -- | A target build from a target rule179 Target180 -- | The target name181 FilePath182 -- | The target definition183 TgtDef184 | -- | A target build from an inference rule185 Inferred186 -- | The target name187 FilePath188 -- | The source file name (`$<`)189 FilePath190 -- | The target definition191 TgtDef192 deriving (Show)193194-- | Obtain the target name.195getName :: Target -> String196getName (Target name _) = name197getName (Inferred name _ _) = name198199-- | Obtain the target definition, see 'TgtDef'.200getDef :: Target -> TgtDef201getDef (Target _ def) = def202getDef (Inferred _ _ def) = def203204-- | Create a new 'Target' where the encapsulated 'TgtDef' is changed.205setDef :: Target -> TgtDef -> Target206setDef (Target name _) newDef = Target name newDef207setDef (Inferred name src _) newDef = Inferred name src newDef208209------------------------------------------------------------------------210211-- | A command consisting of a list of prefixes and the shell command.212data Cmd = Cmd213 { cmdIgnore :: Bool,214 cmdSilent :: Bool,215 cmdExec :: Bool,216 cmdShell :: String217 }218219instance Show Cmd where220 show cmd = cmdShell cmd221222mkCmd :: String -> Cmd223mkCmd str = mkCmd' $ Cmd False False False str224 where225 mkCmd' :: Cmd -> Cmd226 mkCmd' Cmd {cmdShell = '-' : xs, ..} =227 mkCmd' $ Cmd True cmdSilent cmdExec xs228 mkCmd' Cmd {cmdShell = '@' : xs, ..} =229 mkCmd' $ Cmd cmdIgnore True cmdExec xs230 mkCmd' Cmd {cmdShell = '+' : xs, ..} =231 mkCmd' $ Cmd cmdIgnore cmdSilent True xs232 mkCmd' cmd = cmd233234-- | Retrieve expanded commands for a 'Target'.235getCmds :: MkDef -> Target -> [Cmd]236getCmds MkDef {assigns = env} target =237 (mkCmd . (expand $ Map.union internalMacros env)) <$> getRawCmds targetDef238 where239 targetDef :: TgtDef240 targetDef = getDef target241242 internalMacros :: Env243 internalMacros =244 Map.fromList245 [ ("^", T.AssignI $ unwords (getPreqs targetDef)),246 ("@", T.AssignI $ getName target),247 ( "<",248 T.AssignI $ case target of249 Target _ _ -> ""250 Inferred _ src _ -> src251 ),252 ("*", T.AssignI $ stripSuffix (getName target))253 ]254255------------------------------------------------------------------------256257-- | Makefile environment consisting of macro definitions.258type Env = Map.Map String T.MacroAssign259260lookupAssign :: Env -> String -> Maybe String261lookupAssign env name = Map.lookup name env >>= lookupAssign'262 where263 lookupAssign' :: T.MacroAssign -> Maybe String264 lookupAssign' (T.AssignI str) = Just str265 lookupAssign' (T.AssignD tok) = Just $ expand env tok266267-- Expand a given macro in the context of a given environment.268expand :: Env -> T.Token -> String269expand _ (T.Lit t) = t270expand env (T.Exp t) = fromMaybe "" (lookupAssign env (expand env t))271expand env (T.Seq s) = foldr (\x acc -> expand env x ++ acc) "" s272expand env (T.ExpSub t s1 s2) =273 unwords $274 map275 (subWord s1 s2)276 (words $ expand env t)277 where278 subWord :: String -> String -> String -> String279 subWord s r w280 | s `isSuffixOf` w = take (length w - length s) w ++ r281 | otherwise = w282283------------------------------------------------------------------------284285evalAssign :: Env -> T.Assign -> (String, T.MacroAssign)286evalAssign env (T.Assign name' ty val) =287 let name = expand env name'288 in case ty of289 T.Delayed -> (name, T.AssignD val)290 T.Immediate -> (name, T.AssignI $ expand env val)291 T.StrictDelay -> error "unsupported"292 T.System -> error "unsupported"293 T.Cond -> (name, fromMaybe (T.AssignD val) $ Map.lookup name env)294 T.Append -> (name, maybe (T.AssignD val) appendAssign $ Map.lookup name env)295 where296 appendAssign :: T.MacroAssign -> T.MacroAssign297 appendAssign (T.AssignI str) = T.AssignI $ str ++ " " ++ expand env val298 appendAssign (T.AssignD tok) = T.AssignD $ T.Seq [tok, T.Lit " ", val]299300evalInclude :: MkDef -> [T.Token] -> IO MkDef301evalInclude def@MkDef {assigns = env} elems =302 foldM303 ( \mkDef path -> do304 mk <- parseMkFile path305 eval' mkDef mk306 )307 def308 $ map (expand env) elems309310evalTgtRule :: Env -> T.TgtRule -> Map.Map String TgtDef311evalTgtRule env (T.TgtRule tgts preqs cmds) =312 let def = TgtDef (exLst preqs) cmds313 in Map.fromList $ map (\tgt -> (tgt, def)) (exLst tgts)314 where315 exLst = concatMap (words . expand env)316317eval' :: MkDef -> T.MkFile -> IO MkDef318eval' def [] = pure def319eval' (MkDef env fstTgt inf1 inf2 targets) ((T.MkAssign assign) : xs) =320 let (key, val) = evalAssign env assign321 newEnviron = Map.insert key val env322 in eval' (MkDef newEnviron fstTgt inf1 inf2 targets) xs323eval' def ((T.MkInclude elems) : xs) =324 evalInclude def elems >>= flip eval' xs325eval' (MkDef env fstTgt inf1 inf2 targets) ((T.MkInfRule (T.InfRule target cmds)) : xs) =326 let tdef = TgtDef [] cmds327 (inf1', inf2') =328 case length $ elemIndices '.' target of329 1 -> ((target, tdef) : inf1, inf2)330 2 -> (inf1, (target, tdef) : inf2)331 _ -> error "unreachable"332 in eval' (MkDef env fstTgt inf1' inf2' targets) xs333eval' (MkDef env fstTgt inf1 inf2 targets) ((T.MkTgtRule rule) : xs) =334 let newTgtDefs = evalTgtRule env rule335 newTargets = Map.keys newTgtDefs336 initTgtDef =337 if isSpecial (head newTargets)338 then Nothing339 else Just (head newTargets)340 in case mergeDefs targets newTgtDefs of341 Nothing -> throwIO $ TargetErr MultipleDefines342 Just nt -> eval' (MkDef env (fstTgt <|> initTgtDef) inf1 inf2 nt) xs343344eval :: T.MkFile -> IO MkDef345eval = eval' (MkDef Map.empty Nothing [] [] Map.empty)