main
main.hs
1module Main where
2import Char (isSpace)
3import Directory (doesFileExist)
4import System (system, ExitCode(..))
5import List (find)
6import qualified Data.Map as Map
7
8-- IF YOU USE GHC COMMENT THE ABOVE IMPORTS AND UNCOMMENT THESE
9-- i dont reccomend to use GHC as it is not bootstrappable which
10-- could lead to a trusting-trust attack
11-- import Data.Char (isSpace)
12-- import System.Directory (doesFileExist)
13-- import System.Process (system)
14-- import System.Exit (ExitCode(..))
15-- import Data.List (find)
16
17data TypedLine
18 = Blank
19 | Recipe String
20 | Statement String
21 | Assignment String
22 | Unknown String
23 deriving (Show, Eq)
24
25-- thanks ricardo for suggesting this
26data Rule = Rule
27 { ruleName :: String
28 , rulePrereqs :: [String]
29 , ruleRecipes :: [String]
30 } deriving (Show, Eq)
31
32parseLine :: String -> TypedLine
33parseLine ('\t':rest) = Recipe rest
34parseLine line
35 | null trimmed = Blank
36 | elem ':' stripped = Statement stripped
37 | elem '=' stripped = Assignment stripped
38 | otherwise = Unknown stripped
39 where
40 stripComment line = takeWhile(/= '#') line
41 stripped = stripComment line
42 trimmed = dropWhile isSpace stripped
43
44isRecipe :: TypedLine -> Bool
45isRecipe (Recipe _) = True
46isRecipe _ = False
47
48recipeText :: TypedLine -> String
49recipeText (Recipe text) = text
50
51parseRules :: [TypedLine] -> Either String [Rule]
52parseRules [] = Right []
53parseRules (Unknown line : rest) = Left "unknown line type"
54parseRules (Recipe line : rest) = Left "recipe without preceding dependency declaration"
55parseRules (Blank : rest) = parseRules rest
56parseRules (Assignment _ : rest) = parseRules rest
57parseRules (Statement stmt : rest) = -- if you see stmt split into stmt and rest
58 -- split into 1) recipe lines immedtiately following the stmt
59 -- 2) everything else
60 let (recipeLines, remaining) = span isRecipe rest
61 -- create a rule with the statement and recipe lines
62 currentRule = mkRule stmt recipeLines
63 in case parseRules remaining of
64 Left message -> Left message
65 Right nextRules -> Right (currentRule : nextRules)
66
67-- we take a string and a list of typed lines and produce a rule
68mkRule :: String -> [TypedLine] -> Rule
69mkRule stmt recipeLines =
70 let (name, prereqs) = splitStmt stmt
71 recipes = map recipeText recipeLines
72 in Rule name prereqs recipes
73 where
74 splitStmt stmt = case break(== ':') stmt of
75 (lhs, ':':rhs) -> (lhs, words rhs)
76 _ -> error "statement has no colon"
77
78-- take a string (assignment) and a map of assignments and return new map of assignments
79parseAssign :: String -> Map.Map String String -> Map.Map String String
80parseAssign line macros =
81 let (name, val) = break(== '=') line
82 trimmedName = trim name
83 -- drop =
84 trimmedVal = trim (drop 1 val)
85 -- insert the pair into new map
86 in Map.insert trimmedName trimmedVal macros
87 where
88 -- trim both sides
89 trim = reverse . dropWhile isSpace . reverse . dropWhile isSpace
90
91
92collectAssigns :: [TypedLine] -> Map.Map String String
93collectAssigns [] = Map.empty
94-- if its an assignment parse it, add it to map, then keep iterating on remaning typed lines
95collectAssigns (Assignment text : rest) = parseAssign text (collectAssigns rest)
96-- if not assignment ignore
97collectAssigns (_ : rest) = collectAssigns rest
98
99expandMacro :: Map.Map String String -> String -> String
100expandMacro _ [] = []
101-- we found a macro
102expandMacro macros ('$':'(':rest) =
103 let (name, afterName) = break (== ')') rest
104 in case afterName of
105 ')':after ->
106 -- search for the macro
107 case Map.lookup name macros of
108 Just value ->
109 -- expand the macro's value and the remaining text
110 expandMacro macros value ++ expandMacro macros after
111 Nothing ->
112 -- replace with an empty string and expand the rest
113 expandMacro macros after
114 -- for a case where theres no closing )
115 _ -> "$(" ++ rest
116expandMacro macros (char:rest) = char : expandMacro macros rest
117
118expandLine :: Map.Map String String -> TypedLine -> TypedLine
119expandLine macros (Statement text) = Statement (expandMacro macros text)
120expandLine macros (Recipe text) = Recipe (expandMacro macros text)
121expandLine _ line = line
122
123findRule :: String -> [Rule] -> Maybe Rule
124findRule name rules = find (\rule -> ruleName rule == name) rules
125
126-- take a list of rules, a target name and return its recipe
127targetRecipes :: [Rule] -> String -> [String]
128targetRecipes rules name =
129 case findRule name rules of
130 Nothing -> []
131 Just rule -> ruleRecipes rule
132
133-- take a list of rules,
134-- a target name and do DFS, return list of rules in correct order
135orderTarget :: [Rule] -> String -> [Rule]
136orderTarget rules wanted =
137 case findRule wanted rules of
138 Nothing -> [Rule wanted [] []]
139 Just rule ->
140 let prereqs = rulePrereqs rule
141 -- run "orderTarget rules" on every prereq in prereqs
142 -- this returns a list of dependency lists so use concat
143 -- to just make it one list
144 prereqOrders = map (orderTarget rules) prereqs
145 in concat prereqOrders ++ [rule]
146 -- xxx handle cycles and duplicates
147
148-- print and run a recipe
149runRecipe :: String -> IO Bool
150runRecipe command = do
151 putStrLn command
152 exitCode <- system command
153 case exitCode of
154 ExitSuccess -> return True
155 ExitFailure _ -> return False
156
157-- this should prob be called isSource or smth as a leaf can have a recipe
158-- as long as it has no prereqs but whateva
159isLeaf :: Rule -> Bool
160isLeaf rule = null (rulePrereqs rule) && null (ruleRecipes rule)
161
162main :: IO ()
163main = do
164 mkf <- readFile "Makefile"
165 let rawLines = lines mkf
166 let parsedLines = map parseLine rawLines
167 let macros = collectAssigns parsedLines
168 let expandedLines = map (expandLine macros) parsedLines
169 case parseRules expandedLines of
170 Left message -> putStrLn ("parse error: " ++ message)
171 Right rules ->
172 case rules of
173 [] -> putStrLn "no targets"
174 firstRule: _ -> do
175 -- xxx cli option for requested target
176 let wanted = ruleName firstRule
177 let order = (orderTarget rules wanted)
178 let leafs = filter isLeaf order
179 let leafNames = map ruleName leafs
180 exists <- mapM doesFileExist leafNames
181 let recipes = concatMap ruleRecipes order
182 if and exists
183 then mapM_ runRecipe recipes
184 else putStrLn "a leaf node is missing"