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