You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
FreeRTOS/FreeRTOS-Plus/Test/CBMC/proofs/make_proof_makefiles.py

417 lines
14 KiB
Python

#!/usr/bin/env python3
#
# Generation of Makefiles for CBMC proofs.
#
# Copyright (C) 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import argparse
import ast
import collections
import json
import logging
import operator
import os
import os.path
import platform
import re
import sys
import textwrap
import traceback
# ______________________________________________________________________________
# Compatibility between different python versions
# ``````````````````````````````````````````````````````````````````````````````
# Python 3 doesn't have basestring
try:
basestring
except NameError:
basestring = str
# ast.Num was deprecated in python 3.8
_plat = platform.python_version().split(".")
if _plat[0] == "3" and int(_plat[1]) > 7:
ast_num = ast.Constant
else:
ast_num = ast.Num
# ______________________________________________________________________________
def prolog():
return textwrap.dedent("""\
This script generates Makefiles that can be used either on Windows (with
NMake) or Linux (with GNU Make). The Makefiles consist only of variable
definitions; they are intended to be used with a common Makefile that
defines the actual rules.
The Makefiles are generated from JSON specifications. These are simple
key-value dicts, but can contain variables and computation, as
well as comments (lines whose first character is "#"). If the
JSON file contains the following information:
{
# 'T was brillig, and the slithy toves
"FOO": "BAR",
"BAZ": "{FOO}",
# Did gyre and gimble in the wabe;
"QUUX": 30
"XYZZY": "__eval 5 if {QUUZ} < 5 else min({QUUX}, 60)"
}
then the resulting Makefile will look like
H_FOO = BAR
H_BAZ = BAR
H_QUUX = 30
H_XYZZY = 30
The language used for evaluation is highly restricted; arbitrary
python is not allowed. JSON values that are lists will be
joined with whitespace:
{ "FOO": ["BAR", "BAZ", "QUX"] }
->
H_FOO = BAR BAZ QUX
As a special case, if a key is equal to "DEF", "INC" (and maybe more,
read the code) the list of values is treated as a list of defines or
include paths. Thus, they have -D or /D, or -I or /I, prepended to them
as appropriate for the platform.
{ "DEF": ["DEBUG", "FOO=BAR"] }
on Linux ->
H_DEF = -DDEBUG -DFOO=BAR
Pathnames are written with a forward slash in the JSON file. In
each value, all slashes are replaced with backslashes if
generating Makefiles for Windows. If you wish to generate a
slash even on Windows, use two slashes---that will be changed
into a single forward slash on both Windows and Linux.
{
"INC": [ "my/cool/directory" ],
"DEF": [ "HALF=//2" ]
}
On Windows ->
H_INC = /Imy\cool\directory
H_DEF = /DHALF=/2
When invoked, this script walks the directory tree looking for files
called "Makefile.json". It reads that file and dumps a Makefile in that
same directory. We assume that this script lives in the same directory
as a Makefile called "Makefile.common", which contains the actual Make
rules. The final line of each of the generated Makefiles will be an
include statement, including Makefile.common.
""")
def load_json_config_file(file):
with open(file) as handle:
lines = handle.read().splitlines()
no_comments = "\n".join([line for line in lines
if line and not line.lstrip().startswith("#")])
try:
data = json.loads(no_comments,
object_pairs_hook=collections.OrderedDict)
except json.decoder.JSONDecodeError:
traceback.print_exc()
logging.error("parsing file %s", file)
sys.exit(1)
return data
def dump_makefile(dyr, system):
data = load_json_config_file(os.path.join(dyr, "Makefile.json"))
makefile = collections.OrderedDict()
# Makefile.common expects a variable called OBJS_EXCEPT_HARNESS to be
# set to a list of all the object files except the harness.
if "OBJS" not in data:
logging.error(
"Expected a list of object files in %s/Makefile.json" % dyr)
exit(1)
makefile["OBJS_EXCEPT_HARNESS"] = " ".join(
o for o in data["OBJS"] if not o.endswith("_harness.goto"))
so_far = collections.OrderedDict()
for name, value in data.items():
if isinstance(value, list):
new_value = []
for item in value:
new_value.append(compute(item, so_far, system, name, dyr, True))
makefile[name] = " ".join(new_value)
else:
makefile[name] = compute(value, so_far, system, name, dyr)
if (("EXPECTED" not in makefile.keys()) or
str(makefile["EXPECTED"]).lower() == "true"):
makefile["EXPECTED"] = "SUCCESSFUL"
elif str(makefile["EXPECTED"]).lower() == "false":
makefile["EXPECTED"] = "FAILURE"
makefile = ["H_%s = %s" % (k, v) for k, v in makefile.items()]
# Deal with the case of a harness being nested several levels under
# the top-level proof directory, where the common Makefile lives
common_dir_path = "..%s" % _platform_choices[system]["path-sep"]
common_dir_path = common_dir_path * len(dyr.split(os.path.sep)[1:])
with open(os.path.join(dyr, "Makefile"), "w") as handle:
handle.write(("""{contents}
{include} {common_dir_path}Makefile.common""").format(
contents="\n".join(makefile),
include=_platform_choices[system]["makefile-inc"],
common_dir_path=common_dir_path))
def compute(value, so_far, system, key, harness, appending=False):
if not isinstance(value, (basestring, float, int)):
logging.error(wrap("""\
in file %s, the key '%s' has value '%s',
which is of the wrong type (%s)"""),
os.path.join(harness, "Makefile.json"), key,
str(value), str(type(value)))
exit(1)
value = str(value)
try:
var_subbed = value.format(**so_far)
except KeyError as e:
logging.error(wrap("""\
in file %s, the key '%s' has value '%s', which
contains the variable %s, but that variable has
not previously been set in the file"""),
os.path.join(harness, "Makefile.json"), key,
value, str(e))
exit(1)
if var_subbed[:len("__eval")] != "__eval":
tmp = re.sub("//", "__DOUBLE_SLASH__", var_subbed)
tmp = re.sub("/", _platform_choices[system]["path-sep-re"], tmp)
evaluated = re.sub("__DOUBLE_SLASH__", "/", tmp)
else:
to_eval = var_subbed[len("__eval"):].strip()
logging.debug("About to evaluate '%s'", to_eval)
evaluated = eval_expr(to_eval,
os.path.join(harness, "Makefile.json"),
key, value)
if key == "DEF":
final_value = "%s%s" % (_platform_choices[system]["define"],
evaluated)
elif key == "INC":
final_value = "%s%s" % (_platform_choices[system]["include"],
evaluated)
else:
final_value = evaluated
# Allow this value to be used for future variable substitution
if appending:
try:
so_far[key] = "%s %s" % (so_far[key], final_value)
except KeyError:
so_far[key] = final_value
logging.debug("Appending final value '%s' to key '%s'",
final_value, key)
else:
so_far[key] = final_value
logging.info("Key '%s' set to final value '%s'", key, final_value)
return final_value
def eval_expr(expr_string, harness, key, value):
"""
Safe evaluation of purely arithmetic expressions
"""
try:
tree = ast.parse(expr_string, mode="eval").body
except SyntaxError:
traceback.print_exc()
logging.error(wrap("""\
in file %s at key '%s', value '%s' contained the expression
'%s' which is an invalid expression"""), harness, key,
value, expr_string)
exit(1)
def eval_single_node(node):
logging.debug(node)
if isinstance(node, ast_num):
return node.n
# We're only doing IfExp, which is Python's ternary operator
# (i.e. it's an expression). NOT If, which is a statement.
if isinstance(node, ast.IfExp):
# Let's be strict and only allow actual booleans in the guard
guard = eval_single_node(node.test)
if guard is not True and guard is not False:
logging.error(wrap("""\
in file %s at key '%s', there was an invalid guard
for an if statement."""), harness, key)
exit(1)
if guard:
return eval_single_node(node.body)
return eval_single_node(node.orelse)
if isinstance(node, ast.Compare):
left = eval_single_node(node.left)
# Don't allow expressions like (a < b) < c
right = eval_single_node(node.comparators[0])
op = eval_single_node(node.ops[0])
return op(left, right)
if isinstance(node, ast.BinOp):
left = eval_single_node(node.left)
right = eval_single_node(node.right)
op = eval_single_node(node.op)
return op(left, right)
if isinstance(node, ast.Call):
valid_calls = {
"max": max,
"min": min,
}
if node.func.id not in valid_calls:
logging.error(wrap("""\
in file %s at key '%s', there was an invalid
call to %s()"""), harness, key, node.func.id)
exit(1)
left = eval_single_node(node.args[0])
right = eval_single_node(node.args[1])
return valid_calls[node.func.id](left, right)
try:
return {
ast.Eq: operator.eq,
ast.NotEq: operator.ne,
ast.Lt: operator.lt,
ast.LtE: operator.le,
ast.Gt: operator.gt,
ast.GtE: operator.ge,
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
# Use floordiv (i.e. //) so that we never need to
# cast to an int
ast.Div: operator.floordiv,
}[type(node)]
except KeyError:
logging.error(wrap("""\
in file %s at key '%s', there was expression that
was impossible to evaluate"""), harness, key)
exit(1)
return eval_single_node(tree)
_platform_choices = {
"linux": {
"platform": "linux",
"path-sep": "/",
"path-sep-re": "/",
"define": "-D",
"include": "-I",
"makefile-inc": "include",
},
"windows": {
"platform": "win32",
"path-sep": "\\",
"path-sep-re": re.escape("\\"),
"define": "/D",
"include": "/I",
"makefile-inc": "!INCLUDE",
},
}
# Assuming macos is the same as linux
_mac_os = dict(_platform_choices["linux"])
_mac_os["platform"] = "darwin"
_platform_choices["macos"] = _mac_os
def default_platform():
for arg_string, os_data in _platform_choices.items():
if sys.platform == os_data["platform"]:
return arg_string
return "linux"
_args = [{
"flags": ["-s", "--system"],
"metavar": "OS",
"choices": _platform_choices,
"default": str(default_platform()),
"help": textwrap.dedent("""\
which operating system to generate makefiles for.
Defaults to the current platform (%(default)s);
choices are {choices}""").format(
choices="[%s]" % ", ".join(_platform_choices)),
}, {
"flags": ["-v", "--verbose"],
"help": "verbose output",
"action": "store_true",
}, {
"flags": ["-w", "--very-verbose"],
"help": "very verbose output",
"action": "store_true",
}]
def get_args():
pars = argparse.ArgumentParser(
description=prolog(),
formatter_class=argparse.RawDescriptionHelpFormatter)
for arg in _args:
flags = arg.pop("flags")
pars.add_argument(*flags, **arg)
return pars.parse_args()
def set_up_logging(args):
fmt = "{script}: %(levelname)s %(message)s".format(
script=os.path.basename(__file__))
if args.very_verbose:
logging.basicConfig(format=fmt, level=logging.DEBUG)
elif args.verbose:
logging.basicConfig(format=fmt, level=logging.INFO)
else:
logging.basicConfig(format=fmt, level=logging.WARNING)
def wrap(string):
return re.sub(r"\s+", " ", re.sub("\n", " ", string))
def main():
args = get_args()
set_up_logging(args)
for root, _, fyles in os.walk("."):
if "Makefile.json" in fyles:
dump_makefile(root, args.system)
if __name__ == "__main__":
main()