-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathsoftwipe.py
More file actions
executable file
·492 lines (406 loc) · 22.2 KB
/
softwipe.py
File metadata and controls
executable file
·492 lines (406 loc) · 22.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
#!/usr/bin/env python3
"""
The main module of softwipe. Here, command line arguments get parsed and the pipeline gets started.
"""
import argparse
import os
import re
import sys
import time
import itertools
from multiprocessing.pool import ThreadPool
import automatic_tool_installation
import compile_phase
import execution_phase
import scoring
import strings
import util
from analysis_tools import CppcheckTool, ClangTool, ClangTidyTool, KWStyleTool, LizardTool, AssertionTool, InferTool, \
ValgrindTool, TestCountTool
def parse_arguments():
"""
Parse command line arguments.
:return: The "args" Namespace that contains the command line arguments specified by the user.
"""
# Preparser, used for the command, execute, and compiler options file helps. Without the preparser, one would get
# an error because 'programdir' is a required argument but is missing. With the preparser, the help can be
# printed anyway.
preparser = argparse.ArgumentParser(add_help=False)
preparser.add_argument('--commandfilehelp', default=False, action='store_true')
preparser.add_argument('--executefilehelp', default=False, action='store_true')
preparser.add_argument('--compileroptionsfilehelp', default=False, action='store_true')
preargs, _ = preparser.parse_known_args()
# All helps can be printed at once
if preargs.executefilehelp:
print(strings.EXECUTE_FILE_HELP)
if preargs.commandfilehelp:
print(strings.COMMAND_FILE_HELP)
if preargs.compileroptionsfilehelp:
print(strings.COMPILER_OPTIONS_FILE_HELP)
if preargs.executefilehelp or preargs.commandfilehelp or preargs.compileroptionsfilehelp:
# Exit if either one, any of the, or all helps have been printed
sys.exit(0)
# Main parser
parser = argparse.ArgumentParser(description='Check the software quality of a C/C++ program\n\n'
'Important arguments you probably want to use:\n'
' -c/-C to tell me whether your program is C or C++\n'
' -M/-m/-l to tell me how to build your program (cmake, make, '
'raw clang)\n'
' -e to specify a file that tells me how to execute your program\n'
'Example command line for a CMake-based C++ program:\n'
'./softwipe.py -CM path/to/program -e path/to/executefile\n',
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('programdir', help="the root directory of your target program")
c = parser.add_mutually_exclusive_group()
c.add_argument('-c', '--cc', action='store_true', help='use C. This is the default option')
c.add_argument('-C', '--cpp', action='store_true', help='use C++')
mode = parser.add_mutually_exclusive_group()
mode.add_argument('-M', '--cmake', action='store_true', help='compile the program using cmake. This is the default '
'option')
mode.add_argument('-m', '--make', action='store_true', help='compile the program using make. Note that this '
'option requires a "standard" style makefile that '
'uses common variable names like ${CC}, ${CFLAGS}, '
'${LDFLAGS} etc. to work properly')
mode.add_argument('-l', '--clang', nargs='+', metavar='target', help='compile the program using the clang/clang++ '
'compiler. This option takes as arguments the'
' files to compile')
parser.add_argument('-e', '--executefile', nargs=1, help='path to an "execute file" which contains a command line '
'that executes your program')
parser.add_argument('--executefilehelp', action='store_true', help='print detailed information about how the '
'execute file works and exit')
parser.add_argument('-f', '--commandfile', nargs=1, help='path to a "command file" which can be used to provide '
'commands that should be executed for building a '
'make-based project')
parser.add_argument('--commandfilehelp', action='store_true', help='print detailed information about how the '
'command file works and exit')
parser.add_argument('-o', '--compileroptionsfile', nargs=1, help='path to a "compiler options file" which '
'contains one line with options that must be '
'passed to the compiler for correct compilation '
'of your program')
parser.add_argument("-O", nargs=1, help="path to a file containing necessary options to successfully run cmake"
"(works similar to the '-o' or '--compileroptionsfile' argument)")
parser.add_argument('--compileroptionsfilehelp', action='store_true', help='print detailed information about how '
'the compiler options file works and '
'exit')
parser.add_argument('-x', '--exclude', nargs=1, help='a comma separated list of files and directories that should '
'be excluded from being analyzed by this program. If you '
'specify relative paths, they should be relative to the '
'target program path.',
action="append")
parser.add_argument('-X', nargs=1, help='a file containing a list of files and directories that '
'should be excluded from the analysis. Each file and directory in the list '
'has to be noted in a separate line. The paths in the list have to be '
'absolute, or relative to the target program path.')
parser.add_argument('-p', '--path', nargs=1, help='a comma separated lst of paths that should be added to the '
'PATH environment variable. Use this if you have a dependency '
'installed but not accessible via your default PATH')
parser.add_argument('--no-execution', action='store_true', help='Do not execute your program. This skips the '
'clang sanitizer check')
parser.add_argument('-a', '--custom-assert', nargs=1, help='a comma separated lst of custom assertions that '
'might be used in the code. Can be used to correct '
'your assertion score if you only/mostly use custom '
'assertion functions rather than raw C ones')
parser.add_argument('--allow-running-as-root', action='store_true', help='Do not print a warning if the user is '
'root')
parser.add_argument('--add-badge', nargs=1)
parser.add_argument('--exclude-assertions', action='store_true', help='Excludes the counting of assertions')
# parser.add_argument('--exclude-infer', action='store_true', help='Excludes Infer from the analysis')
parser.add_argument('--use-infer', action='store_true', help='Uses Infer static analysis. This requires a '
'lengthy download for the first usage.')
parser.add_argument('--exclude-compilation', action='store_true',
help='Excludes the compilation of the program from the analysis')
parser.add_argument('--exclude-lizard', action='store_true', help='Excludes Lizard from the analysis')
parser.add_argument('--exclude-cppcheck', action='store_true', help='Excludes Cppcheck from the analysis')
parser.add_argument('--exclude-kwstyle', action='store_true', help='Excludes KWStyle from the analysis')
parser.add_argument('--exclude-clang-tidy', action='store_true', help='Excludes Clang-Tidy from the analysis')
args = parser.parse_args()
return args
def add_to_path_variable(paths):
"""
Add paths to the system PATH environment variable.
:param paths: A comma separated lst of paths to add.
"""
path_list = []
for path in paths.split(','):
path_list.append(path)
for path in path_list:
os.environ['PATH'] += os.pathsep + path
def add_kwstyle_to_path_variable():
"""
Adjusts the PATH variable by adding KWStyle to the PATH if it is contained in the softwipe directory (which it is
if the user did the auto-installation of it).
"""
kwstyle_dir = os.path.join(util.get_softwipe_directory(), 'KWStyle')
if os.path.isdir(kwstyle_dir):
add_to_path_variable(os.path.join(kwstyle_dir, strings.SOFTWIPE_BUILD_DIR_NAME))
else:
automatic_tool_installation.handle_kwstyle_download()
add_to_path_variable(os.path.join(kwstyle_dir, strings.SOFTWIPE_BUILD_DIR_NAME))
#add_kwstyle_to_path_variable()
def add_lizard_to_path_variable():
# TODO: fix versioning
lizard_dir = os.path.join(util.get_softwipe_directory(), 'lizard-1.17.7')
if os.path.isdir(lizard_dir):
add_to_path_variable(lizard_dir)
else:
automatic_tool_installation.handle_lizard_download()
add_to_path_variable(lizard_dir)
#add_lizard_to_path_variable()
def add_infer_to_path_variable():
# TODO: fix versioning
infer_dir = os.path.join(util.get_softwipe_directory(), 'infer-linux64-v0.17.0')
infer_dir = os.path.join(infer_dir, "lib/infer/infer/bin")
automatic_tool_installation.install_apt_package_if_needed("libtinfo5")
if os.path.isdir(infer_dir):
add_to_path_variable(infer_dir)
else:
automatic_tool_installation.handle_infer_download()
#add_infer_to_path_variable()
def add_user_paths_to_path_variable(args):
"""
Adjusts the PATH variable if necessary by adding user specified paths (if any were specified) to the PATH.
:param args: The "args" Namespace as returned from parse_arguments().
"""
user_paths = args.path[0] if args.path else None
if user_paths:
add_to_path_variable(user_paths)
def warn_if_user_is_root():
"""
Check if the user is root, and print a warning if he is.
"""
if os.geteuid() == 0: # if user is root
print(strings.USER_IS_ROOT_WARNING)
while True:
user_in = input('>>> ')
if user_in in ('Y', 'Yes'):
print("Okay, running as root now!")
break
elif user_in in ('n', 'no'):
sys.exit(1)
else:
print('Please answer with "Y" (Yes) or "n" (no)!')
def compile_program(args, lines_of_code, cpp, compiler_flags, excluded_paths):
"""
Run the automatic compilation of the target project.
:param args: The "args" Namespace as returned from parse_arguments().
:param lines_of_code: The lines of pure code count.
:param cpp: Whether C++ is used or not. True if C++, False if C.
:param compiler_flags: The flags to be used for compilation. Typically, these should be strings.COMPILE_FLAGS or,
if no_execution, strings.COMPILER_WARNING_FLAGS.
:param excluded_paths: A tuple containing the paths to be excluded.
:return: The compiler score.
"""
print(strings.RUN_COMPILER_HEADER)
program_dir_abs = os.path.abspath(args.programdir)
command_file = args.commandfile
if args.O:
additional_args = open(args.O[0], 'r').read().rstrip().split()
else:
additional_args = []
if args.make:
if command_file:
score = compile_phase.compile_program_make(program_dir_abs, lines_of_code, compiler_flags, excluded_paths,
make_command_file=command_file[0])
else:
score = compile_phase.compile_program_make(program_dir_abs, lines_of_code, compiler_flags, excluded_paths)
elif args.clang:
score = compile_phase.compile_program_clang(program_dir_abs, args.clang, lines_of_code, compiler_flags,
excluded_paths, cpp)
else:
if command_file:
score = compile_phase.compile_program_cmake(program_dir_abs, lines_of_code, compiler_flags, excluded_paths,
make_command_file=command_file[0],
additional_args=additional_args)
else:
score = compile_phase.compile_program_cmake(program_dir_abs, lines_of_code, compiler_flags, excluded_paths,
additional_args=additional_args)
return score
def compile_program_with_infer(args, excluded_paths):
"""
Calls Infer compilation functions depending on the arguments received.
:param args: softwipe arguments
:param excluded_paths: paths to exclude from infer analysis
:return: true - if compilation successful
false - if compilation is not successful
"""
program_dir_abs = os.path.abspath(args.programdir)
if args.cmake:
infer_compilation_status = compile_phase.compile_program_infer_cmake(program_dir_abs, excluded_paths)
elif args.make:
infer_compilation_status = compile_phase.compile_program_infer_make(program_dir_abs, excluded_paths)
else:
# TODO: allow clang compilation as well!!!
print("Only make/cmake supported to analyze the program with Infer right now!")
infer_compilation_status = False
return infer_compilation_status
def execute_program(program_dir_abs, executefile, cmake, lines_of_code):
"""
Execute the program and parse the output of the clang sanitizers.
:param program_dir_abs: The absolute path to the root directory of the target program.
:param executefile: The executefile that contains a command line for executing the program.
:param cmake: Whether CMake has been used for compilation or not.
:param lines_of_code: The lines of pure code count.
:return The weighted sanitizer error count.
"""
try:
weighted_error_count = execution_phase.run_execution(program_dir_abs, executefile, cmake, lines_of_code)
except execution_phase.ExecutionFailedException:
print(strings.WARNING_PROGRAM_EXECUTION_SKIPPED)
weighted_error_count = 0
return weighted_error_count
def compile_and_execute_program_with_sanitizers(args, lines_of_code, program_dir_abs, cpp, excluded_paths,
no_exec=False):
"""
Automatically compile and execute the program
:param args: The "args" Namespace as returned from parse_arguments().
:param lines_of_code: The lines of pure code count.
:param program_dir_abs: The absolute path to the root directory of the target program.
:param cpp: Whether C++ is used or not. True if C++, False if C.
:param excluded_paths: A tuple containing the paths to be excluded.
:param no_exec: If True, skip execution of the program.
:return The compiler + sanitizer score.
"""
compiler_flags = strings.COMPILER_WARNING_FLAGS if no_exec else strings.COMPILE_FLAGS
if args.compileroptionsfile:
options = open(args.compileroptionsfile[0], 'r').read().rstrip()
compiler_flags += " " + options
weighted_sum_of_compiler_warnings = compile_program(args, lines_of_code, cpp, compiler_flags, excluded_paths)
if not no_exec:
execute_file = args.executefile[0] if args.executefile else None
weighted_sum_of_sanitizer_warnings = execute_program(program_dir_abs, execute_file, args.cmake, lines_of_code)
else:
weighted_sum_of_sanitizer_warnings = 0
print(strings.WARNING_PROGRAM_EXECUTION_SKIPPED)
weighted_warning_rate = (weighted_sum_of_compiler_warnings + weighted_sum_of_sanitizer_warnings) / lines_of_code
score = scoring.calculate_compiler_and_sanitizer_score(weighted_warning_rate)
scoring.print_score(score, 'Compiler + Sanitizer')
return score
def add_badge_to_file(path, overall_score):
# TODO: Clean and test this function
"""
Experimental function to add a softwipe score badge to a github readme.
:param path: path of the readme file
:param overall_score: softwipe score received by the project
"""
badge_string = strings.BADGE_LINK.format(round(overall_score, 1))
lines = ""
output = ""
badge_set = False
with open(path, 'r') as file:
for line in file:
lines += line
if "[![Softwipe Score]" in lines:
for line in lines.split("\n"):
if "[![Softwipe Score]" in line:
line = re.sub(r'\[!\[Softwipe Score\]\(([^\)\]]+)\)\]\(([^\)\]]+)\)', badge_string,
line.rstrip()) + "\n"
output += line
elif "[![" in lines:
for line in lines.split("\n"):
if "[![" in line and not badge_set:
badge_set = True
line += badge_string
output += line
else:
for line in lines.split("\n"):
output += line
if not badge_set:
badge_set = True
output += badge_string + "\n"
with open(path, 'w') as modified:
modified.write(output)
def main():
"""
Main function: Runs compilation, static analysis and prints results.
"""
# add_kwstyle_to_path_variable() # TODO: hopefully get a conda package for this sometime
# add_lizard_to_path_variable()
# Allow the user to auto-install the dependencies by just running "./softwipe.py" without any arguments
# Should not be needed if conda is used. TODO: maybe remove this
if len(sys.argv) == 1:
automatic_tool_installation.check_if_all_required_tools_are_installed()
args = parse_arguments()
for argument in sys.argv:
print(argument, end=" ")
print()
# Normal check for the dependencies
if len(sys.argv) != 1:
automatic_tool_installation.check_if_all_required_tools_are_installed()
# if args.use_infer:
# add_infer_to_path_variable()
add_user_paths_to_path_variable(args)
if not args.allow_running_as_root:
warn_if_user_is_root()
use_cpp = args.cpp
use_cmake = args.cmake
use_make = args.make
program_dir_abs = os.path.abspath(args.programdir)
if args.exclude:
exclude = list(itertools.chain.from_iterable(args.exclude))
exclude = ",".join(exclude)
else:
exclude = None
exclude_file = args.X[0] if args.X else None
excluded_paths = util.get_excluded_paths(program_dir_abs, exclude, exclude_file)
custom_asserts = args.custom_assert[0].split(',') if args.custom_assert else None
source_files = util.find_all_source_files(program_dir_abs, excluded_paths)
lines_of_code = util.count_lines_of_code(source_files)
analysis_tools = [] # TODO: maybe add valgrind at some point if we get its error counts normalized somehow
all_scores = []
data = {"program_dir_abs": program_dir_abs,
"args": args,
"excluded_paths": excluded_paths,
"use_cpp": use_cpp,
"use_cmake": use_cmake,
"use_make": use_make,
"custom_asserts": custom_asserts,
"source_files": source_files,
"lines_of_code": lines_of_code,
"executefile": args.executefile
}
"""t1 = time.perf_counter()"""
if not args.exclude_compilation:
compiler_and_sanitizer_score = compile_and_execute_program_with_sanitizers(
args, lines_of_code, program_dir_abs, use_cpp, excluded_paths, args.no_execution)
all_scores.append(compiler_and_sanitizer_score)
if args.use_infer: # TODO: maybe completely remove Infer since it requires a lot of disk space
add_infer_to_path_variable()
analysis_tools.append(InferTool)
"""t2 = time.perf_counter()
print("Compilation time: {}s".format(t2 - t1))
sys.exit()"""
if not args.exclude_assertions:
analysis_tools.append(AssertionTool)
if not args.exclude_clang_tidy:
analysis_tools.append(ClangTidyTool)
if not args.exclude_cppcheck:
analysis_tools.append(CppcheckTool)
if not args.exclude_lizard:
analysis_tools.append(LizardTool)
if not args.exclude_kwstyle:
analysis_tools.append(KWStyleTool)
analysis_tools.append(TestCountTool)
thread_pool = ThreadPool(processes=len(analysis_tools))
instances = []
outs = []
"""t1 = time.perf_counter()"""
for i in range(len(analysis_tools)):
instances.append(thread_pool.apply_async(analysis_tools[i].run, (data, )))
for i in range(len(instances)):
outs.append(instances[i].get())
for i in range(len(outs)):
scores, log, success = outs[i]
if success:
print(log)
all_scores.extend(scores)
else:
print("excluded {} from analysis\n".format(analysis_tools[i].name()))
"""t2 = time.perf_counter()
print("Time static analysis: {}s".format(t2 - t1))"""
overall_score = scoring.average_score(all_scores)
scoring.print_score(overall_score, 'Overall program absolute')
if args.add_badge:
add_badge_to_file(args.add_badge[0], overall_score)
print("Added badge to file {}".format(args.add_badge[0]))
if __name__ == "__main__":
main()