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