Leonardo Sandoval | d05adab | 2020-08-10 14:01:54 -0500 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | # |
Karl Zhang | 08681e6 | 2020-10-30 13:56:03 +0800 | [diff] [blame] | 3 | # Copyright (c) 2019-2020, Arm Limited. All rights reserved. |
Leonardo Sandoval | d05adab | 2020-08-10 14:01:54 -0500 | [diff] [blame] | 4 | # |
| 5 | # SPDX-License-Identifier: BSD-3-Clause |
| 6 | # |
| 7 | |
| 8 | # |
| 9 | # Run the Coverity tool on the Trusted Firmware and produce a tarball ready to |
| 10 | # be submitted to Coverity Scan Online. |
| 11 | # |
| 12 | |
| 13 | import sys |
| 14 | import argparse |
| 15 | import urllib.request |
| 16 | import tarfile |
| 17 | import os |
| 18 | import subprocess |
| 19 | import re |
Leonardo Sandoval | c503d7e | 2020-08-10 15:47:11 -0500 | [diff] [blame] | 20 | |
| 21 | # local libraries |
| 22 | sys.path.append(os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), "script", "tf-coverity"))) |
Leonardo Sandoval | d05adab | 2020-08-10 14:01:54 -0500 | [diff] [blame] | 23 | import utils |
| 24 | import coverity_tf_conf |
| 25 | |
| 26 | |
| 27 | def tarball_name(filename): |
| 28 | "Isolate the tarball name without the filename's extension." |
| 29 | # Handle a selection of "composite" extensions |
| 30 | for ext in [".tar.gz", ".tar.bz2"]: |
| 31 | if filename.endswith(ext): |
| 32 | return filename[:-len(ext)] |
| 33 | # For all other extensions, let the vanilla splitext() function handle it |
| 34 | return os.path.splitext(filename)[0] |
| 35 | |
| 36 | assert tarball_name("foo.gz") == "foo" |
| 37 | assert tarball_name("bar.tar.gz") == "bar" |
| 38 | assert tarball_name("baz.tar.bz2") == "baz" |
| 39 | |
| 40 | |
| 41 | def get_coverity_tool(): |
| 42 | coverity_tarball = "cov-analysis-linux64-2019.03.tar.gz" |
| 43 | url = "http://files.oss.arm.com/downloads/tf-a/" + coverity_tarball |
| 44 | print("Downloading Coverity Build tool from %s..." % url) |
| 45 | file_handle = urllib.request.urlopen(url) |
| 46 | output = open(coverity_tarball, "wb") |
| 47 | output.write(file_handle.read()) |
| 48 | output.close() |
| 49 | print("Download complete.") |
| 50 | |
| 51 | print("\nUnpacking tarball %s..." % coverity_tarball) |
| 52 | tarfile.open(coverity_tarball).extractall() |
| 53 | print("Tarball unpacked.") |
| 54 | |
| 55 | print("\nNow please load the Coverity tool in your PATH...") |
| 56 | print("E.g.:") |
| 57 | cov_dir_name = tarball_name(coverity_tarball) |
| 58 | cov_dir_path = os.path.abspath(os.path.join(cov_dir_name, "bin")) |
| 59 | print(" export PATH=%s$PATH" % (cov_dir_path + os.pathsep)) |
| 60 | |
| 61 | # Patch is needed for coverity version 2019.03 |
| 62 | patch_file = os.path.abspath(os.path.join(__file__, os.pardir, "cov-2019.03-fix.patch")) |
| 63 | cov_file = os.path.abspath(os.path.join(cov_dir_name, "config", |
| 64 | "templates", "gnu", "compiler-compat-arm-intrin.h")) |
| 65 | print("Patching file") |
| 66 | print(cov_file) |
| 67 | utils.exec_prog("patch", [cov_file, "-i", patch_file], |
| 68 | out=subprocess.PIPE, out_text_mode=True) |
| 69 | |
| 70 | def print_coverage(coverity_dir, tf_dir, exclude_paths=[], log_filename=None): |
| 71 | analyzed = [] |
| 72 | not_analyzed = [] |
| 73 | excluded = [] |
| 74 | |
| 75 | # Print the coverage report to a file (or stdout if no file is specified) |
| 76 | if log_filename is not None: |
| 77 | log_file = open(log_filename, "w") |
| 78 | else: |
| 79 | log_file = sys.stdout |
| 80 | |
| 81 | # Get the list of files analyzed by Coverity. |
| 82 | # |
| 83 | # To do that, we examine the build log file Coverity generated and look for |
| 84 | # compilation lines. These are the lines starting with "COMPILING:" or |
| 85 | # "EXECUTING:". We consider only those lines that actually compile C files, |
| 86 | # i.e. lines of the form: |
| 87 | # gcc -c file.c -o file.o |
| 88 | # This filters out other compilation lines like generation of dependency files |
| 89 | # (*.d) and such. |
| 90 | # We then extract the C filename. |
| 91 | coverity_build_log = os.path.join(coverity_dir, "build-log.txt") |
| 92 | with open(coverity_build_log, encoding="utf-8") as build_log: |
| 93 | for line in build_log: |
| 94 | line = re.sub('//','/', line) |
Leonardo Sandoval | c503d7e | 2020-08-10 15:47:11 -0500 | [diff] [blame] | 95 | results = re.search("(?:COMPILING|EXECUTING):.*-o.*\.o .*-c (.*\.c)", line) |
Leonardo Sandoval | d05adab | 2020-08-10 14:01:54 -0500 | [diff] [blame] | 96 | if results is not None: |
| 97 | filename = results.group(1) |
| 98 | if filename not in analyzed: |
| 99 | analyzed.append(filename) |
| 100 | |
| 101 | # Now get the list of C files in the Trusted Firmware source tree. |
| 102 | # Header files and assembly files are ignored, as well as anything that |
| 103 | # matches the patterns list in the exclude_paths[] list. |
| 104 | # Build a list of files that are in this source tree but were not analyzed |
| 105 | # by comparing the 2 sets of files. |
| 106 | all_files_count = 0 |
| 107 | old_cwd = os.path.abspath(os.curdir) |
| 108 | os.chdir(tf_dir) |
| 109 | git_process = utils.exec_prog("git", ["ls-files", "*.c"], |
| 110 | out=subprocess.PIPE, out_text_mode=True) |
| 111 | for filename in git_process.stdout: |
| 112 | # Remove final \n in filename |
| 113 | filename = filename.strip() |
| 114 | |
Leonardo Sandoval | c503d7e | 2020-08-10 15:47:11 -0500 | [diff] [blame] | 115 | # Expand to absolute path |
| 116 | filename = os.path.abspath(filename) |
| 117 | |
Leonardo Sandoval | d05adab | 2020-08-10 14:01:54 -0500 | [diff] [blame] | 118 | def is_excluded(filename, excludes): |
| 119 | for pattern in excludes: |
| 120 | if re.match(pattern[0], filename): |
| 121 | excluded.append((filename, pattern[1])) |
| 122 | return True |
| 123 | return False |
| 124 | |
| 125 | if is_excluded(filename, exclude_paths): |
| 126 | continue |
| 127 | |
| 128 | # Keep track of the number of C files in the source tree. Used to |
| 129 | # compute the coverage percentage at the end. |
| 130 | all_files_count += 1 |
| 131 | if filename not in analyzed: |
| 132 | not_analyzed.append(filename) |
| 133 | os.chdir(old_cwd) |
| 134 | |
| 135 | # Compute the coverage percentage |
| 136 | # Note: The 1.0 factor here is used to make a float division instead of an |
| 137 | # integer one. |
| 138 | percentage = (1 - ((1.0 * len(not_analyzed) ) / all_files_count)) * 100 |
| 139 | |
| 140 | # |
| 141 | # Print a report |
| 142 | # |
| 143 | log_file.write("Files coverage: %d%%\n\n" % percentage) |
| 144 | log_file.write("Analyzed %d files\n" % len(analyzed)) |
| 145 | |
| 146 | if len(excluded) > 0: |
| 147 | log_file.write("\n%d files were ignored on purpose:\n" % len(excluded)) |
| 148 | for exc in excluded: |
| 149 | log_file.write(" - {0:50} (Reason: {1})\n".format(exc[0], exc[1])) |
| 150 | |
Leonardo Sandoval | c503d7e | 2020-08-10 15:47:11 -0500 | [diff] [blame] | 151 | if len(analyzed) > 0: |
| 152 | log_file.write("\n%d files analyzed:\n" % len(analyzed)) |
| 153 | for f in analyzed: |
| 154 | log_file.write(" - %s\n" % f) |
| 155 | |
Leonardo Sandoval | d05adab | 2020-08-10 14:01:54 -0500 | [diff] [blame] | 156 | if len(not_analyzed) > 0: |
| 157 | log_file.write("\n%d files were not analyzed:\n" % len(not_analyzed)) |
| 158 | for f in not_analyzed: |
| 159 | log_file.write(" - %s\n" % f) |
| 160 | log_file.write(""" |
| 161 | =============================================================================== |
| 162 | Please investigate why the above files are not run through Coverity. |
| 163 | |
| 164 | There are 2 possible reasons: |
| 165 | |
| 166 | 1) The build coverage is insufficient. Please review the tf-cov-make script to |
| 167 | add the missing build config(s) that will involve the file in the build. |
| 168 | |
| 169 | 2) The file is expected to be ignored, for example because it is deprecated |
| 170 | code. Please update the TF Coverity configuration to list the file and |
| 171 | indicate the reason why it is safe to ignore it. |
| 172 | =============================================================================== |
| 173 | """) |
| 174 | log_file.close() |
| 175 | |
| 176 | |
| 177 | def parse_cmd_line(argv, prog_name): |
| 178 | parser = argparse.ArgumentParser( |
| 179 | prog=prog_name, |
| 180 | description="Run Coverity on Trusted Firmware", |
| 181 | epilog=""" |
| 182 | Please ensure the AArch64 & AArch32 cross-toolchains are loaded in your |
| 183 | PATH. Ditto for the Coverity tools. If you don't have the latter then |
| 184 | you can use the --get-coverity-tool to download them for you. |
| 185 | """) |
| 186 | parser.add_argument("--tf", default=None, |
| 187 | metavar="<Trusted Firmware source dir>", |
| 188 | help="Specify the location of ARM Trusted Firmware sources to analyze") |
| 189 | parser.add_argument("--get-coverity-tool", default=False, |
| 190 | help="Download the Coverity build tool and exit", |
| 191 | action="store_true") |
| 192 | parser.add_argument("--mode", choices=["offline", "online"], default="online", |
| 193 | help="Choose between online or offline mode for the analysis") |
| 194 | parser.add_argument("--output", "-o", |
| 195 | help="Name of the output file containing the results of the analysis") |
| 196 | parser.add_argument("--build-cmd", "-b", |
| 197 | help="Command used to build TF through Coverity") |
| 198 | parser.add_argument("--analysis-profile", "-p", |
| 199 | action="append", nargs=1, |
| 200 | help="Analysis profile for a local analysis") |
| 201 | args = parser.parse_args(argv) |
| 202 | |
| 203 | # Set a default name for the output file if none is provided. |
| 204 | # If running in offline mode, this will be a text file; |
| 205 | # If running in online mode, this will be a tarball name. |
| 206 | if not args.output: |
| 207 | if args.mode == "offline": |
| 208 | args.output = "arm-tf-coverity-report.txt" |
| 209 | else: |
| 210 | args.output = "arm-tf-coverity-results.tgz" |
| 211 | |
| 212 | return args |
| 213 | |
| 214 | |
| 215 | if __name__ == "__main__": |
| 216 | prog_name = sys.argv[0] |
| 217 | args = parse_cmd_line(sys.argv[1:], prog_name) |
| 218 | |
| 219 | # If the user asked to download the Coverity build tool then just do that |
| 220 | # and exit. |
| 221 | if args.get_coverity_tool: |
| 222 | # If running locally, use the commercial version of Coverity from the |
| 223 | # EUHPC cluster. |
| 224 | if args.mode == "offline": |
| 225 | print("To load the Coverity tools, use the following command:") |
| 226 | print("export PATH=/arm/tools/coverity/static-analysis/8.7.1/bin/:$PATH") |
| 227 | else: |
| 228 | get_coverity_tool() |
| 229 | sys.exit(0) |
| 230 | |
| 231 | if args.tf is None: |
| 232 | print("ERROR: Please specify the Trusted Firmware sources using the --tf option.", |
| 233 | file=sys.stderr) |
| 234 | sys.exit(1) |
| 235 | |
| 236 | # Get some important paths in the platform-ci scripts |
Leonardo Sandoval | c503d7e | 2020-08-10 15:47:11 -0500 | [diff] [blame] | 237 | tf_root_dir = os.path.abspath(os.path.dirname(prog_name)) |
Leonardo Sandoval | d05adab | 2020-08-10 14:01:54 -0500 | [diff] [blame] | 238 | |
| 239 | if not args.build_cmd: |
Leonardo Sandoval | c503d7e | 2020-08-10 15:47:11 -0500 | [diff] [blame] | 240 | args.build_cmd = os.path.join(tf_root_dir, "script", "tf-coverity", "tf-cov-make") |
Leonardo Sandoval | d05adab | 2020-08-10 14:01:54 -0500 | [diff] [blame] | 241 | |
Leonardo Sandoval | c503d7e | 2020-08-10 15:47:11 -0500 | [diff] [blame] | 242 | run_coverity_script = os.path.join(tf_root_dir, "coverity", "run_coverity.sh") |
Leonardo Sandoval | d05adab | 2020-08-10 14:01:54 -0500 | [diff] [blame] | 243 | |
| 244 | ret = subprocess.call([run_coverity_script, "check_tools", args.mode]) |
| 245 | if ret != 0: |
| 246 | sys.exit(1) |
| 247 | |
| 248 | ret = subprocess.call([run_coverity_script, "configure"]) |
| 249 | if ret != 0: |
| 250 | sys.exit(1) |
| 251 | |
| 252 | ret = subprocess.call([run_coverity_script, "build", args.build_cmd]) |
| 253 | if ret != 0: |
| 254 | sys.exit(1) |
| 255 | |
| 256 | if args.mode == "online": |
| 257 | ret = subprocess.call([run_coverity_script, "package", args.output]) |
| 258 | else: |
| 259 | for profile in args.analysis_profile: |
| 260 | ret = subprocess.call([run_coverity_script, "analyze", |
| 261 | args.output, |
| 262 | args.tf, |
| 263 | profile[0]]) |
| 264 | if ret != 0: |
| 265 | break |
| 266 | if ret != 0: |
| 267 | print("An error occured (%d)." % ret, file=sys.stderr) |
| 268 | sys.exit(ret) |
| 269 | |
| 270 | print("-----------------------------------------------------------------") |
| 271 | print("Results can be found in file '%s'" % args.output) |
| 272 | if args.mode == "online": |
| 273 | print("This tarball can be uploaded at Coverity Scan Online:" ) |
| 274 | print("https://scan.coverity.com/projects/arm-software-arm-trusted-firmware/builds/new?tab=upload") |
| 275 | print("-----------------------------------------------------------------") |
| 276 | |
| 277 | print_coverage("cov-int", args.tf, coverity_tf_conf.exclude_paths, "tf_coverage.log") |
| 278 | with open("tf_coverage.log") as log_file: |
| 279 | for line in log_file: |
| 280 | print(line, end="") |