3 """Check CFC - Check Compile Flow Consistency
5 This is a compiler wrapper for testing that code generation is consistent with
6 different compilation processes. It checks that code is not unduly affected by
7 compiler options or other changes which should not have side effects.
10 -Ensure that the compiler under test (i.e. clang, clang++) is on the PATH
11 -On Linux copy this script to the name of the compiler
12 e.g. cp check_cfc.py clang && cp check_cfc.py clang++
13 -On Windows use setup.py to generate check_cfc.exe and copy that to clang.exe
15 -Enable the desired checks in check_cfc.cfg (in the same directory as the
19 dash_g_no_change = true
20 dash_s_no_change = false
22 -The wrapper can be run using its absolute path or added to PATH before the
24 e.g. export PATH=<path to check_cfc>:$PATH
25 -Compile as normal. The wrapper intercepts normal -c compiles and will return
26 non-zero if the check fails.
29 Code difference detected with -g
33 - 0: 48 8b 05 51 0b 20 00 mov 0x200b51(%rip),%rax
34 + 0: 48 39 3d 51 0b 20 00 cmp %rdi,0x200b51(%rip)
36 -To run LNT with Check CFC specify the absolute path to the wrapper to the --cc
39 lnt runtest nt --cc <path to check_cfc>/clang \\
40 --cxx <path to check_cfc>/clang++ ...
43 -Create a new subclass of WrapperCheck
44 -Implement the perform_check() method. This should perform the alternate compile
45 and do the comparison.
46 -Add the new check to check_cfc.cfg. The check has the same name as the
50 from __future__
import absolute_import
, division
, print_function
63 import ConfigParser
as configparser
70 """Returns True if running on Windows."""
71 return platform
.system() == "Windows"
74 class WrapperStepException(Exception):
75 """Exception type to be used when a step other than the original compile
78 def __init__(self
, msg
, stdout
, stderr
):
84 class WrapperCheckException(Exception):
85 """Exception type to be used when a comparison check fails."""
87 def __init__(self
, msg
):
92 """Returns True when running as a py2exe executable."""
94 hasattr(sys
, "frozen")
95 or hasattr(sys
, "importers") # new py2exe
96 or imp
.is_frozen("__main__") # old py2exe
101 """Get the directory that the script or executable is located in."""
103 return os
.path
.dirname(sys
.executable
)
104 return os
.path
.dirname(sys
.argv
[0])
107 def remove_dir_from_path(path_var
, directory
):
108 """Remove the specified directory from path_var, a string representing
110 pathlist
= path_var
.split(os
.pathsep
)
111 norm_directory
= os
.path
.normpath(os
.path
.normcase(directory
))
113 x
for x
in pathlist
if os
.path
.normpath(os
.path
.normcase(x
)) != norm_directory
115 return os
.pathsep
.join(pathlist
)
118 def path_without_wrapper():
119 """Returns the PATH variable modified to remove the path to this program."""
120 scriptdir
= get_main_dir()
121 path
= os
.environ
["PATH"]
122 return remove_dir_from_path(path
, scriptdir
)
125 def flip_dash_g(args
):
126 """Search for -g in args. If it exists then return args without. If not then
129 # Return args without any -g
130 return [x
for x
in args
if x
!= "-g"]
136 def derive_output_file(args
):
137 """Derive output file from the input file (if just one) or None
139 infile
= get_input_file(args
)
143 return "{}.o".format(os
.path
.splitext(infile
)[0])
146 def get_output_file(args
):
147 """Return the output file specified by this command or None if not
154 # Specified as a separate arg
156 elif arg
.startswith("-o"):
157 # Specified conjoined with -o
159 assert grabnext
== False
164 def is_output_specified(args
):
165 """Return true is output file is specified in args."""
166 return get_output_file(args
) is not None
169 def replace_output_file(args
, new_name
):
170 """Replaces the specified name of an output file with the specified name.
171 Assumes that the output file name is specified in the command line args."""
174 for idx
, val
in enumerate(args
):
178 elif val
.startswith("-o"):
182 if replaceidx
is None:
184 replacement
= new_name
186 replacement
= "-o" + new_name
187 args
[replaceidx
] = replacement
191 def add_output_file(args
, output_file
):
192 """Append an output file to args, presuming not already specified."""
193 return args
+ ["-o", output_file
]
196 def set_output_file(args
, output_file
):
197 """Set the output file within the arguments. Appends or replaces as
199 if is_output_specified(args
):
200 args
= replace_output_file(args
, output_file
)
202 args
= add_output_file(args
, output_file
)
206 gSrcFileSuffixes
= (".c", ".cpp", ".cxx", ".c++", ".cp", ".cc")
209 def get_input_file(args
):
210 """Return the input file string if it can be found (and there is only
216 while testarg
.endswith(quotes
):
217 testarg
= testarg
[:-1]
218 testarg
= os
.path
.normcase(testarg
)
220 # Test if it is a source file
221 if testarg
.endswith(gSrcFileSuffixes
):
222 inputFiles
.append(arg
)
223 if len(inputFiles
) == 1:
229 def set_input_file(args
, input_file
):
230 """Replaces the input file with that specified."""
231 infile
= get_input_file(args
)
233 infile_idx
= args
.index(infile
)
234 args
[infile_idx
] = input_file
237 # Could not find input file
241 def is_normal_compile(args
):
242 """Check if this is a normal compile which will output an object file rather
243 than a preprocess or link. args is a list of command line arguments."""
244 compile_step
= "-c" in args
245 # Bitcode cannot be disassembled in the same way
246 bitcode
= "-flto" in args
or "-emit-llvm" in args
247 # Version and help are queries of the compiler and override -c if specified
248 query
= "--version" in args
or "--help" in args
249 # Options to output dependency files for make
250 dependency
= "-M" in args
or "-MM" in args
251 # Check if the input is recognised as a source file (this may be too
252 # strong a restriction)
253 input_is_valid
= bool(get_input_file(args
))
255 compile_step
and not bitcode
and not query
and not dependency
and input_is_valid
259 def run_step(command
, my_env
, error_on_failure
):
260 """Runs a step of the compilation. Reports failure as exception."""
261 # Need to use shell=True on Windows as Popen won't use PATH otherwise.
262 p
= subprocess
.Popen(
264 stdout
=subprocess
.PIPE
,
265 stderr
=subprocess
.PIPE
,
269 (stdout
, stderr
) = p
.communicate()
270 if p
.returncode
!= 0:
271 raise WrapperStepException(error_on_failure
, stdout
, stderr
)
274 def get_temp_file_name(suffix
):
275 """Get a temporary file name with a particular suffix. Let the caller be
276 responsible for deleting it."""
277 tf
= tempfile
.NamedTemporaryFile(suffix
=suffix
, delete
=False)
282 class WrapperCheck(object):
283 """Base class for a check. Subclass this to add a check."""
285 def __init__(self
, output_file_a
):
286 """Record the base output file that will be compared against."""
287 self
._output
_file
_a
= output_file_a
289 def perform_check(self
, arguments
, my_env
):
290 """Override this to perform the modified compilation and required
292 raise NotImplementedError("Please Implement this method")
295 class dash_g_no_change(WrapperCheck
):
296 def perform_check(self
, arguments
, my_env
):
297 """Check if different code is generated with/without the -g flag."""
298 output_file_b
= get_temp_file_name(".o")
300 alternate_command
= list(arguments
)
301 alternate_command
= flip_dash_g(alternate_command
)
302 alternate_command
= set_output_file(alternate_command
, output_file_b
)
303 run_step(alternate_command
, my_env
, "Error compiling with -g")
305 # Compare disassembly (returns first diff if differs)
306 difference
= obj_diff
.compare_object_files(self
._output
_file
_a
, output_file_b
)
308 raise WrapperCheckException(
309 "Code difference detected with -g\n{}".format(difference
)
312 # Clean up temp file if comparison okay
313 os
.remove(output_file_b
)
316 class dash_s_no_change(WrapperCheck
):
317 def perform_check(self
, arguments
, my_env
):
318 """Check if compiling to asm then assembling in separate steps results
319 in different code than compiling to object directly."""
320 output_file_b
= get_temp_file_name(".o")
322 alternate_command
= arguments
+ ["-via-file-asm"]
323 alternate_command
= set_output_file(alternate_command
, output_file_b
)
324 run_step(alternate_command
, my_env
, "Error compiling with -via-file-asm")
326 # Compare if object files are exactly the same
327 exactly_equal
= obj_diff
.compare_exact(self
._output
_file
_a
, output_file_b
)
328 if not exactly_equal
:
329 # Compare disassembly (returns first diff if differs)
330 difference
= obj_diff
.compare_object_files(
331 self
._output
_file
_a
, output_file_b
334 raise WrapperCheckException(
335 "Code difference detected with -S\n{}".format(difference
)
338 # Code is identical, compare debug info
339 dbgdifference
= obj_diff
.compare_debug_info(
340 self
._output
_file
_a
, output_file_b
343 raise WrapperCheckException(
344 "Debug info difference detected with -S\n{}".format(dbgdifference
)
347 raise WrapperCheckException("Object files not identical with -S\n")
349 # Clean up temp file if comparison okay
350 os
.remove(output_file_b
)
353 if __name__
== "__main__":
354 # Create configuration defaults from list of checks
359 # Find all subclasses of WrapperCheck
360 checks
= [cls
.__name
__ for cls
in vars()["WrapperCheck"].__subclasses
__()]
363 default_config
+= "{} = false\n".format(c
)
365 config
= configparser
.RawConfigParser()
366 config
.readfp(io
.BytesIO(default_config
))
367 scriptdir
= get_main_dir()
368 config_path
= os
.path
.join(scriptdir
, "check_cfc.cfg")
370 config
.read(os
.path
.join(config_path
))
372 print("Could not read config from {}, " "using defaults.".format(config_path
))
374 my_env
= os
.environ
.copy()
375 my_env
["PATH"] = path_without_wrapper()
377 arguments_a
= list(sys
.argv
)
379 # Prevent infinite loop if called with absolute path.
380 arguments_a
[0] = os
.path
.basename(arguments_a
[0])
382 # Basic correctness check
384 check_name
for check_name
in checks
if config
.getboolean("Checks", check_name
)
386 checks_comma_separated
= ", ".join(enabled_checks
)
387 print("Check CFC, checking: {}".format(checks_comma_separated
))
389 # A - original compilation
390 output_file_orig
= get_output_file(arguments_a
)
391 if output_file_orig
is None:
392 output_file_orig
= derive_output_file(arguments_a
)
394 p
= subprocess
.Popen(arguments_a
, env
=my_env
, shell
=is_windows())
396 if p
.returncode
!= 0:
397 sys
.exit(p
.returncode
)
399 if not is_normal_compile(arguments_a
) or output_file_orig
is None:
400 # Bail out here if we can't apply checks in this case.
401 # Does not indicate an error.
402 # Maybe not straight compilation (e.g. -S or --version or -flto)
403 # or maybe > 1 input files.
406 # Sometimes we generate files which have very long names which can't be
407 # read/disassembled. This will exit early if we can't find the file we
408 # expected to be output.
409 if not os
.path
.isfile(output_file_orig
):
412 # Copy output file to a temp file
413 temp_output_file_orig
= get_temp_file_name(".o")
414 shutil
.copyfile(output_file_orig
, temp_output_file_orig
)
416 # Run checks, if they are enabled in config and if they are appropriate for
418 current_module
= sys
.modules
[__name__
]
419 for check_name
in checks
:
420 if config
.getboolean("Checks", check_name
):
421 class_
= getattr(current_module
, check_name
)
422 checker
= class_(temp_output_file_orig
)
424 checker
.perform_check(arguments_a
, my_env
)
425 except WrapperCheckException
as e
:
428 "{} {}".format(get_input_file(arguments_a
), e
.msg
), file=sys
.stderr
431 # Remove file to comply with build system expectations (no
432 # output file if failed)
433 os
.remove(output_file_orig
)
436 except WrapperStepException
as e
:
437 # Compile step failure
438 print(e
.msg
, file=sys
.stderr
)
439 print("*** stdout ***", file=sys
.stderr
)
440 print(e
.stdout
, file=sys
.stderr
)
441 print("*** stderr ***", file=sys
.stderr
)
442 print(e
.stderr
, file=sys
.stderr
)
444 # Remove file to comply with build system expectations (no
445 # output file if failed)
446 os
.remove(output_file_orig
)