mach

A work-in-progress implementation of make(1)

git clone https://git.8pit.net/mach.git

  1{-# LANGUAGE LambdaCase #-}
  2{-# LANGUAGE RecordWildCards #-}
  3
  4-- | This module performs Makefile macro expansion.
  5module Mach.Eval
  6  ( 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  )
 26where
 27
 28import Control.Applicative ((<|>))
 29import Control.Exception (throwIO)
 30import Control.Monad (foldM, unless)
 31import Data.Functor ((<&>))
 32import Data.List (elemIndices, isSuffixOf)
 33import qualified Data.Map as Map
 34import Data.Maybe (fromMaybe)
 35import Mach.Error (MakeErr (TargetErr), TargetError (MultipleDefines, NoTargetOrFile, UnexpectedPrereqs))
 36import Mach.Parser (parseMkFile)
 37import qualified Mach.Types as T
 38import Mach.Util (firstJustM, isSpecial, stripSuffix)
 39import System.Directory (doesPathExist)
 40
 41-- | Expanded target definition of a target rule or inference rule.
 42-- The same target definition may be used for multiple files. For
 43-- example, when a target rule is defined for multiple targets.
 44--
 45-- Refer to 'Target' for more information.
 46data TgtDef = TgtDef
 47  { -- | Prerequisites (expanded)
 48    getPreqs :: [String],
 49    -- | Commands
 50    getRawCmds :: [T.Token]
 51  }
 52  deriving (Show)
 53
 54-- | Expanded makefile definition.
 55data MkDef = MkDef
 56  { -- | 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 TgtDef
 66  }
 67  deriving (Show)
 68
 69getSpecialPreqs :: MkDef -> String -> Maybe [String]
 70getSpecialPreqs MkDef {targetDefs = targets} name =
 71  getPreqs <$> Map.lookup name targets
 72
 73-- | Return all suffixes (.SUFFIXES special target).
 74suffixes :: MkDef -> [String]
 75suffixes mkDef = fromMaybe [] (getSpecialPreqs mkDef ".SUFFIXES")
 76
 77phony :: MkDef -> [String]
 78phony mkDef = fromMaybe [] (getSpecialPreqs mkDef ".PHONY")
 79
 80-- | Returns all names of silent targets (.SILENT special target).
 81silent :: MkDef -> Maybe [String]
 82silent mkDef = getSpecialPreqs mkDef ".SILENT"
 83
 84-- | Returns all names of ignored targets (.IGNORE special target).
 85ignore :: MkDef -> Maybe [String]
 86ignore mkDef = getSpecialPreqs mkDef ".IGNORE"
 87
 88-- | Returns a target built from the default rule (if defined).
 89defaultTarget :: FilePath -> MkDef -> Either TargetError Target
 90defaultTarget name MkDef {targetDefs = targets} = do
 91  target <- case Map.lookup ".DEFAULT" targets of
 92    Nothing -> Left $ NoTargetOrFile name
 93    Just t -> pure t
 94
 95  unless (null $ getPreqs target) $
 96    Left UnexpectedPrereqs
 97
 98  pure $ Target name (TgtDef [] $ getRawCmds target)
 99
100-- | 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 targets
104  where
105    lookupRule' :: Maybe TgtDef -> IO (Maybe Target)
106    lookupRule' Nothing = infRule
107    lookupRule' (Just t)
108      | null (getRawCmds t) =
109          infRule <&> \case
110            Nothing -> Just (Target name t)
111            -- mergeDef will never return Nothing as
112            -- inference rules do not have any prerqs.
113            Just x -> setDef x <$> mergeDef (getDef x) t
114      | otherwise = pure $ Just (Target name t)
115
116    infRule :: IO (Maybe Target)
117    infRule = suffixLookup mk name (suffixes mk)
118
119suffixLookup :: MkDef -> String -> [String] -> IO (Maybe Target)
120suffixLookup _ _ [] = pure Nothing
121suffixLookup mk@MkDef {singleSuffix = inf1, doubleSuffix = inf2} name (suffix : xs) = do
122  target <-
123    if '.' `elem` name
124      then firstJustM (lookupDouble name) inf2
125      else firstJustM (lookupSingle name) inf1
126
127  case target of
128    Nothing -> suffixLookup mk name xs
129    result -> pure result
130  where
131    maybeTarget :: TgtDef -> FilePath -> FilePath -> IO (Maybe Target)
132    maybeTarget tgtDef tgtName srcName = do
133      srcExists <- doesPathExist srcName
134      pure $
135        if srcExists
136          then Just $ Inferred tgtName srcName (TgtDef [srcName] $ getRawCmds tgtDef)
137          else Nothing
138
139    lookupSingle :: String -> (String, TgtDef) -> IO (Maybe Target)
140    lookupSingle tgtName (ruleName, tgtDef) =
141      if ruleName == suffix
142        then maybeTarget tgtDef tgtName (tgtName ++ suffix)
143        else pure Nothing
144
145    lookupDouble :: String -> (String, TgtDef) -> IO (Maybe Target)
146    lookupDouble tgtName (ruleName, tgtDef) =
147      let indices = elemIndices '.' ruleName
148          baseName = stripSuffix tgtName
149          (src, tgt) = splitAt (last indices) ruleName
150       in if suffix `isSuffixOf` name && suffix `isSuffixOf` ruleName
151            then maybeTarget tgtDef (baseName ++ tgt) (baseName ++ src)
152            else pure Nothing
153
154-- 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 both
157-- targets define commands.
158mergeDef :: TgtDef -> TgtDef -> Maybe TgtDef
159mergeDef (TgtDef p c) (TgtDef p' c')
160  | null c || null c' = Just $ TgtDef (p ++ p') (c ++ c')
161  | otherwise = Nothing
162
163mergeDefs :: Map.Map String TgtDef -> Map.Map String TgtDef -> Maybe (Map.Map String TgtDef)
164mergeDefs old new =
165  flip Map.union old
166    <$> Map.traverseWithKey
167      ( \k v -> case Map.lookup k old of
168          Just v' -> mergeDef v' v
169          Nothing -> Just v
170      )
171      new
172
173------------------------------------------------------------------------
174
175-- | A 'TgtDef' initialized for a specific target. The target
176-- can either be build from a target rule or an inference rule.
177data Target
178  = -- | A target build from a target rule
179    Target
180      -- | The target name
181      FilePath
182      -- | The target definition
183      TgtDef
184  | -- | A target build from an inference rule
185    Inferred
186      -- | The target name
187      FilePath
188      -- | The source file name (`$<`)
189      FilePath
190      -- | The target definition
191      TgtDef
192  deriving (Show)
193
194-- | Obtain the target name.
195getName :: Target -> String
196getName (Target name _) = name
197getName (Inferred name _ _) = name
198
199-- | Obtain the target definition, see 'TgtDef'.
200getDef :: Target -> TgtDef
201getDef (Target _ def) = def
202getDef (Inferred _ _ def) = def
203
204-- | Create a new 'Target' where the encapsulated 'TgtDef' is changed.
205setDef :: Target -> TgtDef -> Target
206setDef (Target name _) newDef = Target name newDef
207setDef (Inferred name src _) newDef = Inferred name src newDef
208
209------------------------------------------------------------------------
210
211-- | A command consisting of a list of prefixes and the shell command.
212data Cmd = Cmd
213  { cmdIgnore :: Bool,
214    cmdSilent :: Bool,
215    cmdExec :: Bool,
216    cmdShell :: String
217  }
218
219instance Show Cmd where
220  show cmd = cmdShell cmd
221
222mkCmd :: String -> Cmd
223mkCmd str = mkCmd' $ Cmd False False False str
224  where
225    mkCmd' :: Cmd -> Cmd
226    mkCmd' Cmd {cmdShell = '-' : xs, ..} =
227      mkCmd' $ Cmd True cmdSilent cmdExec xs
228    mkCmd' Cmd {cmdShell = '@' : xs, ..} =
229      mkCmd' $ Cmd cmdIgnore True cmdExec xs
230    mkCmd' Cmd {cmdShell = '+' : xs, ..} =
231      mkCmd' $ Cmd cmdIgnore cmdSilent True xs
232    mkCmd' cmd = cmd
233
234-- | Retrieve expanded commands for a 'Target'.
235getCmds :: MkDef -> Target -> [Cmd]
236getCmds MkDef {assigns = env} target =
237  (mkCmd . (expand $ Map.union internalMacros env)) <$> getRawCmds targetDef
238  where
239    targetDef :: TgtDef
240    targetDef = getDef target
241
242    internalMacros :: Env
243    internalMacros =
244      Map.fromList
245        [ ("^", T.AssignI $ unwords (getPreqs targetDef)),
246          ("@", T.AssignI $ getName target),
247          ( "<",
248            T.AssignI $ case target of
249              Target _ _ -> ""
250              Inferred _ src _ -> src
251          ),
252          ("*", T.AssignI $ stripSuffix (getName target))
253        ]
254
255------------------------------------------------------------------------
256
257-- | Makefile environment consisting of macro definitions.
258type Env = Map.Map String T.MacroAssign
259
260lookupAssign :: Env -> String -> Maybe String
261lookupAssign env name = Map.lookup name env >>= lookupAssign'
262  where
263    lookupAssign' :: T.MacroAssign -> Maybe String
264    lookupAssign' (T.AssignI str) = Just str
265    lookupAssign' (T.AssignD tok) = Just $ expand env tok
266
267-- Expand a given macro in the context of a given environment.
268expand :: Env -> T.Token -> String
269expand _ (T.Lit t) = t
270expand env (T.Exp t) = fromMaybe "" (lookupAssign env (expand env t))
271expand env (T.Seq s) = foldr (\x acc -> expand env x ++ acc) "" s
272expand env (T.ExpSub t s1 s2) =
273  unwords $
274    map
275      (subWord s1 s2)
276      (words $ expand env t)
277  where
278    subWord :: String -> String -> String -> String
279    subWord s r w
280      | s `isSuffixOf` w = take (length w - length s) w ++ r
281      | otherwise = w
282
283------------------------------------------------------------------------
284
285evalAssign :: Env -> T.Assign -> (String, T.MacroAssign)
286evalAssign env (T.Assign name' ty val) =
287  let name = expand env name'
288   in case ty of
289        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  where
296    appendAssign :: T.MacroAssign -> T.MacroAssign
297    appendAssign (T.AssignI str) = T.AssignI $ str ++ " " ++ expand env val
298    appendAssign (T.AssignD tok) = T.AssignD $ T.Seq [tok, T.Lit " ", val]
299
300evalInclude :: MkDef -> [T.Token] -> IO MkDef
301evalInclude def@MkDef {assigns = env} elems =
302  foldM
303    ( \mkDef path -> do
304        mk <- parseMkFile path
305        eval' mkDef mk
306    )
307    def
308    $ map (expand env) elems
309
310evalTgtRule :: Env -> T.TgtRule -> Map.Map String TgtDef
311evalTgtRule env (T.TgtRule tgts preqs cmds) =
312  let def = TgtDef (exLst preqs) cmds
313   in Map.fromList $ map (\tgt -> (tgt, def)) (exLst tgts)
314  where
315    exLst = concatMap (words . expand env)
316
317eval' :: MkDef -> T.MkFile -> IO MkDef
318eval' def [] = pure def
319eval' (MkDef env fstTgt inf1 inf2 targets) ((T.MkAssign assign) : xs) =
320  let (key, val) = evalAssign env assign
321      newEnviron = Map.insert key val env
322   in eval' (MkDef newEnviron fstTgt inf1 inf2 targets) xs
323eval' def ((T.MkInclude elems) : xs) =
324  evalInclude def elems >>= flip eval' xs
325eval' (MkDef env fstTgt inf1 inf2 targets) ((T.MkInfRule (T.InfRule target cmds)) : xs) =
326  let tdef = TgtDef [] cmds
327      (inf1', inf2') =
328        case length $ elemIndices '.' target of
329          1 -> ((target, tdef) : inf1, inf2)
330          2 -> (inf1, (target, tdef) : inf2)
331          _ -> error "unreachable"
332   in eval' (MkDef env fstTgt inf1' inf2' targets) xs
333eval' (MkDef env fstTgt inf1 inf2 targets) ((T.MkTgtRule rule) : xs) =
334  let newTgtDefs = evalTgtRule env rule
335      newTargets = Map.keys newTgtDefs
336      initTgtDef =
337        if isSpecial (head newTargets)
338          then Nothing
339          else Just (head newTargets)
340   in case mergeDefs targets newTgtDefs of
341        Nothing -> throwIO $ TargetErr MultipleDefines
342        Just nt -> eval' (MkDef env (fstTgt <|> initTgtDef) inf1 inf2 nt) xs
343
344eval :: T.MkFile -> IO MkDef
345eval = eval' (MkDef Map.empty Nothing [] [] Map.empty)