1e5dd7070Spatrick#!/usr/bin/env python
2e5dd7070Spatrick
3e5dd7070Spatrick"""Check CFC - Check Compile Flow Consistency
4e5dd7070Spatrick
5e5dd7070SpatrickThis is a compiler wrapper for testing that code generation is consistent with
6e5dd7070Spatrickdifferent compilation processes. It checks that code is not unduly affected by
7e5dd7070Spatrickcompiler options or other changes which should not have side effects.
8e5dd7070Spatrick
9e5dd7070SpatrickTo use:
10e5dd7070Spatrick-Ensure that the compiler under test (i.e. clang, clang++) is on the PATH
11e5dd7070Spatrick-On Linux copy this script to the name of the compiler
12e5dd7070Spatrick   e.g. cp check_cfc.py clang && cp check_cfc.py clang++
13e5dd7070Spatrick-On Windows use setup.py to generate check_cfc.exe and copy that to clang.exe
14e5dd7070Spatrick and clang++.exe
15e5dd7070Spatrick-Enable the desired checks in check_cfc.cfg (in the same directory as the
16e5dd7070Spatrick wrapper)
17e5dd7070Spatrick   e.g.
18e5dd7070Spatrick[Checks]
19e5dd7070Spatrickdash_g_no_change = true
20e5dd7070Spatrickdash_s_no_change = false
21e5dd7070Spatrick
22e5dd7070Spatrick-The wrapper can be run using its absolute path or added to PATH before the
23e5dd7070Spatrick compiler under test
24e5dd7070Spatrick   e.g. export PATH=<path to check_cfc>:$PATH
25e5dd7070Spatrick-Compile as normal. The wrapper intercepts normal -c compiles and will return
26e5dd7070Spatrick non-zero if the check fails.
27e5dd7070Spatrick   e.g.
28e5dd7070Spatrick$ clang -c test.cpp
29e5dd7070SpatrickCode difference detected with -g
30e5dd7070Spatrick--- /tmp/tmp5nv893.o
31e5dd7070Spatrick+++ /tmp/tmp6Vwjnc.o
32e5dd7070Spatrick@@ -1 +1 @@
33e5dd7070Spatrick-   0:       48 8b 05 51 0b 20 00    mov    0x200b51(%rip),%rax
34e5dd7070Spatrick+   0:       48 39 3d 51 0b 20 00    cmp    %rdi,0x200b51(%rip)
35e5dd7070Spatrick
36e5dd7070Spatrick-To run LNT with Check CFC specify the absolute path to the wrapper to the --cc
37e5dd7070Spatrick and --cxx options
38e5dd7070Spatrick   e.g.
39e5dd7070Spatrick   lnt runtest nt --cc <path to check_cfc>/clang \\
40e5dd7070Spatrick           --cxx <path to check_cfc>/clang++ ...
41e5dd7070Spatrick
42e5dd7070SpatrickTo add a new check:
43e5dd7070Spatrick-Create a new subclass of WrapperCheck
44e5dd7070Spatrick-Implement the perform_check() method. This should perform the alternate compile
45e5dd7070Spatrick and do the comparison.
46e5dd7070Spatrick-Add the new check to check_cfc.cfg. The check has the same name as the
47e5dd7070Spatrick subclass.
48e5dd7070Spatrick"""
49e5dd7070Spatrick
50e5dd7070Spatrickfrom __future__ import absolute_import, division, print_function
51e5dd7070Spatrick
52e5dd7070Spatrickimport imp
53e5dd7070Spatrickimport os
54e5dd7070Spatrickimport platform
55e5dd7070Spatrickimport shutil
56e5dd7070Spatrickimport subprocess
57e5dd7070Spatrickimport sys
58e5dd7070Spatrickimport tempfile
59e5dd7070Spatricktry:
60e5dd7070Spatrick    import configparser
61e5dd7070Spatrickexcept ImportError:
62e5dd7070Spatrick    import ConfigParser as configparser
63e5dd7070Spatrickimport io
64e5dd7070Spatrick
65e5dd7070Spatrickimport obj_diff
66e5dd7070Spatrick
67e5dd7070Spatrickdef is_windows():
68e5dd7070Spatrick    """Returns True if running on Windows."""
69e5dd7070Spatrick    return platform.system() == 'Windows'
70e5dd7070Spatrick
71e5dd7070Spatrickclass WrapperStepException(Exception):
72e5dd7070Spatrick    """Exception type to be used when a step other than the original compile
73e5dd7070Spatrick    fails."""
74e5dd7070Spatrick    def __init__(self, msg, stdout, stderr):
75e5dd7070Spatrick        self.msg = msg
76e5dd7070Spatrick        self.stdout = stdout
77e5dd7070Spatrick        self.stderr = stderr
78e5dd7070Spatrick
79e5dd7070Spatrickclass WrapperCheckException(Exception):
80e5dd7070Spatrick    """Exception type to be used when a comparison check fails."""
81e5dd7070Spatrick    def __init__(self, msg):
82e5dd7070Spatrick        self.msg = msg
83e5dd7070Spatrick
84e5dd7070Spatrickdef main_is_frozen():
85e5dd7070Spatrick    """Returns True when running as a py2exe executable."""
86e5dd7070Spatrick    return (hasattr(sys, "frozen") or # new py2exe
87e5dd7070Spatrick            hasattr(sys, "importers") or # old py2exe
88e5dd7070Spatrick            imp.is_frozen("__main__")) # tools/freeze
89e5dd7070Spatrick
90e5dd7070Spatrickdef get_main_dir():
91e5dd7070Spatrick    """Get the directory that the script or executable is located in."""
92e5dd7070Spatrick    if main_is_frozen():
93e5dd7070Spatrick        return os.path.dirname(sys.executable)
94e5dd7070Spatrick    return os.path.dirname(sys.argv[0])
95e5dd7070Spatrick
96e5dd7070Spatrickdef remove_dir_from_path(path_var, directory):
97e5dd7070Spatrick    """Remove the specified directory from path_var, a string representing
98e5dd7070Spatrick    PATH"""
99e5dd7070Spatrick    pathlist = path_var.split(os.pathsep)
100e5dd7070Spatrick    norm_directory = os.path.normpath(os.path.normcase(directory))
101e5dd7070Spatrick    pathlist = [x for x in pathlist if os.path.normpath(
102e5dd7070Spatrick        os.path.normcase(x)) != norm_directory]
103e5dd7070Spatrick    return os.pathsep.join(pathlist)
104e5dd7070Spatrick
105e5dd7070Spatrickdef path_without_wrapper():
106e5dd7070Spatrick    """Returns the PATH variable modified to remove the path to this program."""
107e5dd7070Spatrick    scriptdir = get_main_dir()
108e5dd7070Spatrick    path = os.environ['PATH']
109e5dd7070Spatrick    return remove_dir_from_path(path, scriptdir)
110e5dd7070Spatrick
111e5dd7070Spatrickdef flip_dash_g(args):
112e5dd7070Spatrick    """Search for -g in args. If it exists then return args without. If not then
113e5dd7070Spatrick    add it."""
114e5dd7070Spatrick    if '-g' in args:
115e5dd7070Spatrick        # Return args without any -g
116e5dd7070Spatrick        return [x for x in args if x != '-g']
117e5dd7070Spatrick    else:
118e5dd7070Spatrick        # No -g, add one
119e5dd7070Spatrick        return args + ['-g']
120e5dd7070Spatrick
121e5dd7070Spatrickdef derive_output_file(args):
122e5dd7070Spatrick    """Derive output file from the input file (if just one) or None
123e5dd7070Spatrick    otherwise."""
124e5dd7070Spatrick    infile = get_input_file(args)
125e5dd7070Spatrick    if infile is None:
126e5dd7070Spatrick        return None
127e5dd7070Spatrick    else:
128e5dd7070Spatrick        return '{}.o'.format(os.path.splitext(infile)[0])
129e5dd7070Spatrick
130e5dd7070Spatrickdef get_output_file(args):
131e5dd7070Spatrick    """Return the output file specified by this command or None if not
132e5dd7070Spatrick    specified."""
133e5dd7070Spatrick    grabnext = False
134e5dd7070Spatrick    for arg in args:
135e5dd7070Spatrick        if grabnext:
136e5dd7070Spatrick            return arg
137e5dd7070Spatrick        if arg == '-o':
138e5dd7070Spatrick            # Specified as a separate arg
139e5dd7070Spatrick            grabnext = True
140e5dd7070Spatrick        elif arg.startswith('-o'):
141e5dd7070Spatrick            # Specified conjoined with -o
142e5dd7070Spatrick            return arg[2:]
143e5dd7070Spatrick    assert grabnext == False
144e5dd7070Spatrick
145e5dd7070Spatrick    return None
146e5dd7070Spatrick
147e5dd7070Spatrickdef is_output_specified(args):
148e5dd7070Spatrick    """Return true is output file is specified in args."""
149e5dd7070Spatrick    return get_output_file(args) is not None
150e5dd7070Spatrick
151e5dd7070Spatrickdef replace_output_file(args, new_name):
152e5dd7070Spatrick    """Replaces the specified name of an output file with the specified name.
153e5dd7070Spatrick    Assumes that the output file name is specified in the command line args."""
154e5dd7070Spatrick    replaceidx = None
155e5dd7070Spatrick    attached = False
156e5dd7070Spatrick    for idx, val in enumerate(args):
157e5dd7070Spatrick        if val == '-o':
158e5dd7070Spatrick            replaceidx = idx + 1
159e5dd7070Spatrick            attached = False
160e5dd7070Spatrick        elif val.startswith('-o'):
161e5dd7070Spatrick            replaceidx = idx
162e5dd7070Spatrick            attached = True
163e5dd7070Spatrick
164e5dd7070Spatrick    if replaceidx is None:
165e5dd7070Spatrick        raise Exception
166e5dd7070Spatrick    replacement = new_name
167e5dd7070Spatrick    if attached == True:
168e5dd7070Spatrick        replacement = '-o' + new_name
169e5dd7070Spatrick    args[replaceidx] = replacement
170e5dd7070Spatrick    return args
171e5dd7070Spatrick
172e5dd7070Spatrickdef add_output_file(args, output_file):
173e5dd7070Spatrick    """Append an output file to args, presuming not already specified."""
174e5dd7070Spatrick    return args + ['-o', output_file]
175e5dd7070Spatrick
176e5dd7070Spatrickdef set_output_file(args, output_file):
177e5dd7070Spatrick    """Set the output file within the arguments. Appends or replaces as
178e5dd7070Spatrick    appropriate."""
179e5dd7070Spatrick    if is_output_specified(args):
180e5dd7070Spatrick        args = replace_output_file(args, output_file)
181e5dd7070Spatrick    else:
182e5dd7070Spatrick        args = add_output_file(args, output_file)
183e5dd7070Spatrick    return args
184e5dd7070Spatrick
185e5dd7070SpatrickgSrcFileSuffixes = ('.c', '.cpp', '.cxx', '.c++', '.cp', '.cc')
186e5dd7070Spatrick
187e5dd7070Spatrickdef get_input_file(args):
188e5dd7070Spatrick    """Return the input file string if it can be found (and there is only
189e5dd7070Spatrick    one)."""
190e5dd7070Spatrick    inputFiles = list()
191e5dd7070Spatrick    for arg in args:
192e5dd7070Spatrick        testarg = arg
193e5dd7070Spatrick        quotes = ('"', "'")
194e5dd7070Spatrick        while testarg.endswith(quotes):
195e5dd7070Spatrick            testarg = testarg[:-1]
196e5dd7070Spatrick        testarg = os.path.normcase(testarg)
197e5dd7070Spatrick
198e5dd7070Spatrick        # Test if it is a source file
199e5dd7070Spatrick        if testarg.endswith(gSrcFileSuffixes):
200e5dd7070Spatrick            inputFiles.append(arg)
201e5dd7070Spatrick    if len(inputFiles) == 1:
202e5dd7070Spatrick        return inputFiles[0]
203e5dd7070Spatrick    else:
204e5dd7070Spatrick        return None
205e5dd7070Spatrick
206e5dd7070Spatrickdef set_input_file(args, input_file):
207e5dd7070Spatrick    """Replaces the input file with that specified."""
208e5dd7070Spatrick    infile = get_input_file(args)
209e5dd7070Spatrick    if infile:
210e5dd7070Spatrick        infile_idx = args.index(infile)
211e5dd7070Spatrick        args[infile_idx] = input_file
212e5dd7070Spatrick        return args
213e5dd7070Spatrick    else:
214e5dd7070Spatrick        # Could not find input file
215e5dd7070Spatrick        assert False
216e5dd7070Spatrick
217e5dd7070Spatrickdef is_normal_compile(args):
218e5dd7070Spatrick    """Check if this is a normal compile which will output an object file rather
219e5dd7070Spatrick    than a preprocess or link. args is a list of command line arguments."""
220e5dd7070Spatrick    compile_step = '-c' in args
221e5dd7070Spatrick    # Bitcode cannot be disassembled in the same way
222e5dd7070Spatrick    bitcode = '-flto' in args or '-emit-llvm' in args
223e5dd7070Spatrick    # Version and help are queries of the compiler and override -c if specified
224e5dd7070Spatrick    query = '--version' in args or '--help' in args
225e5dd7070Spatrick    # Options to output dependency files for make
226e5dd7070Spatrick    dependency = '-M' in args or '-MM' in args
227e5dd7070Spatrick    # Check if the input is recognised as a source file (this may be too
228e5dd7070Spatrick    # strong a restriction)
229e5dd7070Spatrick    input_is_valid = bool(get_input_file(args))
230e5dd7070Spatrick    return compile_step and not bitcode and not query and not dependency and input_is_valid
231e5dd7070Spatrick
232e5dd7070Spatrickdef run_step(command, my_env, error_on_failure):
233e5dd7070Spatrick    """Runs a step of the compilation. Reports failure as exception."""
234e5dd7070Spatrick    # Need to use shell=True on Windows as Popen won't use PATH otherwise.
235e5dd7070Spatrick    p = subprocess.Popen(command, stdout=subprocess.PIPE,
236e5dd7070Spatrick                         stderr=subprocess.PIPE, env=my_env, shell=is_windows())
237e5dd7070Spatrick    (stdout, stderr) = p.communicate()
238e5dd7070Spatrick    if p.returncode != 0:
239e5dd7070Spatrick        raise WrapperStepException(error_on_failure, stdout, stderr)
240e5dd7070Spatrick
241e5dd7070Spatrickdef get_temp_file_name(suffix):
242e5dd7070Spatrick    """Get a temporary file name with a particular suffix. Let the caller be
243e5dd7070Spatrick    responsible for deleting it."""
244e5dd7070Spatrick    tf = tempfile.NamedTemporaryFile(suffix=suffix, delete=False)
245e5dd7070Spatrick    tf.close()
246e5dd7070Spatrick    return tf.name
247e5dd7070Spatrick
248e5dd7070Spatrickclass WrapperCheck(object):
249e5dd7070Spatrick    """Base class for a check. Subclass this to add a check."""
250e5dd7070Spatrick    def __init__(self, output_file_a):
251e5dd7070Spatrick        """Record the base output file that will be compared against."""
252e5dd7070Spatrick        self._output_file_a = output_file_a
253e5dd7070Spatrick
254e5dd7070Spatrick    def perform_check(self, arguments, my_env):
255e5dd7070Spatrick        """Override this to perform the modified compilation and required
256e5dd7070Spatrick        checks."""
257e5dd7070Spatrick        raise NotImplementedError("Please Implement this method")
258e5dd7070Spatrick
259e5dd7070Spatrickclass dash_g_no_change(WrapperCheck):
260e5dd7070Spatrick    def perform_check(self, arguments, my_env):
261e5dd7070Spatrick        """Check if different code is generated with/without the -g flag."""
262e5dd7070Spatrick        output_file_b = get_temp_file_name('.o')
263e5dd7070Spatrick
264e5dd7070Spatrick        alternate_command = list(arguments)
265e5dd7070Spatrick        alternate_command = flip_dash_g(alternate_command)
266e5dd7070Spatrick        alternate_command = set_output_file(alternate_command, output_file_b)
267e5dd7070Spatrick        run_step(alternate_command, my_env, "Error compiling with -g")
268e5dd7070Spatrick
269e5dd7070Spatrick        # Compare disassembly (returns first diff if differs)
270e5dd7070Spatrick        difference = obj_diff.compare_object_files(self._output_file_a,
271e5dd7070Spatrick                                                   output_file_b)
272e5dd7070Spatrick        if difference:
273e5dd7070Spatrick            raise WrapperCheckException(
274e5dd7070Spatrick                "Code difference detected with -g\n{}".format(difference))
275e5dd7070Spatrick
276e5dd7070Spatrick        # Clean up temp file if comparison okay
277e5dd7070Spatrick        os.remove(output_file_b)
278e5dd7070Spatrick
279e5dd7070Spatrickclass dash_s_no_change(WrapperCheck):
280e5dd7070Spatrick    def perform_check(self, arguments, my_env):
281e5dd7070Spatrick        """Check if compiling to asm then assembling in separate steps results
282e5dd7070Spatrick        in different code than compiling to object directly."""
283e5dd7070Spatrick        output_file_b = get_temp_file_name('.o')
284e5dd7070Spatrick
285e5dd7070Spatrick        alternate_command = arguments + ['-via-file-asm']
286e5dd7070Spatrick        alternate_command = set_output_file(alternate_command, output_file_b)
287e5dd7070Spatrick        run_step(alternate_command, my_env,
288e5dd7070Spatrick                 "Error compiling with -via-file-asm")
289e5dd7070Spatrick
290e5dd7070Spatrick        # Compare if object files are exactly the same
291e5dd7070Spatrick        exactly_equal = obj_diff.compare_exact(self._output_file_a, output_file_b)
292e5dd7070Spatrick        if not exactly_equal:
293e5dd7070Spatrick            # Compare disassembly (returns first diff if differs)
294e5dd7070Spatrick            difference = obj_diff.compare_object_files(self._output_file_a,
295e5dd7070Spatrick                                                       output_file_b)
296e5dd7070Spatrick            if difference:
297e5dd7070Spatrick                raise WrapperCheckException(
298e5dd7070Spatrick                    "Code difference detected with -S\n{}".format(difference))
299e5dd7070Spatrick
300e5dd7070Spatrick            # Code is identical, compare debug info
301e5dd7070Spatrick            dbgdifference = obj_diff.compare_debug_info(self._output_file_a,
302e5dd7070Spatrick                                                        output_file_b)
303e5dd7070Spatrick            if dbgdifference:
304e5dd7070Spatrick                raise WrapperCheckException(
305e5dd7070Spatrick                    "Debug info difference detected with -S\n{}".format(dbgdifference))
306e5dd7070Spatrick
307e5dd7070Spatrick            raise WrapperCheckException("Object files not identical with -S\n")
308e5dd7070Spatrick
309e5dd7070Spatrick        # Clean up temp file if comparison okay
310e5dd7070Spatrick        os.remove(output_file_b)
311e5dd7070Spatrick
312e5dd7070Spatrickif __name__ == '__main__':
313e5dd7070Spatrick    # Create configuration defaults from list of checks
314e5dd7070Spatrick    default_config = """
315e5dd7070Spatrick[Checks]
316e5dd7070Spatrick"""
317e5dd7070Spatrick
318e5dd7070Spatrick    # Find all subclasses of WrapperCheck
319e5dd7070Spatrick    checks = [cls.__name__ for cls in vars()['WrapperCheck'].__subclasses__()]
320e5dd7070Spatrick
321e5dd7070Spatrick    for c in checks:
322e5dd7070Spatrick        default_config += "{} = false\n".format(c)
323e5dd7070Spatrick
324e5dd7070Spatrick    config = configparser.RawConfigParser()
325e5dd7070Spatrick    config.readfp(io.BytesIO(default_config))
326e5dd7070Spatrick    scriptdir = get_main_dir()
327e5dd7070Spatrick    config_path = os.path.join(scriptdir, 'check_cfc.cfg')
328e5dd7070Spatrick    try:
329e5dd7070Spatrick        config.read(os.path.join(config_path))
330e5dd7070Spatrick    except:
331e5dd7070Spatrick        print("Could not read config from {}, "
332e5dd7070Spatrick              "using defaults.".format(config_path))
333e5dd7070Spatrick
334e5dd7070Spatrick    my_env = os.environ.copy()
335e5dd7070Spatrick    my_env['PATH'] = path_without_wrapper()
336e5dd7070Spatrick
337e5dd7070Spatrick    arguments_a = list(sys.argv)
338e5dd7070Spatrick
339e5dd7070Spatrick    # Prevent infinite loop if called with absolute path.
340e5dd7070Spatrick    arguments_a[0] = os.path.basename(arguments_a[0])
341e5dd7070Spatrick
342*12c85518Srobert    # Basic correctness check
343e5dd7070Spatrick    enabled_checks = [check_name
344e5dd7070Spatrick                      for check_name in checks
345e5dd7070Spatrick                      if config.getboolean('Checks', check_name)]
346e5dd7070Spatrick    checks_comma_separated = ', '.join(enabled_checks)
347e5dd7070Spatrick    print("Check CFC, checking: {}".format(checks_comma_separated))
348e5dd7070Spatrick
349e5dd7070Spatrick    # A - original compilation
350e5dd7070Spatrick    output_file_orig = get_output_file(arguments_a)
351e5dd7070Spatrick    if output_file_orig is None:
352e5dd7070Spatrick        output_file_orig = derive_output_file(arguments_a)
353e5dd7070Spatrick
354e5dd7070Spatrick    p = subprocess.Popen(arguments_a, env=my_env, shell=is_windows())
355e5dd7070Spatrick    p.communicate()
356e5dd7070Spatrick    if p.returncode != 0:
357e5dd7070Spatrick        sys.exit(p.returncode)
358e5dd7070Spatrick
359e5dd7070Spatrick    if not is_normal_compile(arguments_a) or output_file_orig is None:
360e5dd7070Spatrick        # Bail out here if we can't apply checks in this case.
361e5dd7070Spatrick        # Does not indicate an error.
362e5dd7070Spatrick        # Maybe not straight compilation (e.g. -S or --version or -flto)
363e5dd7070Spatrick        # or maybe > 1 input files.
364e5dd7070Spatrick        sys.exit(0)
365e5dd7070Spatrick
366e5dd7070Spatrick    # Sometimes we generate files which have very long names which can't be
367e5dd7070Spatrick    # read/disassembled. This will exit early if we can't find the file we
368e5dd7070Spatrick    # expected to be output.
369e5dd7070Spatrick    if not os.path.isfile(output_file_orig):
370e5dd7070Spatrick        sys.exit(0)
371e5dd7070Spatrick
372e5dd7070Spatrick    # Copy output file to a temp file
373e5dd7070Spatrick    temp_output_file_orig = get_temp_file_name('.o')
374e5dd7070Spatrick    shutil.copyfile(output_file_orig, temp_output_file_orig)
375e5dd7070Spatrick
376e5dd7070Spatrick    # Run checks, if they are enabled in config and if they are appropriate for
377e5dd7070Spatrick    # this command line.
378e5dd7070Spatrick    current_module = sys.modules[__name__]
379e5dd7070Spatrick    for check_name in checks:
380e5dd7070Spatrick        if config.getboolean('Checks', check_name):
381e5dd7070Spatrick            class_ = getattr(current_module, check_name)
382e5dd7070Spatrick            checker = class_(temp_output_file_orig)
383e5dd7070Spatrick            try:
384e5dd7070Spatrick                checker.perform_check(arguments_a, my_env)
385e5dd7070Spatrick            except WrapperCheckException as e:
386e5dd7070Spatrick                # Check failure
387e5dd7070Spatrick                print("{} {}".format(get_input_file(arguments_a), e.msg), file=sys.stderr)
388e5dd7070Spatrick
389e5dd7070Spatrick                # Remove file to comply with build system expectations (no
390e5dd7070Spatrick                # output file if failed)
391e5dd7070Spatrick                os.remove(output_file_orig)
392e5dd7070Spatrick                sys.exit(1)
393e5dd7070Spatrick
394e5dd7070Spatrick            except WrapperStepException as e:
395e5dd7070Spatrick                # Compile step failure
396e5dd7070Spatrick                print(e.msg, file=sys.stderr)
397e5dd7070Spatrick                print("*** stdout ***", file=sys.stderr)
398e5dd7070Spatrick                print(e.stdout, file=sys.stderr)
399e5dd7070Spatrick                print("*** stderr ***", file=sys.stderr)
400e5dd7070Spatrick                print(e.stderr, file=sys.stderr)
401e5dd7070Spatrick
402e5dd7070Spatrick                # Remove file to comply with build system expectations (no
403e5dd7070Spatrick                # output file if failed)
404e5dd7070Spatrick                os.remove(output_file_orig)
405e5dd7070Spatrick                sys.exit(1)
406