1#!/usr/bin/env python3 2 3import os, re, shutil 4from . import settings, utils, cppcheck_report_utils, exclusion_file_list 5from .exclusion_file_list import (ExclusionFileListError, 6 cppcheck_exclusion_file_list) 7 8class GetMakeVarsPhaseError(Exception): 9 pass 10 11class CppcheckDepsPhaseError(Exception): 12 pass 13 14class CppcheckReportPhaseError(Exception): 15 pass 16 17CPPCHECK_BUILD_DIR = "build-dir-cppcheck" 18CPPCHECK_HTMLREPORT_OUTDIR = "cppcheck-htmlreport" 19CPPCHECK_REPORT_OUTDIR = "cppcheck-report" 20cppcheck_extra_make_args = "" 21xen_cc = "" 22 23def get_make_vars(): 24 global xen_cc 25 invoke_make = utils.invoke_command( 26 "make -C {} {} export-variable-CC" 27 .format(settings.xen_dir, settings.make_forward_args), 28 True, GetMakeVarsPhaseError, 29 "Error occured retrieving make vars:\n{}" 30 ) 31 32 cc_var_regex = re.search('^CC=(.*)$', invoke_make, flags=re.M) 33 if cc_var_regex: 34 xen_cc = cc_var_regex.group(1) 35 36 if xen_cc == "": 37 raise GetMakeVarsPhaseError("CC variable not found in Xen make output") 38 39 40def __generate_suppression_list(out_file): 41 # The following lambda function will return a file if it contains lines with 42 # a comment containing "cppcheck-suppress[*]" on a single line. 43 grep_action = lambda x: utils.grep(x, 44 r'^.*/\* cppcheck-suppress\[(?P<id>.*)\] \*/$') 45 # Look for a list of .h files that matches the condition above 46 headers = utils.recursive_find_file(settings.xen_dir, r'.*\.h$', 47 grep_action) 48 49 try: 50 with open(out_file, "wt") as supplist_file: 51 # Add this rule to skip every finding in the autogenerated 52 # header for cppcheck 53 supplist_file.write("*:*generated/compiler-def.h\n") 54 55 try: 56 exclusion_file = \ 57 "{}/docs/misra/exclude-list.json".format(settings.repo_dir) 58 exclusion_list = cppcheck_exclusion_file_list(exclusion_file) 59 except ExclusionFileListError as e: 60 raise CppcheckDepsPhaseError( 61 "Issue with reading file {}: {}".format(exclusion_file, e) 62 ) 63 64 # Append excluded files to the suppression list, using * as error id 65 # to suppress every findings on that file 66 for path in exclusion_list: 67 supplist_file.write("*:{}\n".format(path)) 68 69 for entry in headers: 70 filename = entry["file"] 71 try: 72 with open(filename, "rt") as infile: 73 header_content = infile.readlines() 74 except OSError as e: 75 raise CppcheckDepsPhaseError( 76 "Issue with reading file {}: {}" 77 .format(filename, e) 78 ) 79 header_lines_len = len(header_content) 80 # line_num in entry will be header_content[line_num-1], here we 81 # are going to search the first line after line_num that have 82 # anything different from comments or empty line, because the 83 # in-code comment suppression is related to that line then. 84 for line_num in entry["matches"]: 85 cppcheck_violation_id = "" 86 tmp_line = line_num 87 # look up to which line is referring the comment at 88 # line_num (which would be header_content[tmp_line-1]) 89 comment_section = False 90 while tmp_line < header_lines_len: 91 line = header_content[tmp_line] 92 # Matches a line with just optional spaces/tabs and the 93 # start of a comment '/*' 94 comment_line_starts = re.match(r'^[ \t]*/\*.*$', line) 95 # Matches a line with text and the end of a comment '*/' 96 comment_line_stops = re.match(r'^.*\*/$', line) 97 if (not comment_section) and comment_line_starts: 98 comment_section = True 99 if (len(line.strip()) != 0) and (not comment_section): 100 cppcheck_violation_id = entry["matches"][line_num]['id'] 101 break 102 if comment_section and comment_line_stops: 103 comment_section = False 104 tmp_line = tmp_line + 1 105 106 if cppcheck_violation_id == "": 107 raise CppcheckDepsPhaseError( 108 "Error matching cppcheck comment in {} at line {}." 109 .format(filename, line_num) 110 ) 111 # Write [error id]:[filename]:[line] 112 # tmp_line refers to the array index, so translated to the 113 # file line (that begins with 1) it is tmp_line+1 114 supplist_file.write( 115 "{}:{}:{}\n".format(cppcheck_violation_id, filename, 116 (tmp_line + 1)) 117 ) 118 except OSError as e: 119 raise CppcheckDepsPhaseError("Issue with writing file {}: {}" 120 .format(out_file, e)) 121 122 123def generate_cppcheck_deps(): 124 global cppcheck_extra_make_args 125 126 # Compile flags to pass to cppcheck: 127 # - include config.h as this is passed directly to the compiler. 128 # - define CPPCHECK as we use it to disable or enable some specific part of 129 # the code to solve some cppcheck issues. 130 # - explicitely enable some cppcheck checks as we do not want to use "all" 131 # which includes unusedFunction which gives wrong positives as we check 132 # file per file. 133 # - Explicitly suppress warnings on compiler-def.h because cppcheck throws 134 # an unmatchedSuppression due to the rule we put in suppression-list.txt 135 # to skip every finding in the file. 136 # - Explicitly suppress findings for unusedStructMember that is not very 137 # reliable and causes lots of false positives. 138 # 139 # Compiler defines are in compiler-def.h which is included in config.h 140 # 141 cppcheck_flags=""" 142 --max-ctu-depth=10 143 --enable=style,information,missingInclude 144 --template=\'{{file}}({{line}},{{column}}):{{id}}:{{severity}}:{{message}}\' 145 --relative-paths={} 146 --inline-suppr 147 --suppressions-list={}/suppression-list.txt 148 --suppress='unmatchedSuppression:*' 149 --suppress='unusedStructMember:*' 150 --include={}/include/xen/config.h 151 -DCPPCHECK 152""".format(settings.repo_dir, settings.outdir, settings.xen_dir) 153 154 invoke_cppcheck = utils.invoke_command( 155 "{} --version".format(settings.cppcheck_binpath), 156 True, CppcheckDepsPhaseError, 157 "Error occured retrieving cppcheck version:\n{}\n\n{}" 158 ) 159 160 version_regex = re.search(r'^Cppcheck (\d+)\.(\d+)(?:\.\d+)?$', 161 invoke_cppcheck, flags=re.M) 162 # Currently, only cppcheck version >= 2.7 is supported, but version 2.8 is 163 # known to be broken, please refer to docs/misra/cppcheck.txt 164 if (not version_regex) or len(version_regex.groups()) < 2: 165 raise CppcheckDepsPhaseError( 166 "Can't find cppcheck version or version not identified: " 167 "{}".format(invoke_cppcheck) 168 ) 169 version = (int(version_regex.group(1)), int(version_regex.group(2))) 170 if version < (2, 7) or version == (2, 8): 171 raise CppcheckDepsPhaseError( 172 "Cppcheck version < 2.7 or 2.8 are not supported" 173 ) 174 175 # If misra option is selected, append misra addon and generate cppcheck 176 # files for misra analysis 177 if settings.cppcheck_misra: 178 cppcheck_flags = cppcheck_flags + " --addon=cppcheck-misra.json" 179 180 skip_rules_arg = "" 181 if settings.cppcheck_skip_rules != "": 182 skip_rules_arg = "-s {}".format(settings.cppcheck_skip_rules) 183 184 utils.invoke_command( 185 "{}/convert_misra_doc.py -i {}/docs/misra/rules.rst" 186 " -o {}/cppcheck-misra.txt -j {}/cppcheck-misra.json {}" 187 .format(settings.tools_dir, settings.repo_dir, 188 settings.outdir, settings.outdir, skip_rules_arg), 189 False, CppcheckDepsPhaseError, 190 "An error occured when running:\n{}" 191 ) 192 193 # Generate compiler macros 194 os.makedirs("{}/include/generated".format(settings.outdir), exist_ok=True) 195 utils.invoke_command( 196 "{} -dM -E -o \"{}/include/generated/compiler-def.h\" - < /dev/null" 197 .format(xen_cc, settings.outdir), 198 False, CppcheckDepsPhaseError, 199 "An error occured when running:\n{}" 200 ) 201 202 # Generate cppcheck suppression list 203 __generate_suppression_list( 204 "{}/suppression-list.txt".format(settings.outdir)) 205 206 # Generate cppcheck build folder 207 os.makedirs("{}/{}".format(settings.outdir, CPPCHECK_BUILD_DIR), 208 exist_ok=True) 209 210 cppcheck_cc_flags = """--compiler={} --cppcheck-cmd={} {} 211 --cppcheck-plat={}/cppcheck-plat --ignore-path=tools/ 212 --ignore-path=arch/x86/efi/check.c --build-dir={}/{} 213""".format(xen_cc, settings.cppcheck_binpath, cppcheck_flags, 214 settings.tools_dir, settings.outdir, CPPCHECK_BUILD_DIR) 215 216 if settings.cppcheck_html: 217 cppcheck_cc_flags = cppcheck_cc_flags + " --cppcheck-html" 218 219 # Generate the extra make argument to pass the cppcheck-cc.sh wrapper as CC 220 cppcheck_extra_make_args = "CC=\"{}/cppcheck-cc.sh {} --\"".format( 221 settings.tools_dir, 222 cppcheck_cc_flags 223 ).replace("\n", "") 224 225 226def generate_cppcheck_report(): 227 # Prepare text report 228 # Look for a list of .cppcheck.txt files, those are the txt report 229 # fragments 230 fragments = utils.recursive_find_file(settings.outdir, r'.*\.cppcheck.txt$') 231 text_report_dir = "{}/{}".format(settings.outdir, 232 CPPCHECK_REPORT_OUTDIR) 233 report_filename = "{}/xen-cppcheck.txt".format(text_report_dir) 234 os.makedirs(text_report_dir, exist_ok=True) 235 try: 236 cppcheck_report_utils.cppcheck_merge_txt_fragments(fragments, 237 report_filename, 238 [settings.repo_dir]) 239 except cppcheck_report_utils.CppcheckTXTReportError as e: 240 raise CppcheckReportPhaseError(e) 241 242 # If HTML output is requested 243 if settings.cppcheck_html: 244 # Look for a list of .cppcheck.xml files, those are the XML report 245 # fragments 246 fragments = utils.recursive_find_file(settings.outdir, 247 r'.*\.cppcheck.xml$') 248 html_report_dir = "{}/{}".format(settings.outdir, 249 CPPCHECK_HTMLREPORT_OUTDIR) 250 xml_filename = "{}/xen-cppcheck.xml".format(html_report_dir) 251 os.makedirs(html_report_dir, exist_ok=True) 252 try: 253 cppcheck_report_utils.cppcheck_merge_xml_fragments(fragments, 254 xml_filename, 255 settings.repo_dir, 256 settings.outdir) 257 except cppcheck_report_utils.CppcheckHTMLReportError as e: 258 raise CppcheckReportPhaseError(e) 259 # Call cppcheck-htmlreport utility to generate the HTML output 260 utils.invoke_command( 261 "{} --file={} --source-dir={} --report-dir={}/html --title=Xen" 262 .format(settings.cppcheck_htmlreport_binpath, xml_filename, 263 settings.repo_dir, html_report_dir), 264 False, CppcheckReportPhaseError, 265 "Error occured generating Cppcheck HTML report:\n{}" 266 ) 267 # Strip src and obj path from *.html files 268 html_files = utils.recursive_find_file(html_report_dir, r'.*\.html$') 269 try: 270 cppcheck_report_utils.cppcheck_strip_path_html(html_files, 271 (settings.repo_dir, 272 settings.outdir)) 273 except cppcheck_report_utils.CppcheckHTMLReportError as e: 274 raise CppcheckReportPhaseError(e) 275 276 277def clean_analysis_artifacts(): 278 clean_files = ("suppression-list.txt", "cppcheck-misra.txt", 279 "cppcheck-misra.json") 280 cppcheck_build_dir = "{}/{}".format(settings.outdir, CPPCHECK_BUILD_DIR) 281 if os.path.isdir(cppcheck_build_dir): 282 shutil.rmtree(cppcheck_build_dir) 283 artifact_files = utils.recursive_find_file(settings.outdir, 284 r'.*\.(?:c\.json|cppcheck\.txt|cppcheck\.xml)$') 285 for file in clean_files: 286 file = "{}/{}".format(settings.outdir, file) 287 if os.path.isfile(file): 288 artifact_files.append(file) 289 for delfile in artifact_files: 290 os.remove(delfile) 291 292 293def clean_reports(): 294 text_report_dir = "{}/{}".format(settings.outdir, 295 CPPCHECK_REPORT_OUTDIR) 296 html_report_dir = "{}/{}".format(settings.outdir, 297 CPPCHECK_HTMLREPORT_OUTDIR) 298 if os.path.isdir(text_report_dir): 299 shutil.rmtree(text_report_dir) 300 if os.path.isdir(html_report_dir): 301 shutil.rmtree(html_report_dir) 302