blob: 88435ef02de4e818965ea765ab4e606bca83d2d2 [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.
Darryl Green418527b2018-04-16 12:02:29 +010015Note: must be run from Mbed TLS root.
Darryl Green78696802018-04-06 11:23:22 +010016"""
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):
Gilles Peskine712afa72019-02-25 20:36:52 +010029 """API and ABI checker."""
Darryl Green7c2dd582018-03-01 14:53:49 +000030
31 def __init__(self, report_dir, old_rev, new_rev, keep_all_reports):
Gilles Peskine712afa72019-02-25 20:36:52 +010032 """Instantiate the API/ABI checker.
33
34 report_dir: directory for output files
35 old_rev: reference git revision to compare against
36 new_rev: git revision to check
37 keep_all_reports: if false, delete old reports
38 """
Darryl Green7c2dd582018-03-01 14:53:49 +000039 self.repo_path = "."
40 self.log = None
41 self.setup_logger()
42 self.report_dir = os.path.abspath(report_dir)
43 self.keep_all_reports = keep_all_reports
44 self.should_keep_report_dir = os.path.isdir(self.report_dir)
45 self.old_rev = old_rev
46 self.new_rev = new_rev
47 self.mbedtls_modules = ["libmbedcrypto", "libmbedtls", "libmbedx509"]
48 self.old_dumps = {}
49 self.new_dumps = {}
50 self.git_command = "git"
51 self.make_command = "make"
52
Gilles Peskine712afa72019-02-25 20:36:52 +010053 @staticmethod
54 def check_repo_path():
Darryl Greena6f430f2018-03-15 10:12:06 +000055 current_dir = os.path.realpath('.')
56 root_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
57 if current_dir != root_dir:
Darryl Green7c2dd582018-03-01 14:53:49 +000058 raise Exception("Must be run from Mbed TLS root")
59
60 def setup_logger(self):
61 self.log = logging.getLogger()
62 self.log.setLevel(logging.INFO)
63 self.log.addHandler(logging.StreamHandler())
64
Gilles Peskine712afa72019-02-25 20:36:52 +010065 @staticmethod
66 def check_abi_tools_are_installed():
Darryl Green7c2dd582018-03-01 14:53:49 +000067 for command in ["abi-dumper", "abi-compliance-checker"]:
68 if not shutil.which(command):
69 raise Exception("{} not installed, aborting".format(command))
70
71 def get_clean_worktree_for_git_revision(self, git_rev):
Gilles Peskine712afa72019-02-25 20:36:52 +010072 """Make a separate worktree with git_rev checked out.
73 Do not modify the current worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +000074 self.log.info(
75 "Checking out git worktree for revision {}".format(git_rev)
76 )
77 git_worktree_path = tempfile.mkdtemp()
78 worktree_process = subprocess.Popen(
Jaeden Amero7acb0cf2018-11-02 16:22:37 +000079 [self.git_command, "worktree", "add", "--detach", git_worktree_path, git_rev],
Darryl Green7c2dd582018-03-01 14:53:49 +000080 cwd=self.repo_path,
81 stdout=subprocess.PIPE,
82 stderr=subprocess.STDOUT
83 )
84 worktree_output, _ = worktree_process.communicate()
85 self.log.info(worktree_output.decode("utf-8"))
86 if worktree_process.returncode != 0:
87 raise Exception("Checking out worktree failed, aborting")
88 return git_worktree_path
89
Jaeden Ameroffeb1b82018-11-02 16:35:09 +000090 def update_git_submodules(self, git_worktree_path):
91 process = subprocess.Popen(
92 [self.git_command, "submodule", "update", "--init", '--recursive'],
93 cwd=git_worktree_path,
94 stdout=subprocess.PIPE,
95 stderr=subprocess.STDOUT
96 )
97 output, _ = process.communicate()
98 self.log.info(output.decode("utf-8"))
99 if process.returncode != 0:
100 raise Exception("git submodule update failed, aborting")
101
Darryl Green7c2dd582018-03-01 14:53:49 +0000102 def build_shared_libraries(self, git_worktree_path):
Gilles Peskine712afa72019-02-25 20:36:52 +0100103 """Build the shared libraries in the specified worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000104 my_environment = os.environ.copy()
105 my_environment["CFLAGS"] = "-g -Og"
106 my_environment["SHARED"] = "1"
107 make_process = subprocess.Popen(
108 self.make_command,
109 env=my_environment,
110 cwd=git_worktree_path,
111 stdout=subprocess.PIPE,
112 stderr=subprocess.STDOUT
113 )
114 make_output, _ = make_process.communicate()
115 self.log.info(make_output.decode("utf-8"))
116 if make_process.returncode != 0:
117 raise Exception("make failed, aborting")
118
119 def get_abi_dumps_from_shared_libraries(self, git_ref, git_worktree_path):
Gilles Peskine712afa72019-02-25 20:36:52 +0100120 """Generate the ABI dumps for the specified git revision.
121 It must be checked out in git_worktree_path and the shared libraries
122 must have been built."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000123 abi_dumps = {}
124 for mbed_module in self.mbedtls_modules:
125 output_path = os.path.join(
126 self.report_dir, "{}-{}.dump".format(mbed_module, git_ref)
127 )
128 abi_dump_command = [
129 "abi-dumper",
130 os.path.join(
131 git_worktree_path, "library", mbed_module + ".so"),
132 "-o", output_path,
133 "-lver", git_ref
134 ]
135 abi_dump_process = subprocess.Popen(
136 abi_dump_command,
137 stdout=subprocess.PIPE,
138 stderr=subprocess.STDOUT
139 )
140 abi_dump_output, _ = abi_dump_process.communicate()
141 self.log.info(abi_dump_output.decode("utf-8"))
142 if abi_dump_process.returncode != 0:
143 raise Exception("abi-dumper failed, aborting")
144 abi_dumps[mbed_module] = output_path
145 return abi_dumps
146
147 def cleanup_worktree(self, git_worktree_path):
Gilles Peskine712afa72019-02-25 20:36:52 +0100148 """Remove the specified git worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000149 shutil.rmtree(git_worktree_path)
150 worktree_process = subprocess.Popen(
151 [self.git_command, "worktree", "prune"],
152 cwd=self.repo_path,
153 stdout=subprocess.PIPE,
154 stderr=subprocess.STDOUT
155 )
156 worktree_output, _ = worktree_process.communicate()
157 self.log.info(worktree_output.decode("utf-8"))
158 if worktree_process.returncode != 0:
159 raise Exception("Worktree cleanup failed, aborting")
160
161 def get_abi_dump_for_ref(self, git_rev):
Gilles Peskine712afa72019-02-25 20:36:52 +0100162 """Generate the ABI dumps for the specified git revision."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000163 git_worktree_path = self.get_clean_worktree_for_git_revision(git_rev)
Jaeden Ameroffeb1b82018-11-02 16:35:09 +0000164 self.update_git_submodules(git_worktree_path)
Darryl Green7c2dd582018-03-01 14:53:49 +0000165 self.build_shared_libraries(git_worktree_path)
166 abi_dumps = self.get_abi_dumps_from_shared_libraries(
167 git_rev, git_worktree_path
168 )
169 self.cleanup_worktree(git_worktree_path)
170 return abi_dumps
171
172 def get_abi_compatibility_report(self):
Gilles Peskine712afa72019-02-25 20:36:52 +0100173 """Generate a report of the differences between the reference ABI
174 and the new ABI. ABI dumps from self.old_rev and self.new_rev must
175 be available."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000176 compatibility_report = ""
177 compliance_return_code = 0
178 for mbed_module in self.mbedtls_modules:
179 output_path = os.path.join(
180 self.report_dir, "{}-{}-{}.html".format(
181 mbed_module, self.old_rev, self.new_rev
182 )
183 )
184 abi_compliance_command = [
185 "abi-compliance-checker",
186 "-l", mbed_module,
187 "-old", self.old_dumps[mbed_module],
188 "-new", self.new_dumps[mbed_module],
189 "-strict",
190 "-report-path", output_path
191 ]
192 abi_compliance_process = subprocess.Popen(
193 abi_compliance_command,
194 stdout=subprocess.PIPE,
195 stderr=subprocess.STDOUT
196 )
197 abi_compliance_output, _ = abi_compliance_process.communicate()
198 self.log.info(abi_compliance_output.decode("utf-8"))
199 if abi_compliance_process.returncode == 0:
200 compatibility_report += (
201 "No compatibility issues for {}\n".format(mbed_module)
202 )
203 if not self.keep_all_reports:
204 os.remove(output_path)
205 elif abi_compliance_process.returncode == 1:
206 compliance_return_code = 1
207 self.should_keep_report_dir = True
208 compatibility_report += (
209 "Compatibility issues found for {}, "
210 "for details see {}\n".format(mbed_module, output_path)
211 )
212 else:
213 raise Exception(
214 "abi-compliance-checker failed with a return code of {},"
215 " aborting".format(abi_compliance_process.returncode)
216 )
217 os.remove(self.old_dumps[mbed_module])
218 os.remove(self.new_dumps[mbed_module])
219 if not self.should_keep_report_dir and not self.keep_all_reports:
220 os.rmdir(self.report_dir)
221 self.log.info(compatibility_report)
222 return compliance_return_code
223
224 def check_for_abi_changes(self):
Gilles Peskine712afa72019-02-25 20:36:52 +0100225 """Generate a report of ABI differences
226 between self.old_rev and self.new_rev."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000227 self.check_repo_path()
228 self.check_abi_tools_are_installed()
229 self.old_dumps = self.get_abi_dump_for_ref(self.old_rev)
230 self.new_dumps = self.get_abi_dump_for_ref(self.new_rev)
231 return self.get_abi_compatibility_report()
232
233
234def run_main():
235 try:
236 parser = argparse.ArgumentParser(
237 description=(
Darryl Green418527b2018-04-16 12:02:29 +0100238 """This script is a small wrapper around the
239 abi-compliance-checker and abi-dumper tools, applying them
240 to compare the ABI and API of the library files from two
241 different Git revisions within an Mbed TLS repository.
242 The results of the comparison are formatted as HTML and stored
243 at a configurable location. Returns 0 on success, 1 on ABI/API
244 non-compliance, and 2 if there is an error while running the
245 script. Note: must be run from Mbed TLS root."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000246 )
247 )
248 parser.add_argument(
Darryl Green418527b2018-04-16 12:02:29 +0100249 "-r", "--report-dir", type=str, default="reports",
Darryl Green7c2dd582018-03-01 14:53:49 +0000250 help="directory where reports are stored, default is reports",
251 )
252 parser.add_argument(
Darryl Green418527b2018-04-16 12:02:29 +0100253 "-k", "--keep-all-reports", action="store_true",
Darryl Green7c2dd582018-03-01 14:53:49 +0000254 help="keep all reports, even if there are no compatibility issues",
255 )
256 parser.add_argument(
Darryl Green418527b2018-04-16 12:02:29 +0100257 "-o", "--old-rev", type=str, help="revision for old version",
Darryl Green7c2dd582018-03-01 14:53:49 +0000258 required=True
259 )
260 parser.add_argument(
Darryl Green418527b2018-04-16 12:02:29 +0100261 "-n", "--new-rev", type=str, help="revision for new version",
Darryl Green7c2dd582018-03-01 14:53:49 +0000262 required=True
263 )
264 abi_args = parser.parse_args()
265 abi_check = AbiChecker(
266 abi_args.report_dir, abi_args.old_rev,
267 abi_args.new_rev, abi_args.keep_all_reports
268 )
269 return_code = abi_check.check_for_abi_changes()
270 sys.exit(return_code)
Darryl Greena6f430f2018-03-15 10:12:06 +0000271 except Exception:
272 traceback.print_exc()
Darryl Green7c2dd582018-03-01 14:53:49 +0000273 sys.exit(2)
274
275
276if __name__ == "__main__":
277 run_main()