406 lines
14 KiB
Python
Executable File
406 lines
14 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
"""Check CFC - Check Compile Flow Consistency
|
|
|
|
This is a compiler wrapper for testing that code generation is consistent with
|
|
different compilation processes. It checks that code is not unduly affected by
|
|
compiler options or other changes which should not have side effects.
|
|
|
|
To use:
|
|
-Ensure that the compiler under test (i.e. clang, clang++) is on the PATH
|
|
-On Linux copy this script to the name of the compiler
|
|
e.g. cp check_cfc.py clang && cp check_cfc.py clang++
|
|
-On Windows use setup.py to generate check_cfc.exe and copy that to clang.exe
|
|
and clang++.exe
|
|
-Enable the desired checks in check_cfc.cfg (in the same directory as the
|
|
wrapper)
|
|
e.g.
|
|
[Checks]
|
|
dash_g_no_change = true
|
|
dash_s_no_change = false
|
|
|
|
-The wrapper can be run using its absolute path or added to PATH before the
|
|
compiler under test
|
|
e.g. export PATH=<path to check_cfc>:$PATH
|
|
-Compile as normal. The wrapper intercepts normal -c compiles and will return
|
|
non-zero if the check fails.
|
|
e.g.
|
|
$ clang -c test.cpp
|
|
Code difference detected with -g
|
|
--- /tmp/tmp5nv893.o
|
|
+++ /tmp/tmp6Vwjnc.o
|
|
@@ -1 +1 @@
|
|
- 0: 48 8b 05 51 0b 20 00 mov 0x200b51(%rip),%rax
|
|
+ 0: 48 39 3d 51 0b 20 00 cmp %rdi,0x200b51(%rip)
|
|
|
|
-To run LNT with Check CFC specify the absolute path to the wrapper to the --cc
|
|
and --cxx options
|
|
e.g.
|
|
lnt runtest nt --cc <path to check_cfc>/clang \\
|
|
--cxx <path to check_cfc>/clang++ ...
|
|
|
|
To add a new check:
|
|
-Create a new subclass of WrapperCheck
|
|
-Implement the perform_check() method. This should perform the alternate compile
|
|
and do the comparison.
|
|
-Add the new check to check_cfc.cfg. The check has the same name as the
|
|
subclass.
|
|
"""
|
|
|
|
from __future__ import absolute_import, division, print_function
|
|
|
|
import imp
|
|
import os
|
|
import platform
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
try:
|
|
import configparser
|
|
except ImportError:
|
|
import ConfigParser as configparser
|
|
import io
|
|
|
|
import obj_diff
|
|
|
|
def is_windows():
|
|
"""Returns True if running on Windows."""
|
|
return platform.system() == 'Windows'
|
|
|
|
class WrapperStepException(Exception):
|
|
"""Exception type to be used when a step other than the original compile
|
|
fails."""
|
|
def __init__(self, msg, stdout, stderr):
|
|
self.msg = msg
|
|
self.stdout = stdout
|
|
self.stderr = stderr
|
|
|
|
class WrapperCheckException(Exception):
|
|
"""Exception type to be used when a comparison check fails."""
|
|
def __init__(self, msg):
|
|
self.msg = msg
|
|
|
|
def main_is_frozen():
|
|
"""Returns True when running as a py2exe executable."""
|
|
return (hasattr(sys, "frozen") or # new py2exe
|
|
hasattr(sys, "importers") or # old py2exe
|
|
imp.is_frozen("__main__")) # tools/freeze
|
|
|
|
def get_main_dir():
|
|
"""Get the directory that the script or executable is located in."""
|
|
if main_is_frozen():
|
|
return os.path.dirname(sys.executable)
|
|
return os.path.dirname(sys.argv[0])
|
|
|
|
def remove_dir_from_path(path_var, directory):
|
|
"""Remove the specified directory from path_var, a string representing
|
|
PATH"""
|
|
pathlist = path_var.split(os.pathsep)
|
|
norm_directory = os.path.normpath(os.path.normcase(directory))
|
|
pathlist = [x for x in pathlist if os.path.normpath(
|
|
os.path.normcase(x)) != norm_directory]
|
|
return os.pathsep.join(pathlist)
|
|
|
|
def path_without_wrapper():
|
|
"""Returns the PATH variable modified to remove the path to this program."""
|
|
scriptdir = get_main_dir()
|
|
path = os.environ['PATH']
|
|
return remove_dir_from_path(path, scriptdir)
|
|
|
|
def flip_dash_g(args):
|
|
"""Search for -g in args. If it exists then return args without. If not then
|
|
add it."""
|
|
if '-g' in args:
|
|
# Return args without any -g
|
|
return [x for x in args if x != '-g']
|
|
else:
|
|
# No -g, add one
|
|
return args + ['-g']
|
|
|
|
def derive_output_file(args):
|
|
"""Derive output file from the input file (if just one) or None
|
|
otherwise."""
|
|
infile = get_input_file(args)
|
|
if infile is None:
|
|
return None
|
|
else:
|
|
return '{}.o'.format(os.path.splitext(infile)[0])
|
|
|
|
def get_output_file(args):
|
|
"""Return the output file specified by this command or None if not
|
|
specified."""
|
|
grabnext = False
|
|
for arg in args:
|
|
if grabnext:
|
|
return arg
|
|
if arg == '-o':
|
|
# Specified as a separate arg
|
|
grabnext = True
|
|
elif arg.startswith('-o'):
|
|
# Specified conjoined with -o
|
|
return arg[2:]
|
|
assert grabnext == False
|
|
|
|
return None
|
|
|
|
def is_output_specified(args):
|
|
"""Return true is output file is specified in args."""
|
|
return get_output_file(args) is not None
|
|
|
|
def replace_output_file(args, new_name):
|
|
"""Replaces the specified name of an output file with the specified name.
|
|
Assumes that the output file name is specified in the command line args."""
|
|
replaceidx = None
|
|
attached = False
|
|
for idx, val in enumerate(args):
|
|
if val == '-o':
|
|
replaceidx = idx + 1
|
|
attached = False
|
|
elif val.startswith('-o'):
|
|
replaceidx = idx
|
|
attached = True
|
|
|
|
if replaceidx is None:
|
|
raise Exception
|
|
replacement = new_name
|
|
if attached == True:
|
|
replacement = '-o' + new_name
|
|
args[replaceidx] = replacement
|
|
return args
|
|
|
|
def add_output_file(args, output_file):
|
|
"""Append an output file to args, presuming not already specified."""
|
|
return args + ['-o', output_file]
|
|
|
|
def set_output_file(args, output_file):
|
|
"""Set the output file within the arguments. Appends or replaces as
|
|
appropriate."""
|
|
if is_output_specified(args):
|
|
args = replace_output_file(args, output_file)
|
|
else:
|
|
args = add_output_file(args, output_file)
|
|
return args
|
|
|
|
gSrcFileSuffixes = ('.c', '.cpp', '.cxx', '.c++', '.cp', '.cc')
|
|
|
|
def get_input_file(args):
|
|
"""Return the input file string if it can be found (and there is only
|
|
one)."""
|
|
inputFiles = list()
|
|
for arg in args:
|
|
testarg = arg
|
|
quotes = ('"', "'")
|
|
while testarg.endswith(quotes):
|
|
testarg = testarg[:-1]
|
|
testarg = os.path.normcase(testarg)
|
|
|
|
# Test if it is a source file
|
|
if testarg.endswith(gSrcFileSuffixes):
|
|
inputFiles.append(arg)
|
|
if len(inputFiles) == 1:
|
|
return inputFiles[0]
|
|
else:
|
|
return None
|
|
|
|
def set_input_file(args, input_file):
|
|
"""Replaces the input file with that specified."""
|
|
infile = get_input_file(args)
|
|
if infile:
|
|
infile_idx = args.index(infile)
|
|
args[infile_idx] = input_file
|
|
return args
|
|
else:
|
|
# Could not find input file
|
|
assert False
|
|
|
|
def is_normal_compile(args):
|
|
"""Check if this is a normal compile which will output an object file rather
|
|
than a preprocess or link. args is a list of command line arguments."""
|
|
compile_step = '-c' in args
|
|
# Bitcode cannot be disassembled in the same way
|
|
bitcode = '-flto' in args or '-emit-llvm' in args
|
|
# Version and help are queries of the compiler and override -c if specified
|
|
query = '--version' in args or '--help' in args
|
|
# Options to output dependency files for make
|
|
dependency = '-M' in args or '-MM' in args
|
|
# Check if the input is recognised as a source file (this may be too
|
|
# strong a restriction)
|
|
input_is_valid = bool(get_input_file(args))
|
|
return compile_step and not bitcode and not query and not dependency and input_is_valid
|
|
|
|
def run_step(command, my_env, error_on_failure):
|
|
"""Runs a step of the compilation. Reports failure as exception."""
|
|
# Need to use shell=True on Windows as Popen won't use PATH otherwise.
|
|
p = subprocess.Popen(command, stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE, env=my_env, shell=is_windows())
|
|
(stdout, stderr) = p.communicate()
|
|
if p.returncode != 0:
|
|
raise WrapperStepException(error_on_failure, stdout, stderr)
|
|
|
|
def get_temp_file_name(suffix):
|
|
"""Get a temporary file name with a particular suffix. Let the caller be
|
|
responsible for deleting it."""
|
|
tf = tempfile.NamedTemporaryFile(suffix=suffix, delete=False)
|
|
tf.close()
|
|
return tf.name
|
|
|
|
class WrapperCheck(object):
|
|
"""Base class for a check. Subclass this to add a check."""
|
|
def __init__(self, output_file_a):
|
|
"""Record the base output file that will be compared against."""
|
|
self._output_file_a = output_file_a
|
|
|
|
def perform_check(self, arguments, my_env):
|
|
"""Override this to perform the modified compilation and required
|
|
checks."""
|
|
raise NotImplementedError("Please Implement this method")
|
|
|
|
class dash_g_no_change(WrapperCheck):
|
|
def perform_check(self, arguments, my_env):
|
|
"""Check if different code is generated with/without the -g flag."""
|
|
output_file_b = get_temp_file_name('.o')
|
|
|
|
alternate_command = list(arguments)
|
|
alternate_command = flip_dash_g(alternate_command)
|
|
alternate_command = set_output_file(alternate_command, output_file_b)
|
|
run_step(alternate_command, my_env, "Error compiling with -g")
|
|
|
|
# Compare disassembly (returns first diff if differs)
|
|
difference = obj_diff.compare_object_files(self._output_file_a,
|
|
output_file_b)
|
|
if difference:
|
|
raise WrapperCheckException(
|
|
"Code difference detected with -g\n{}".format(difference))
|
|
|
|
# Clean up temp file if comparison okay
|
|
os.remove(output_file_b)
|
|
|
|
class dash_s_no_change(WrapperCheck):
|
|
def perform_check(self, arguments, my_env):
|
|
"""Check if compiling to asm then assembling in separate steps results
|
|
in different code than compiling to object directly."""
|
|
output_file_b = get_temp_file_name('.o')
|
|
|
|
alternate_command = arguments + ['-via-file-asm']
|
|
alternate_command = set_output_file(alternate_command, output_file_b)
|
|
run_step(alternate_command, my_env,
|
|
"Error compiling with -via-file-asm")
|
|
|
|
# Compare if object files are exactly the same
|
|
exactly_equal = obj_diff.compare_exact(self._output_file_a, output_file_b)
|
|
if not exactly_equal:
|
|
# Compare disassembly (returns first diff if differs)
|
|
difference = obj_diff.compare_object_files(self._output_file_a,
|
|
output_file_b)
|
|
if difference:
|
|
raise WrapperCheckException(
|
|
"Code difference detected with -S\n{}".format(difference))
|
|
|
|
# Code is identical, compare debug info
|
|
dbgdifference = obj_diff.compare_debug_info(self._output_file_a,
|
|
output_file_b)
|
|
if dbgdifference:
|
|
raise WrapperCheckException(
|
|
"Debug info difference detected with -S\n{}".format(dbgdifference))
|
|
|
|
raise WrapperCheckException("Object files not identical with -S\n")
|
|
|
|
# Clean up temp file if comparison okay
|
|
os.remove(output_file_b)
|
|
|
|
if __name__ == '__main__':
|
|
# Create configuration defaults from list of checks
|
|
default_config = """
|
|
[Checks]
|
|
"""
|
|
|
|
# Find all subclasses of WrapperCheck
|
|
checks = [cls.__name__ for cls in vars()['WrapperCheck'].__subclasses__()]
|
|
|
|
for c in checks:
|
|
default_config += "{} = false\n".format(c)
|
|
|
|
config = configparser.RawConfigParser()
|
|
config.readfp(io.BytesIO(default_config))
|
|
scriptdir = get_main_dir()
|
|
config_path = os.path.join(scriptdir, 'check_cfc.cfg')
|
|
try:
|
|
config.read(os.path.join(config_path))
|
|
except:
|
|
print("Could not read config from {}, "
|
|
"using defaults.".format(config_path))
|
|
|
|
my_env = os.environ.copy()
|
|
my_env['PATH'] = path_without_wrapper()
|
|
|
|
arguments_a = list(sys.argv)
|
|
|
|
# Prevent infinite loop if called with absolute path.
|
|
arguments_a[0] = os.path.basename(arguments_a[0])
|
|
|
|
# Sanity check
|
|
enabled_checks = [check_name
|
|
for check_name in checks
|
|
if config.getboolean('Checks', check_name)]
|
|
checks_comma_separated = ', '.join(enabled_checks)
|
|
print("Check CFC, checking: {}".format(checks_comma_separated))
|
|
|
|
# A - original compilation
|
|
output_file_orig = get_output_file(arguments_a)
|
|
if output_file_orig is None:
|
|
output_file_orig = derive_output_file(arguments_a)
|
|
|
|
p = subprocess.Popen(arguments_a, env=my_env, shell=is_windows())
|
|
p.communicate()
|
|
if p.returncode != 0:
|
|
sys.exit(p.returncode)
|
|
|
|
if not is_normal_compile(arguments_a) or output_file_orig is None:
|
|
# Bail out here if we can't apply checks in this case.
|
|
# Does not indicate an error.
|
|
# Maybe not straight compilation (e.g. -S or --version or -flto)
|
|
# or maybe > 1 input files.
|
|
sys.exit(0)
|
|
|
|
# Sometimes we generate files which have very long names which can't be
|
|
# read/disassembled. This will exit early if we can't find the file we
|
|
# expected to be output.
|
|
if not os.path.isfile(output_file_orig):
|
|
sys.exit(0)
|
|
|
|
# Copy output file to a temp file
|
|
temp_output_file_orig = get_temp_file_name('.o')
|
|
shutil.copyfile(output_file_orig, temp_output_file_orig)
|
|
|
|
# Run checks, if they are enabled in config and if they are appropriate for
|
|
# this command line.
|
|
current_module = sys.modules[__name__]
|
|
for check_name in checks:
|
|
if config.getboolean('Checks', check_name):
|
|
class_ = getattr(current_module, check_name)
|
|
checker = class_(temp_output_file_orig)
|
|
try:
|
|
checker.perform_check(arguments_a, my_env)
|
|
except WrapperCheckException as e:
|
|
# Check failure
|
|
print("{} {}".format(get_input_file(arguments_a), e.msg), file=sys.stderr)
|
|
|
|
# Remove file to comply with build system expectations (no
|
|
# output file if failed)
|
|
os.remove(output_file_orig)
|
|
sys.exit(1)
|
|
|
|
except WrapperStepException as e:
|
|
# Compile step failure
|
|
print(e.msg, file=sys.stderr)
|
|
print("*** stdout ***", file=sys.stderr)
|
|
print(e.stdout, file=sys.stderr)
|
|
print("*** stderr ***", file=sys.stderr)
|
|
print(e.stderr, file=sys.stderr)
|
|
|
|
# Remove file to comply with build system expectations (no
|
|
# output file if failed)
|
|
os.remove(output_file_orig)
|
|
sys.exit(1)
|