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