xref: /FreeRTOS-Plus-TCP-v4.0.0/test/cbmc/proofs/make_proof_makefiles.py (revision 1ab6eb88857cf1011bf1f8349b43a1c34c7ced0f)
1#!/usr/bin/env python3
2#
3# Generation of Makefiles for CBMC proofs.
4#
5# Copyright (C) 2022 Amazon.com, Inc. or its affiliates.  All Rights Reserved.
6#
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:
13#
14# The above copyright notice and this permission notice shall be included in all
15# copies or substantial portions of the Software.
16#
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
23# SOFTWARE.
24
25
26import argparse
27import ast
28import collections
29import json
30import logging
31import operator
32import os
33import os.path
34import platform
35import re
36import sys
37import textwrap
38import traceback
39
40
41# ______________________________________________________________________________
42# Compatibility between different python versions
43# ``````````````````````````````````````````````````````````````````````````````
44
45# Python 3 doesn't have basestring
46try:
47    basestring
48except NameError:
49    basestring = str
50
51# ast.Num was deprecated in python 3.8
52_plat = platform.python_version().split(".")
53if _plat[0] == "3" and int(_plat[1]) > 7:
54    ast_num = ast.Constant
55else:
56    ast_num = ast.Num
57# ______________________________________________________________________________
58
59
60def prolog():
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.
66
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:
71
72                {
73                    # 'T was brillig, and the slithy toves
74                    "FOO": "BAR",
75                    "BAZ": "{FOO}",
76
77                    # Did gyre and gimble in the wabe;
78                    "QUUX": 30
79                    "XYZZY": "__eval 5 if {QUUZ} < 5 else min({QUUX}, 60)"
80                }
81
82        then the resulting Makefile will look like
83
84                H_FOO = BAR
85                H_BAZ = BAR
86                H_QUUX = 30
87                H_XYZZY = 30
88
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:
92
93                { "FOO": ["BAR", "BAZ", "QUX"] }
94
95                ->
96
97                H_FOO = BAR BAZ QUX
98
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.
103
104
105                { "DEF": ["DEBUG", "FOO=BAR"] }
106
107                on Linux ->
108
109                H_DEF = -DDEBUG -DFOO=BAR
110
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.
116
117                {
118                    "INC": [ "my/cool/directory" ],
119                    "DEF": [ "HALF=//2" ]
120                }
121
122                On Windows ->
123
124                H_INC = /Imy\cool\directory
125                H_DEF = /DHALF=/2
126
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.
133    """)
134
135def 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("#")])
140    try:
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)
146        sys.exit(1)
147    return data
148
149
150def dump_makefile(dyr, system):
151    data = load_json_config_file(os.path.join(dyr, "Makefile.json"))
152
153    makefile = collections.OrderedDict()
154
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:
158        logging.error(
159            "Expected a list of object files in %s/Makefile.json" % dyr)
160        exit(1)
161    makefile["OBJS_EXCEPT_HARNESS"] = " ".join(
162        o for o in data["OBJS"] if not o.endswith("_harness.goto"))
163
164    so_far = collections.OrderedDict()
165    for name, value in data.items():
166        if isinstance(value, list):
167            new_value = []
168            for item in value:
169                new_value.append(compute(item, so_far, system, name, dyr, True))
170            makefile[name] = " ".join(new_value)
171        else:
172            makefile[name] = compute(value, so_far, system, name, dyr)
173
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()]
180
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:])
185
186    with open(os.path.join(dyr, "Makefile"), "w") as handle:
187        handle.write(("""{contents}
188
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))
193
194
195def 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)))
202        exit(1)
203
204    value = str(value)
205    try:
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,
213                      value, str(e))
214        exit(1)
215
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)
220    else:
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"),
225                              key, value)
226
227    if key == "DEF":
228        final_value = "%s%s" % (_platform_choices[system]["define"],
229                                evaluated)
230    elif key == "INC":
231        final_value = "%s%s" % (_platform_choices[system]["include"],
232                                evaluated)
233    else:
234        final_value = evaluated
235
236    # Allow this value to be used for future variable substitution
237    if appending:
238        try:
239            so_far[key] = "%s %s" % (so_far[key], final_value)
240        except KeyError:
241            so_far[key] = final_value
242        logging.debug("Appending final value '%s' to key '%s'",
243                      final_value, key)
244    else:
245        so_far[key] = final_value
246        logging.info("Key '%s' set to final value '%s'", key, final_value)
247
248    return final_value
249
250
251def eval_expr(expr_string, harness, key, value):
252    """
253    Safe evaluation of purely arithmetic expressions
254    """
255    try:
256        tree = ast.parse(expr_string, mode="eval").body
257    except SyntaxError:
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,
262                      value, expr_string)
263        exit(1)
264
265    def eval_single_node(node):
266        logging.debug(node)
267        if isinstance(node, ast_num):
268            return node.n
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)
278                exit(1)
279            if guard:
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):
294            valid_calls = {
295                "max": max,
296                "min": min,
297            }
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)
302                exit(1)
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)
306        try:
307            return {
308                ast.Eq: operator.eq,
309                ast.NotEq: operator.ne,
310                ast.Lt: operator.lt,
311                ast.LtE: operator.le,
312                ast.Gt: operator.gt,
313                ast.GtE: operator.ge,
314
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
319                # cast to an int
320                ast.Div: operator.floordiv,
321            }[type(node)]
322        except KeyError:
323            logging.error(wrap("""\
324                in file %s at key '%s', there was expression that
325                was impossible to evaluate"""), harness, key)
326            exit(1)
327
328    return eval_single_node(tree)
329
330
331_platform_choices = {
332    "linux": {
333        "platform": "linux",
334        "path-sep": "/",
335        "path-sep-re": "/",
336        "define": "-D",
337        "include": "-I",
338        "makefile-inc": "include",
339    },
340    "windows": {
341        "platform": "win32",
342        "path-sep": "\\",
343        "path-sep-re": re.escape("\\"),
344        "define": "/D",
345        "include": "/I",
346        "makefile-inc": "!INCLUDE",
347    },
348}
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
353
354
355def default_platform():
356    for arg_string, os_data in _platform_choices.items():
357        if sys.platform == os_data["platform"]:
358            return arg_string
359    return "linux"
360
361
362_args = [{
363    "flags": ["-s", "--system"],
364    "metavar": "OS",
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)),
372}, {
373    "flags": ["-v", "--verbose"],
374    "help": "verbose output",
375    "action": "store_true",
376}, {
377    "flags": ["-w", "--very-verbose"],
378    "help": "very verbose output",
379    "action": "store_true",
380    }]
381
382
383def get_args():
384    pars = argparse.ArgumentParser(
385        description=prolog(),
386        formatter_class=argparse.RawDescriptionHelpFormatter)
387    for arg in _args:
388        flags = arg.pop("flags")
389        pars.add_argument(*flags, **arg)
390    return pars.parse_args()
391
392
393def 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)
398    elif args.verbose:
399        logging.basicConfig(format=fmt, level=logging.INFO)
400    else:
401        logging.basicConfig(format=fmt, level=logging.WARNING)
402
403def wrap(string):
404    return re.sub(r"\s+", " ", re.sub("\n", " ", string))
405
406def main():
407    args = get_args()
408    set_up_logging(args)
409
410    for root, _, fyles in os.walk("."):
411        if "Makefile.json" in fyles:
412            dump_makefile(root, args.system)
413
414
415if __name__ == "__main__":
416    main()
417