blob: 14250d2b9cf72a7e1acb9c993c7f4a52fea0a7d1 [file] [log] [blame]
Darryl Green7c2dd582018-03-01 14:53:49 +00001#!/usr/bin/env python3
Darryl Green78696802018-04-06 11:23:22 +01002"""
3This file is part of Mbed TLS (https://tls.mbed.org)
4
5Copyright (c) 2018, Arm Limited, All Rights Reserved
6
7Purpose
8
9This script is a small wrapper around the abi-compliance-checker and
10abi-dumper tools, applying them to compare the ABI and API of the library
11files from two different Git revisions within an Mbed TLS repository.
12The results of the comparison are formatted as HTML and stored at
13a configurable location. Returns 0 on success, 1 on ABI/API non-compliance,
14and 2 if there is an error while running the script.
15Note: requires Python 3, must be run from Mbed TLS root.
16"""
Darryl Green7c2dd582018-03-01 14:53:49 +000017
18import os
19import sys
20import traceback
21import shutil
22import subprocess
23import argparse
24import logging
25import tempfile
26
27
28class AbiChecker(object):
29
30 def __init__(self, report_dir, old_rev, new_rev, keep_all_reports):
31 self.repo_path = "."
32 self.log = None
33 self.setup_logger()
34 self.report_dir = os.path.abspath(report_dir)
35 self.keep_all_reports = keep_all_reports
36 self.should_keep_report_dir = os.path.isdir(self.report_dir)
37 self.old_rev = old_rev
38 self.new_rev = new_rev
39 self.mbedtls_modules = ["libmbedcrypto", "libmbedtls", "libmbedx509"]
40 self.old_dumps = {}
41 self.new_dumps = {}
42 self.git_command = "git"
43 self.make_command = "make"
44
45 def check_repo_path(self):
Darryl Greena6f430f2018-03-15 10:12:06 +000046 current_dir = os.path.realpath('.')
47 root_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
48 if current_dir != root_dir:
Darryl Green7c2dd582018-03-01 14:53:49 +000049 raise Exception("Must be run from Mbed TLS root")
50
51 def setup_logger(self):
52 self.log = logging.getLogger()
53 self.log.setLevel(logging.INFO)
54 self.log.addHandler(logging.StreamHandler())
55
56 def check_abi_tools_are_installed(self):
57 for command in ["abi-dumper", "abi-compliance-checker"]:
58 if not shutil.which(command):
59 raise Exception("{} not installed, aborting".format(command))
60
61 def get_clean_worktree_for_git_revision(self, git_rev):
62 self.log.info(
63 "Checking out git worktree for revision {}".format(git_rev)
64 )
65 git_worktree_path = tempfile.mkdtemp()
66 worktree_process = subprocess.Popen(
67 [self.git_command, "worktree", "add", git_worktree_path, git_rev],
68 cwd=self.repo_path,
69 stdout=subprocess.PIPE,
70 stderr=subprocess.STDOUT
71 )
72 worktree_output, _ = worktree_process.communicate()
73 self.log.info(worktree_output.decode("utf-8"))
74 if worktree_process.returncode != 0:
75 raise Exception("Checking out worktree failed, aborting")
76 return git_worktree_path
77
78 def build_shared_libraries(self, git_worktree_path):
79 my_environment = os.environ.copy()
80 my_environment["CFLAGS"] = "-g -Og"
81 my_environment["SHARED"] = "1"
82 make_process = subprocess.Popen(
83 self.make_command,
84 env=my_environment,
85 cwd=git_worktree_path,
86 stdout=subprocess.PIPE,
87 stderr=subprocess.STDOUT
88 )
89 make_output, _ = make_process.communicate()
90 self.log.info(make_output.decode("utf-8"))
91 if make_process.returncode != 0:
92 raise Exception("make failed, aborting")
93
94 def get_abi_dumps_from_shared_libraries(self, git_ref, git_worktree_path):
95 abi_dumps = {}
96 for mbed_module in self.mbedtls_modules:
97 output_path = os.path.join(
98 self.report_dir, "{}-{}.dump".format(mbed_module, git_ref)
99 )
100 abi_dump_command = [
101 "abi-dumper",
102 os.path.join(
103 git_worktree_path, "library", mbed_module + ".so"),
104 "-o", output_path,
105 "-lver", git_ref
106 ]
107 abi_dump_process = subprocess.Popen(
108 abi_dump_command,
109 stdout=subprocess.PIPE,
110 stderr=subprocess.STDOUT
111 )
112 abi_dump_output, _ = abi_dump_process.communicate()
113 self.log.info(abi_dump_output.decode("utf-8"))
114 if abi_dump_process.returncode != 0:
115 raise Exception("abi-dumper failed, aborting")
116 abi_dumps[mbed_module] = output_path
117 return abi_dumps
118
119 def cleanup_worktree(self, git_worktree_path):
120 shutil.rmtree(git_worktree_path)
121 worktree_process = subprocess.Popen(
122 [self.git_command, "worktree", "prune"],
123 cwd=self.repo_path,
124 stdout=subprocess.PIPE,
125 stderr=subprocess.STDOUT
126 )
127 worktree_output, _ = worktree_process.communicate()
128 self.log.info(worktree_output.decode("utf-8"))
129 if worktree_process.returncode != 0:
130 raise Exception("Worktree cleanup failed, aborting")
131
132 def get_abi_dump_for_ref(self, git_rev):
133 git_worktree_path = self.get_clean_worktree_for_git_revision(git_rev)
134 self.build_shared_libraries(git_worktree_path)
135 abi_dumps = self.get_abi_dumps_from_shared_libraries(
136 git_rev, git_worktree_path
137 )
138 self.cleanup_worktree(git_worktree_path)
139 return abi_dumps
140
141 def get_abi_compatibility_report(self):
142 compatibility_report = ""
143 compliance_return_code = 0
144 for mbed_module in self.mbedtls_modules:
145 output_path = os.path.join(
146 self.report_dir, "{}-{}-{}.html".format(
147 mbed_module, self.old_rev, self.new_rev
148 )
149 )
150 abi_compliance_command = [
151 "abi-compliance-checker",
152 "-l", mbed_module,
153 "-old", self.old_dumps[mbed_module],
154 "-new", self.new_dumps[mbed_module],
155 "-strict",
156 "-report-path", output_path
157 ]
158 abi_compliance_process = subprocess.Popen(
159 abi_compliance_command,
160 stdout=subprocess.PIPE,
161 stderr=subprocess.STDOUT
162 )
163 abi_compliance_output, _ = abi_compliance_process.communicate()
164 self.log.info(abi_compliance_output.decode("utf-8"))
165 if abi_compliance_process.returncode == 0:
166 compatibility_report += (
167 "No compatibility issues for {}\n".format(mbed_module)
168 )
169 if not self.keep_all_reports:
170 os.remove(output_path)
171 elif abi_compliance_process.returncode == 1:
172 compliance_return_code = 1
173 self.should_keep_report_dir = True
174 compatibility_report += (
175 "Compatibility issues found for {}, "
176 "for details see {}\n".format(mbed_module, output_path)
177 )
178 else:
179 raise Exception(
180 "abi-compliance-checker failed with a return code of {},"
181 " aborting".format(abi_compliance_process.returncode)
182 )
183 os.remove(self.old_dumps[mbed_module])
184 os.remove(self.new_dumps[mbed_module])
185 if not self.should_keep_report_dir and not self.keep_all_reports:
186 os.rmdir(self.report_dir)
187 self.log.info(compatibility_report)
188 return compliance_return_code
189
190 def check_for_abi_changes(self):
191 self.check_repo_path()
192 self.check_abi_tools_are_installed()
193 self.old_dumps = self.get_abi_dump_for_ref(self.old_rev)
194 self.new_dumps = self.get_abi_dump_for_ref(self.new_rev)
195 return self.get_abi_compatibility_report()
196
197
198def run_main():
199 try:
200 parser = argparse.ArgumentParser(
201 description=(
202 "This script is a small wrapper around the "
203 "abi-compliance-checker and abi-dumper tools, applying them "
204 "to compare the ABI and API of the library files from two "
205 "different Git revisions within an Mbed TLS repository."
206 " The results of the comparison are formatted as HTML and"
207 " stored at a configurable location. Returns 0 on success, "
208 "1 on ABI/API non-compliance, and 2 if there is an error "
Darryl Green78696802018-04-06 11:23:22 +0100209 "while running the script. Note: requires Python 3, "
210 "must be run from Mbed TLS root."
Darryl Green7c2dd582018-03-01 14:53:49 +0000211 )
212 )
213 parser.add_argument(
214 "-r", "--report_dir", type=str, default="reports",
215 help="directory where reports are stored, default is reports",
216 )
217 parser.add_argument(
218 "-k", "--keep_all_reports", action="store_true",
219 help="keep all reports, even if there are no compatibility issues",
220 )
221 parser.add_argument(
222 "-o", "--old_rev", type=str, help="revision for old version",
223 required=True
224 )
225 parser.add_argument(
226 "-n", "--new_rev", type=str, help="revision for new version",
227 required=True
228 )
229 abi_args = parser.parse_args()
230 abi_check = AbiChecker(
231 abi_args.report_dir, abi_args.old_rev,
232 abi_args.new_rev, abi_args.keep_all_reports
233 )
234 return_code = abi_check.check_for_abi_changes()
235 sys.exit(return_code)
Darryl Greena6f430f2018-03-15 10:12:06 +0000236 except Exception:
237 traceback.print_exc()
Darryl Green7c2dd582018-03-01 14:53:49 +0000238 sys.exit(2)
239
240
241if __name__ == "__main__":
242 run_main()