3 # Generation of Makefiles for CBMC proofs.
5 # Copyright (C) 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
7 # Permission is hereby granted, free of charge, to any person obtaining a copy
8 # of this software and associated documentation files (the "Software"), to deal
9 # in the Software without restriction, including without limitation the rights
10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 # copies of the Software, and to permit persons to whom the Software is
12 # furnished to do so, subject to the following conditions:
14 # The above copyright notice and this permission notice shall be included in all
15 # copies or substantial portions of the Software.
17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
41 # ______________________________________________________________________________
42 # Compatibility between different python versions
43 # ``````````````````````````````````````````````````````````````````````````````
45 # Python 3 doesn't have basestring
51 # ast.Num was deprecated in python 3.8
52 _plat = platform.python_version().split(".")
53 if _plat[0] == "3" and int(_plat[1]) > 7:
54 ast_num = ast.Constant
57 # ______________________________________________________________________________
61 return textwrap.dedent("""\
62 This script generates Makefiles that can be used either on Windows (with
63 NMake) or Linux (with GNU Make). The Makefiles consist only of variable
64 definitions; they are intended to be used with a common Makefile that
65 defines the actual rules.
67 The Makefiles are generated from JSON specifications. These are simple
68 key-value dicts, but can contain variables and computation, as
69 well as comments (lines whose first character is "#"). If the
70 JSON file contains the following information:
73 # 'T was brillig, and the slithy toves
77 # Did gyre and gimble in the wabe;
79 "XYZZY": "__eval 5 if {QUUZ} < 5 else min({QUUX}, 60)"
82 then the resulting Makefile will look like
89 The language used for evaluation is highly restricted; arbitrary
90 python is not allowed. JSON values that are lists will be
91 joined with whitespace:
93 { "FOO": ["BAR", "BAZ", "QUX"] }
99 As a special case, if a key is equal to "DEF", "INC" (and maybe more,
100 read the code) the list of values is treated as a list of defines or
101 include paths. Thus, they have -D or /D, or -I or /I, prepended to them
102 as appropriate for the platform.
105 { "DEF": ["DEBUG", "FOO=BAR"] }
109 H_DEF = -DDEBUG -DFOO=BAR
111 Pathnames are written with a forward slash in the JSON file. In
112 each value, all slashes are replaced with backslashes if
113 generating Makefiles for Windows. If you wish to generate a
114 slash even on Windows, use two slashes---that will be changed
115 into a single forward slash on both Windows and Linux.
118 "INC": [ "my/cool/directory" ],
119 "DEF": [ "HALF=//2" ]
124 H_INC = /Imy\cool\directory
127 When invoked, this script walks the directory tree looking for files
128 called "Makefile.json". It reads that file and dumps a Makefile in that
129 same directory. We assume that this script lives in the same directory
130 as a Makefile called "Makefile.common", which contains the actual Make
131 rules. The final line of each of the generated Makefiles will be an
132 include statement, including Makefile.common.
135 def load_json_config_file(file):
136 with open(file) as handle:
137 lines = handle.read().splitlines()
138 no_comments = "\n".join([line for line in lines
139 if line and not line.lstrip().startswith("#")])
141 data = json.loads(no_comments,
142 object_pairs_hook=collections.OrderedDict)
143 except json.decoder.JSONDecodeError:
144 traceback.print_exc()
145 logging.error("parsing file %s", file)
150 def dump_makefile(dyr, system):
151 data = load_json_config_file(os.path.join(dyr, "Makefile.json"))
153 makefile = collections.OrderedDict()
155 # Makefile.common expects a variable called OBJS_EXCEPT_HARNESS to be
156 # set to a list of all the object files except the harness.
157 if "OBJS" not in data:
159 "Expected a list of object files in %s/Makefile.json" % dyr)
161 makefile["OBJS_EXCEPT_HARNESS"] = " ".join(
162 o for o in data["OBJS"] if not o.endswith("_harness.goto"))
164 so_far = collections.OrderedDict()
165 for name, value in data.items():
166 if isinstance(value, list):
169 new_value.append(compute(item, so_far, system, name, dyr, True))
170 makefile[name] = " ".join(new_value)
172 makefile[name] = compute(value, so_far, system, name, dyr)
174 if (("EXPECTED" not in makefile.keys()) or
175 str(makefile["EXPECTED"]).lower() == "true"):
176 makefile["EXPECTED"] = "SUCCESSFUL"
177 elif str(makefile["EXPECTED"]).lower() == "false":
178 makefile["EXPECTED"] = "FAILURE"
179 makefile = ["H_%s = %s" % (k, v) for k, v in makefile.items()]
181 # Deal with the case of a harness being nested several levels under
182 # the top-level proof directory, where the common Makefile lives
183 common_dir_path = "..%s" % _platform_choices[system]["path-sep"]
184 common_dir_path = common_dir_path * len(dyr.split(os.path.sep)[1:])
186 with open(os.path.join(dyr, "Makefile"), "w") as handle:
187 handle.write(("""{contents}
189 {include} {common_dir_path}Makefile.common""").format(
190 contents="\n".join(makefile),
191 include=_platform_choices[system]["makefile-inc"],
192 common_dir_path=common_dir_path))
195 def compute(value, so_far, system, key, harness, appending=False):
196 if not isinstance(value, (basestring, float, int)):
197 logging.error(wrap("""\
198 in file %s, the key '%s' has value '%s',
199 which is of the wrong type (%s)"""),
200 os.path.join(harness, "Makefile.json"), key,
201 str(value), str(type(value)))
206 var_subbed = value.format(**so_far)
207 except KeyError as e:
208 logging.error(wrap("""\
209 in file %s, the key '%s' has value '%s', which
210 contains the variable %s, but that variable has
211 not previously been set in the file"""),
212 os.path.join(harness, "Makefile.json"), key,
216 if var_subbed[:len("__eval")] != "__eval":
217 tmp = re.sub("//", "__DOUBLE_SLASH__", var_subbed)
218 tmp = re.sub("/", _platform_choices[system]["path-sep-re"], tmp)
219 evaluated = re.sub("__DOUBLE_SLASH__", "/", tmp)
221 to_eval = var_subbed[len("__eval"):].strip()
222 logging.debug("About to evaluate '%s'", to_eval)
223 evaluated = eval_expr(to_eval,
224 os.path.join(harness, "Makefile.json"),
228 final_value = "%s%s" % (_platform_choices[system]["define"],
231 final_value = "%s%s" % (_platform_choices[system]["include"],
234 final_value = evaluated
236 # Allow this value to be used for future variable substitution
239 so_far[key] = "%s %s" % (so_far[key], final_value)
241 so_far[key] = final_value
242 logging.debug("Appending final value '%s' to key '%s'",
245 so_far[key] = final_value
246 logging.info("Key '%s' set to final value '%s'", key, final_value)
251 def eval_expr(expr_string, harness, key, value):
253 Safe evaluation of purely arithmetic expressions
256 tree = ast.parse(expr_string, mode="eval").body
258 traceback.print_exc()
259 logging.error(wrap("""\
260 in file %s at key '%s', value '%s' contained the expression
261 '%s' which is an invalid expression"""), harness, key,
265 def eval_single_node(node):
267 if isinstance(node, ast_num):
269 # We're only doing IfExp, which is Python's ternary operator
270 # (i.e. it's an expression). NOT If, which is a statement.
271 if isinstance(node, ast.IfExp):
272 # Let's be strict and only allow actual booleans in the guard
273 guard = eval_single_node(node.test)
274 if guard is not True and guard is not False:
275 logging.error(wrap("""\
276 in file %s at key '%s', there was an invalid guard
277 for an if statement."""), harness, key)
280 return eval_single_node(node.body)
281 return eval_single_node(node.orelse)
282 if isinstance(node, ast.Compare):
283 left = eval_single_node(node.left)
284 # Don't allow expressions like (a < b) < c
285 right = eval_single_node(node.comparators[0])
286 op = eval_single_node(node.ops[0])
287 return op(left, right)
288 if isinstance(node, ast.BinOp):
289 left = eval_single_node(node.left)
290 right = eval_single_node(node.right)
291 op = eval_single_node(node.op)
292 return op(left, right)
293 if isinstance(node, ast.Call):
298 if node.func.id not in valid_calls:
299 logging.error(wrap("""\
300 in file %s at key '%s', there was an invalid
301 call to %s()"""), harness, key, node.func.id)
303 left = eval_single_node(node.args[0])
304 right = eval_single_node(node.args[1])
305 return valid_calls[node.func.id](left, right)
309 ast.NotEq: operator.ne,
311 ast.LtE: operator.le,
313 ast.GtE: operator.ge,
315 ast.Add: operator.add,
316 ast.Sub: operator.sub,
317 ast.Mult: operator.mul,
318 # Use floordiv (i.e. //) so that we never need to
320 ast.Div: operator.floordiv,
323 logging.error(wrap("""\
324 in file %s at key '%s', there was expression that
325 was impossible to evaluate"""), harness, key)
328 return eval_single_node(tree)
331 _platform_choices = {
338 "makefile-inc": "include",
343 "path-sep-re": re.escape("\\"),
346 "makefile-inc": "!INCLUDE",
349 # Assuming macos is the same as linux
350 _mac_os = dict(_platform_choices["linux"])
351 _mac_os["platform"] = "darwin"
352 _platform_choices["macos"] = _mac_os
355 def default_platform():
356 for arg_string, os_data in _platform_choices.items():
357 if sys.platform == os_data["platform"]:
363 "flags": ["-s", "--system"],
365 "choices": _platform_choices,
366 "default": str(default_platform()),
367 "help": textwrap.dedent("""\
368 which operating system to generate makefiles for.
369 Defaults to the current platform (%(default)s);
370 choices are {choices}""").format(
371 choices="[%s]" % ", ".join(_platform_choices)),
373 "flags": ["-v", "--verbose"],
374 "help": "verbose output",
375 "action": "store_true",
377 "flags": ["-w", "--very-verbose"],
378 "help": "very verbose output",
379 "action": "store_true",
384 pars = argparse.ArgumentParser(
385 description=prolog(),
386 formatter_class=argparse.RawDescriptionHelpFormatter)
388 flags = arg.pop("flags")
389 pars.add_argument(*flags, **arg)
390 return pars.parse_args()
393 def set_up_logging(args):
394 fmt = "{script}: %(levelname)s %(message)s".format(
395 script=os.path.basename(__file__))
396 if args.very_verbose:
397 logging.basicConfig(format=fmt, level=logging.DEBUG)
399 logging.basicConfig(format=fmt, level=logging.INFO)
401 logging.basicConfig(format=fmt, level=logging.WARNING)
404 return re.sub(r"\s+", " ", re.sub("\n", " ", string))
410 for root, _, fyles in os.walk("."):
411 if "Makefile.json" in fyles:
412 dump_makefile(root, args.system)
415 if __name__ == "__main__":