blob: 2051c3eae57342c9a7bfc76244198b838b84d4fd [file] [log] [blame]
Darryl Green7c2dd582018-03-01 14:53:49 +00001#!/usr/bin/env python3
Darryl Green78696802018-04-06 11:23:22 +01002"""
Darryl Green78696802018-04-06 11:23:22 +01003Purpose
4
Gilles Peskine92165362021-04-23 16:37:12 +02005This script compares the interfaces of two versions of Mbed TLS, looking
6for backward incompatibilities between two different Git revisions within
7an Mbed TLS repository. It must be run from the root of a Git working tree.
8
9For the source (API) and runtime (ABI) interface compatibility, this script
10is a small wrapper around the abi-compliance-checker and abi-dumper tools,
11applying them to compare the header and library files.
12
13For the storage format, this script compares the automatically generated
14storage tests, and complains if there is a reduction in coverage.
15
Darryl Greene62f9bb2019-02-21 13:09:26 +000016The results of the comparison are either formatted as HTML and stored at
Darryl Green4cde8a02019-03-05 15:21:32 +000017a configurable location, or are given as a brief list of problems.
Darryl Greene62f9bb2019-02-21 13:09:26 +000018Returns 0 on success, 1 on ABI/API non-compliance, and 2 if there is an error
Gilles Peskine92165362021-04-23 16:37:12 +020019while running the script.
Darryl Green78696802018-04-06 11:23:22 +010020"""
Darryl Green7c2dd582018-03-01 14:53:49 +000021
Bence Szépkúti1e148272020-08-07 13:07:28 +020022# Copyright The Mbed TLS Contributors
Bence Szépkútic7da1fe2020-05-26 01:54:15 +020023# SPDX-License-Identifier: Apache-2.0
24#
25# Licensed under the Apache License, Version 2.0 (the "License"); you may
26# not use this file except in compliance with the License.
27# You may obtain a copy of the License at
28#
29# http://www.apache.org/licenses/LICENSE-2.0
30#
31# Unless required by applicable law or agreed to in writing, software
32# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
33# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
34# See the License for the specific language governing permissions and
35# limitations under the License.
Bence Szépkútic7da1fe2020-05-26 01:54:15 +020036
Darryl Green7c2dd582018-03-01 14:53:49 +000037import os
Gilles Peskine92165362021-04-23 16:37:12 +020038import re
Darryl Green7c2dd582018-03-01 14:53:49 +000039import sys
40import traceback
41import shutil
42import subprocess
43import argparse
44import logging
45import tempfile
Darryl Green9f357d62019-02-25 11:35:05 +000046import fnmatch
Darryl Green0d1ca512019-04-09 09:14:17 +010047from types import SimpleNamespace
Darryl Green7c2dd582018-03-01 14:53:49 +000048
Darryl Greene62f9bb2019-02-21 13:09:26 +000049import xml.etree.ElementTree as ET
50
Darryl Green7c2dd582018-03-01 14:53:49 +000051
Gilles Peskine184c0962020-03-24 18:25:17 +010052class AbiChecker:
Gilles Peskine712afa72019-02-25 20:36:52 +010053 """API and ABI checker."""
Darryl Green7c2dd582018-03-01 14:53:49 +000054
Darryl Green0d1ca512019-04-09 09:14:17 +010055 def __init__(self, old_version, new_version, configuration):
Gilles Peskine712afa72019-02-25 20:36:52 +010056 """Instantiate the API/ABI checker.
57
Darryl Green7c1a7332019-03-05 16:25:38 +000058 old_version: RepoVersion containing details to compare against
59 new_version: RepoVersion containing details to check
Darryl Greenf67e3492019-04-12 15:17:02 +010060 configuration.report_dir: directory for output files
61 configuration.keep_all_reports: if false, delete old reports
62 configuration.brief: if true, output shorter report to stdout
Gilles Peskinec76ab852021-04-23 16:32:32 +020063 configuration.check_api: if true, compare ABIs
64 configuration.check_api: if true, compare APIs
Gilles Peskine92165362021-04-23 16:37:12 +020065 configuration.check_storage: if true, compare storage format tests
Darryl Greenf67e3492019-04-12 15:17:02 +010066 configuration.skip_file: path to file containing symbols and types to skip
Gilles Peskine712afa72019-02-25 20:36:52 +010067 """
Darryl Green7c2dd582018-03-01 14:53:49 +000068 self.repo_path = "."
69 self.log = None
Darryl Green0d1ca512019-04-09 09:14:17 +010070 self.verbose = configuration.verbose
Darryl Green3a5f6c82019-03-05 16:30:39 +000071 self._setup_logger()
Darryl Green0d1ca512019-04-09 09:14:17 +010072 self.report_dir = os.path.abspath(configuration.report_dir)
73 self.keep_all_reports = configuration.keep_all_reports
Darryl Green492bc402019-04-11 15:50:41 +010074 self.can_remove_report_dir = not (os.path.exists(self.report_dir) or
Darryl Green0d1ca512019-04-09 09:14:17 +010075 self.keep_all_reports)
Darryl Green7c1a7332019-03-05 16:25:38 +000076 self.old_version = old_version
77 self.new_version = new_version
Darryl Green0d1ca512019-04-09 09:14:17 +010078 self.skip_file = configuration.skip_file
Gilles Peskinec76ab852021-04-23 16:32:32 +020079 self.check_abi = configuration.check_abi
80 self.check_api = configuration.check_api
81 if self.check_abi != self.check_api:
82 raise Exception('Checking API without ABI or vice versa is not supported')
Gilles Peskine92165362021-04-23 16:37:12 +020083 self.check_storage_tests = configuration.check_storage
Darryl Green0d1ca512019-04-09 09:14:17 +010084 self.brief = configuration.brief
Darryl Green7c2dd582018-03-01 14:53:49 +000085 self.git_command = "git"
86 self.make_command = "make"
87
Gilles Peskine712afa72019-02-25 20:36:52 +010088 @staticmethod
89 def check_repo_path():
Gilles Peskine6aa32cc2019-07-04 18:59:36 +020090 if not all(os.path.isdir(d) for d in ["include", "library", "tests"]):
Darryl Green7c2dd582018-03-01 14:53:49 +000091 raise Exception("Must be run from Mbed TLS root")
92
Darryl Green3a5f6c82019-03-05 16:30:39 +000093 def _setup_logger(self):
Darryl Green7c2dd582018-03-01 14:53:49 +000094 self.log = logging.getLogger()
Darryl Green3c3da792019-03-08 11:30:04 +000095 if self.verbose:
96 self.log.setLevel(logging.DEBUG)
97 else:
98 self.log.setLevel(logging.INFO)
Darryl Green7c2dd582018-03-01 14:53:49 +000099 self.log.addHandler(logging.StreamHandler())
100
Gilles Peskine712afa72019-02-25 20:36:52 +0100101 @staticmethod
102 def check_abi_tools_are_installed():
Darryl Green7c2dd582018-03-01 14:53:49 +0000103 for command in ["abi-dumper", "abi-compliance-checker"]:
104 if not shutil.which(command):
105 raise Exception("{} not installed, aborting".format(command))
106
Darryl Green3a5f6c82019-03-05 16:30:39 +0000107 def _get_clean_worktree_for_git_revision(self, version):
Darryl Green7c1a7332019-03-05 16:25:38 +0000108 """Make a separate worktree with version.revision checked out.
Gilles Peskine712afa72019-02-25 20:36:52 +0100109 Do not modify the current worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000110 git_worktree_path = tempfile.mkdtemp()
Darryl Green7c1a7332019-03-05 16:25:38 +0000111 if version.repository:
Darryl Green3c3da792019-03-08 11:30:04 +0000112 self.log.debug(
Darryl Greenda84e322019-02-19 16:59:33 +0000113 "Checking out git worktree for revision {} from {}".format(
Darryl Green7c1a7332019-03-05 16:25:38 +0000114 version.revision, version.repository
Darryl Greenda84e322019-02-19 16:59:33 +0000115 )
116 )
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100117 fetch_output = subprocess.check_output(
Darryl Green7c1a7332019-03-05 16:25:38 +0000118 [self.git_command, "fetch",
119 version.repository, version.revision],
Darryl Greenda84e322019-02-19 16:59:33 +0000120 cwd=self.repo_path,
Darryl Greenda84e322019-02-19 16:59:33 +0000121 stderr=subprocess.STDOUT
122 )
Darryl Green3c3da792019-03-08 11:30:04 +0000123 self.log.debug(fetch_output.decode("utf-8"))
Darryl Greenda84e322019-02-19 16:59:33 +0000124 worktree_rev = "FETCH_HEAD"
125 else:
Darryl Green3c3da792019-03-08 11:30:04 +0000126 self.log.debug("Checking out git worktree for revision {}".format(
Darryl Green7c1a7332019-03-05 16:25:38 +0000127 version.revision
128 ))
129 worktree_rev = version.revision
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100130 worktree_output = subprocess.check_output(
Darryl Greenda84e322019-02-19 16:59:33 +0000131 [self.git_command, "worktree", "add", "--detach",
132 git_worktree_path, worktree_rev],
Darryl Green7c2dd582018-03-01 14:53:49 +0000133 cwd=self.repo_path,
Darryl Green7c2dd582018-03-01 14:53:49 +0000134 stderr=subprocess.STDOUT
135 )
Darryl Green3c3da792019-03-08 11:30:04 +0000136 self.log.debug(worktree_output.decode("utf-8"))
Gilles Peskine3e2da4a2019-07-04 19:01:22 +0200137 version.commit = subprocess.check_output(
Darryl Green762351b2019-07-25 14:33:33 +0100138 [self.git_command, "rev-parse", "HEAD"],
Gilles Peskine3e2da4a2019-07-04 19:01:22 +0200139 cwd=git_worktree_path,
140 stderr=subprocess.STDOUT
141 ).decode("ascii").rstrip()
142 self.log.debug("Commit is {}".format(version.commit))
Darryl Green7c2dd582018-03-01 14:53:49 +0000143 return git_worktree_path
144
Darryl Green3a5f6c82019-03-05 16:30:39 +0000145 def _update_git_submodules(self, git_worktree_path, version):
Darryl Green8184df52019-04-05 17:06:17 +0100146 """If the crypto submodule is present, initialize it.
147 if version.crypto_revision exists, update it to that revision,
148 otherwise update it to the default revision"""
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100149 update_output = subprocess.check_output(
Jaeden Ameroffeb1b82018-11-02 16:35:09 +0000150 [self.git_command, "submodule", "update", "--init", '--recursive'],
151 cwd=git_worktree_path,
Jaeden Ameroffeb1b82018-11-02 16:35:09 +0000152 stderr=subprocess.STDOUT
153 )
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100154 self.log.debug(update_output.decode("utf-8"))
Darryl Greene29ce702019-03-05 15:23:25 +0000155 if not (os.path.exists(os.path.join(git_worktree_path, "crypto"))
Darryl Green7c1a7332019-03-05 16:25:38 +0000156 and version.crypto_revision):
Darryl Greene29ce702019-03-05 15:23:25 +0000157 return
158
Darryl Green7c1a7332019-03-05 16:25:38 +0000159 if version.crypto_repository:
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100160 fetch_output = subprocess.check_output(
Darryl Green1d95c532019-03-08 11:12:19 +0000161 [self.git_command, "fetch", version.crypto_repository,
162 version.crypto_revision],
Darryl Greene29ce702019-03-05 15:23:25 +0000163 cwd=os.path.join(git_worktree_path, "crypto"),
Darryl Greene29ce702019-03-05 15:23:25 +0000164 stderr=subprocess.STDOUT
165 )
Darryl Green3c3da792019-03-08 11:30:04 +0000166 self.log.debug(fetch_output.decode("utf-8"))
Darryl Green1d95c532019-03-08 11:12:19 +0000167 crypto_rev = "FETCH_HEAD"
168 else:
169 crypto_rev = version.crypto_revision
170
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100171 checkout_output = subprocess.check_output(
Darryl Green1d95c532019-03-08 11:12:19 +0000172 [self.git_command, "checkout", crypto_rev],
173 cwd=os.path.join(git_worktree_path, "crypto"),
Darryl Green1d95c532019-03-08 11:12:19 +0000174 stderr=subprocess.STDOUT
175 )
Darryl Green3c3da792019-03-08 11:30:04 +0000176 self.log.debug(checkout_output.decode("utf-8"))
Jaeden Ameroffeb1b82018-11-02 16:35:09 +0000177
Darryl Green3a5f6c82019-03-05 16:30:39 +0000178 def _build_shared_libraries(self, git_worktree_path, version):
Gilles Peskine712afa72019-02-25 20:36:52 +0100179 """Build the shared libraries in the specified worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000180 my_environment = os.environ.copy()
181 my_environment["CFLAGS"] = "-g -Og"
182 my_environment["SHARED"] = "1"
Darryl Greend2dba362019-05-09 13:03:05 +0100183 if os.path.exists(os.path.join(git_worktree_path, "crypto")):
184 my_environment["USE_CRYPTO_SUBMODULE"] = "1"
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100185 make_output = subprocess.check_output(
Darryl Greenddf25a62019-02-28 11:52:39 +0000186 [self.make_command, "lib"],
Darryl Green7c2dd582018-03-01 14:53:49 +0000187 env=my_environment,
188 cwd=git_worktree_path,
Darryl Green7c2dd582018-03-01 14:53:49 +0000189 stderr=subprocess.STDOUT
190 )
Darryl Green3c3da792019-03-08 11:30:04 +0000191 self.log.debug(make_output.decode("utf-8"))
Darryl Greenf025d532019-04-12 15:18:02 +0100192 for root, _dirs, files in os.walk(git_worktree_path):
Darryl Green9f357d62019-02-25 11:35:05 +0000193 for file in fnmatch.filter(files, "*.so"):
Darryl Green7c1a7332019-03-05 16:25:38 +0000194 version.modules[os.path.splitext(file)[0]] = (
Darryl Green3e7a9802019-02-27 16:53:40 +0000195 os.path.join(root, file)
Darryl Green9f357d62019-02-25 11:35:05 +0000196 )
Darryl Green7c2dd582018-03-01 14:53:49 +0000197
Gilles Peskine3e2da4a2019-07-04 19:01:22 +0200198 @staticmethod
199 def _pretty_revision(version):
200 if version.revision == version.commit:
201 return version.revision
202 else:
203 return "{} ({})".format(version.revision, version.commit)
204
Darryl Green8184df52019-04-05 17:06:17 +0100205 def _get_abi_dumps_from_shared_libraries(self, version):
Gilles Peskine712afa72019-02-25 20:36:52 +0100206 """Generate the ABI dumps for the specified git revision.
Darryl Green8184df52019-04-05 17:06:17 +0100207 The shared libraries must have been built and the module paths
208 present in version.modules."""
Darryl Green7c1a7332019-03-05 16:25:38 +0000209 for mbed_module, module_path in version.modules.items():
Darryl Green7c2dd582018-03-01 14:53:49 +0000210 output_path = os.path.join(
Darryl Greenfe9a6752019-04-04 14:39:33 +0100211 self.report_dir, "{}-{}-{}.dump".format(
212 mbed_module, version.revision, version.version
Darryl Green3e7a9802019-02-27 16:53:40 +0000213 )
Darryl Green7c2dd582018-03-01 14:53:49 +0000214 )
215 abi_dump_command = [
216 "abi-dumper",
Darryl Green9f357d62019-02-25 11:35:05 +0000217 module_path,
Darryl Green7c2dd582018-03-01 14:53:49 +0000218 "-o", output_path,
Gilles Peskine3e2da4a2019-07-04 19:01:22 +0200219 "-lver", self._pretty_revision(version),
Darryl Green7c2dd582018-03-01 14:53:49 +0000220 ]
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100221 abi_dump_output = subprocess.check_output(
Darryl Green7c2dd582018-03-01 14:53:49 +0000222 abi_dump_command,
Darryl Green7c2dd582018-03-01 14:53:49 +0000223 stderr=subprocess.STDOUT
224 )
Darryl Green3c3da792019-03-08 11:30:04 +0000225 self.log.debug(abi_dump_output.decode("utf-8"))
Darryl Green7c1a7332019-03-05 16:25:38 +0000226 version.abi_dumps[mbed_module] = output_path
Darryl Green7c2dd582018-03-01 14:53:49 +0000227
Gilles Peskine92165362021-04-23 16:37:12 +0200228 @staticmethod
229 def _normalize_storage_test_case_data(line):
230 """Eliminate cosmetic or irrelevant details in storage format test cases."""
231 line = re.sub(r'\s+', r'', line)
232 return line
233
234 def _read_storage_tests(self, directory, filename, storage_tests):
235 """Record storage tests from the given file.
236
237 Populate the storage_tests dictionary with test cases read from
238 filename under directory.
239 """
240 at_paragraph_start = True
241 description = None
242 full_path = os.path.join(directory, filename)
243 for line_number, line in enumerate(open(full_path), 1):
244 line = line.strip()
245 if not line:
246 at_paragraph_start = True
247 continue
248 if line.startswith('#'):
249 continue
250 if at_paragraph_start:
251 description = line.strip()
252 at_paragraph_start = False
253 continue
254 if line.startswith('depends_on:'):
255 continue
256 # We've reached a test case data line
257 test_case_data = self._normalize_storage_test_case_data(line)
258 metadata = SimpleNamespace(
259 filename=filename,
260 line_number=line_number,
261 description=description
262 )
263 storage_tests[test_case_data] = metadata
264
265 def _get_storage_format_tests(self, version, git_worktree_path):
266 """Generate and record the storage format tests for the specified git version.
267
268 The version must be checked out at git_worktree_path.
269 """
270 full_test_list = subprocess.check_output(
271 ['tests/scripts/generate_psa_tests.py', '--list'],
272 cwd=git_worktree_path,
273 ).decode('ascii')
274 storage_test_list = [test_file
275 for test_file in full_test_list.split()
276 # If you change the following condition, update
277 # generate_psa_tests.py accordingly.
278 if 'storage_format' in test_file]
279 subprocess.check_call(
280 ['tests/scripts/generate_psa_tests.py'] + storage_test_list,
281 cwd=git_worktree_path,
282 )
283 for test_file in storage_test_list:
284 self._read_storage_tests(git_worktree_path, test_file,
285 version.storage_tests)
286
Darryl Green3a5f6c82019-03-05 16:30:39 +0000287 def _cleanup_worktree(self, git_worktree_path):
Gilles Peskine712afa72019-02-25 20:36:52 +0100288 """Remove the specified git worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000289 shutil.rmtree(git_worktree_path)
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100290 worktree_output = subprocess.check_output(
Darryl Green7c2dd582018-03-01 14:53:49 +0000291 [self.git_command, "worktree", "prune"],
292 cwd=self.repo_path,
Darryl Green7c2dd582018-03-01 14:53:49 +0000293 stderr=subprocess.STDOUT
294 )
Darryl Green3c3da792019-03-08 11:30:04 +0000295 self.log.debug(worktree_output.decode("utf-8"))
Darryl Green7c2dd582018-03-01 14:53:49 +0000296
Darryl Green3a5f6c82019-03-05 16:30:39 +0000297 def _get_abi_dump_for_ref(self, version):
Gilles Peskine92165362021-04-23 16:37:12 +0200298 """Generate the interface information for the specified git revision."""
Darryl Green3a5f6c82019-03-05 16:30:39 +0000299 git_worktree_path = self._get_clean_worktree_for_git_revision(version)
300 self._update_git_submodules(git_worktree_path, version)
Gilles Peskinec76ab852021-04-23 16:32:32 +0200301 if self.check_abi:
302 self._build_shared_libraries(git_worktree_path, version)
303 self._get_abi_dumps_from_shared_libraries(version)
Gilles Peskine92165362021-04-23 16:37:12 +0200304 if self.check_storage_tests:
305 self._get_storage_format_tests(version, git_worktree_path)
Darryl Green3a5f6c82019-03-05 16:30:39 +0000306 self._cleanup_worktree(git_worktree_path)
Darryl Green7c2dd582018-03-01 14:53:49 +0000307
Darryl Green3a5f6c82019-03-05 16:30:39 +0000308 def _remove_children_with_tag(self, parent, tag):
Darryl Greene62f9bb2019-02-21 13:09:26 +0000309 children = parent.getchildren()
310 for child in children:
311 if child.tag == tag:
312 parent.remove(child)
313 else:
Darryl Green3a5f6c82019-03-05 16:30:39 +0000314 self._remove_children_with_tag(child, tag)
Darryl Greene62f9bb2019-02-21 13:09:26 +0000315
Darryl Green3a5f6c82019-03-05 16:30:39 +0000316 def _remove_extra_detail_from_report(self, report_root):
Darryl Greene62f9bb2019-02-21 13:09:26 +0000317 for tag in ['test_info', 'test_results', 'problem_summary',
Darryl Greenc6f874b2019-06-05 12:57:50 +0100318 'added_symbols', 'affected']:
Darryl Green3a5f6c82019-03-05 16:30:39 +0000319 self._remove_children_with_tag(report_root, tag)
Darryl Greene62f9bb2019-02-21 13:09:26 +0000320
321 for report in report_root:
322 for problems in report.getchildren()[:]:
323 if not problems.getchildren():
324 report.remove(problems)
325
Gilles Peskineada828f2019-07-04 19:17:40 +0200326 def _abi_compliance_command(self, mbed_module, output_path):
327 """Build the command to run to analyze the library mbed_module.
328 The report will be placed in output_path."""
329 abi_compliance_command = [
330 "abi-compliance-checker",
331 "-l", mbed_module,
332 "-old", self.old_version.abi_dumps[mbed_module],
333 "-new", self.new_version.abi_dumps[mbed_module],
334 "-strict",
335 "-report-path", output_path,
336 ]
337 if self.skip_file:
338 abi_compliance_command += ["-skip-symbols", self.skip_file,
339 "-skip-types", self.skip_file]
340 if self.brief:
341 abi_compliance_command += ["-report-format", "xml",
342 "-stdout"]
343 return abi_compliance_command
344
345 def _is_library_compatible(self, mbed_module, compatibility_report):
346 """Test if the library mbed_module has remained compatible.
347 Append a message regarding compatibility to compatibility_report."""
348 output_path = os.path.join(
349 self.report_dir, "{}-{}-{}.html".format(
350 mbed_module, self.old_version.revision,
351 self.new_version.revision
352 )
353 )
354 try:
355 subprocess.check_output(
356 self._abi_compliance_command(mbed_module, output_path),
357 stderr=subprocess.STDOUT
358 )
359 except subprocess.CalledProcessError as err:
360 if err.returncode != 1:
361 raise err
362 if self.brief:
363 self.log.info(
364 "Compatibility issues found for {}".format(mbed_module)
365 )
366 report_root = ET.fromstring(err.output.decode("utf-8"))
367 self._remove_extra_detail_from_report(report_root)
368 self.log.info(ET.tostring(report_root).decode("utf-8"))
369 else:
370 self.can_remove_report_dir = False
371 compatibility_report.append(
372 "Compatibility issues found for {}, "
373 "for details see {}".format(mbed_module, output_path)
374 )
375 return False
376 compatibility_report.append(
377 "No compatibility issues for {}".format(mbed_module)
378 )
379 if not (self.keep_all_reports or self.brief):
380 os.remove(output_path)
381 return True
382
Gilles Peskine92165362021-04-23 16:37:12 +0200383 @staticmethod
384 def _is_storage_format_compatible(old_tests, new_tests,
385 compatibility_report):
386 """Check whether all tests present in old_tests are also in new_tests.
387
388 Append a message regarding compatibility to compatibility_report.
389 """
390 missing = frozenset(old_tests.keys()).difference(new_tests.keys())
391 for test_data in sorted(missing):
392 metadata = old_tests[test_data]
393 compatibility_report.append(
394 'Test case from {} line {} "{}" has disappeared: {}'.format(
395 metadata.filename, metadata.line_number,
396 metadata.description, test_data
397 )
398 )
399 compatibility_report.append(
400 'FAIL: {}/{} storage format test cases have changed or disappeared.'.format(
401 len(missing), len(old_tests)
402 ) if missing else
403 'PASS: All {} storage format test cases are preserved.'.format(
404 len(old_tests)
405 )
406 )
407 compatibility_report.append(
408 'Info: number of storage format tests cases: {} -> {}.'.format(
409 len(old_tests), len(new_tests)
410 )
411 )
412 return not missing
413
Darryl Green7c2dd582018-03-01 14:53:49 +0000414 def get_abi_compatibility_report(self):
Gilles Peskine712afa72019-02-25 20:36:52 +0100415 """Generate a report of the differences between the reference ABI
Darryl Green8184df52019-04-05 17:06:17 +0100416 and the new ABI. ABI dumps from self.old_version and self.new_version
417 must be available."""
Gilles Peskineada828f2019-07-04 19:17:40 +0200418 compatibility_report = ["Checking evolution from {} to {}".format(
Gilles Peskine3e2da4a2019-07-04 19:01:22 +0200419 self._pretty_revision(self.old_version),
420 self._pretty_revision(self.new_version)
Gilles Peskineada828f2019-07-04 19:17:40 +0200421 )]
Darryl Green7c2dd582018-03-01 14:53:49 +0000422 compliance_return_code = 0
Gilles Peskine92165362021-04-23 16:37:12 +0200423
Gilles Peskinec76ab852021-04-23 16:32:32 +0200424 if self.check_abi:
425 shared_modules = list(set(self.old_version.modules.keys()) &
426 set(self.new_version.modules.keys()))
427 for mbed_module in shared_modules:
428 if not self._is_library_compatible(mbed_module,
429 compatibility_report):
430 compliance_return_code = 1
431
Gilles Peskine92165362021-04-23 16:37:12 +0200432 if self.check_storage_tests:
433 if not self._is_storage_format_compatible(
434 self.old_version.storage_tests,
435 self.new_version.storage_tests,
436 compatibility_report):
Gilles Peskineada828f2019-07-04 19:17:40 +0200437 compliance_return_code = 1
Gilles Peskine92165362021-04-23 16:37:12 +0200438
Darryl Greenf2688e22019-05-29 11:29:08 +0100439 for version in [self.old_version, self.new_version]:
440 for mbed_module, mbed_module_dump in version.abi_dumps.items():
441 os.remove(mbed_module_dump)
Darryl Green3d3d5522019-02-25 17:01:55 +0000442 if self.can_remove_report_dir:
Darryl Green7c2dd582018-03-01 14:53:49 +0000443 os.rmdir(self.report_dir)
Gilles Peskineada828f2019-07-04 19:17:40 +0200444 self.log.info("\n".join(compatibility_report))
Darryl Green7c2dd582018-03-01 14:53:49 +0000445 return compliance_return_code
446
447 def check_for_abi_changes(self):
Gilles Peskine712afa72019-02-25 20:36:52 +0100448 """Generate a report of ABI differences
449 between self.old_rev and self.new_rev."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000450 self.check_repo_path()
451 self.check_abi_tools_are_installed()
Darryl Green3a5f6c82019-03-05 16:30:39 +0000452 self._get_abi_dump_for_ref(self.old_version)
453 self._get_abi_dump_for_ref(self.new_version)
Darryl Green7c2dd582018-03-01 14:53:49 +0000454 return self.get_abi_compatibility_report()
455
456
457def run_main():
458 try:
459 parser = argparse.ArgumentParser(
460 description=(
Darryl Green418527b2018-04-16 12:02:29 +0100461 """This script is a small wrapper around the
462 abi-compliance-checker and abi-dumper tools, applying them
463 to compare the ABI and API of the library files from two
464 different Git revisions within an Mbed TLS repository.
Darryl Greene62f9bb2019-02-21 13:09:26 +0000465 The results of the comparison are either formatted as HTML and
Darryl Green4cde8a02019-03-05 15:21:32 +0000466 stored at a configurable location, or are given as a brief list
467 of problems. Returns 0 on success, 1 on ABI/API non-compliance,
468 and 2 if there is an error while running the script.
469 Note: must be run from Mbed TLS root."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000470 )
471 )
472 parser.add_argument(
Darryl Green3c3da792019-03-08 11:30:04 +0000473 "-v", "--verbose", action="store_true",
474 help="set verbosity level",
475 )
476 parser.add_argument(
Darryl Green418527b2018-04-16 12:02:29 +0100477 "-r", "--report-dir", type=str, default="reports",
Darryl Green7c2dd582018-03-01 14:53:49 +0000478 help="directory where reports are stored, default is reports",
479 )
480 parser.add_argument(
Darryl Green418527b2018-04-16 12:02:29 +0100481 "-k", "--keep-all-reports", action="store_true",
Darryl Green7c2dd582018-03-01 14:53:49 +0000482 help="keep all reports, even if there are no compatibility issues",
483 )
484 parser.add_argument(
Darryl Greenc5132ff2019-03-01 09:54:44 +0000485 "-o", "--old-rev", type=str, help="revision for old version.",
486 required=True,
Darryl Green7c2dd582018-03-01 14:53:49 +0000487 )
488 parser.add_argument(
Darryl Greenc5132ff2019-03-01 09:54:44 +0000489 "-or", "--old-repo", type=str, help="repository for old version."
Darryl Green9f357d62019-02-25 11:35:05 +0000490 )
491 parser.add_argument(
Darryl Greenc5132ff2019-03-01 09:54:44 +0000492 "-oc", "--old-crypto-rev", type=str,
493 help="revision for old crypto submodule."
Darryl Green7c2dd582018-03-01 14:53:49 +0000494 )
Darryl Greenc2883a22019-02-20 15:01:56 +0000495 parser.add_argument(
Darryl Greenc5132ff2019-03-01 09:54:44 +0000496 "-ocr", "--old-crypto-repo", type=str,
497 help="repository for old crypto submodule."
498 )
499 parser.add_argument(
500 "-n", "--new-rev", type=str, help="revision for new version",
501 required=True,
502 )
503 parser.add_argument(
504 "-nr", "--new-repo", type=str, help="repository for new version."
505 )
506 parser.add_argument(
507 "-nc", "--new-crypto-rev", type=str,
508 help="revision for new crypto version"
509 )
510 parser.add_argument(
511 "-ncr", "--new-crypto-repo", type=str,
512 help="repository for new crypto submodule."
Darryl Green9f357d62019-02-25 11:35:05 +0000513 )
514 parser.add_argument(
Darryl Greenc2883a22019-02-20 15:01:56 +0000515 "-s", "--skip-file", type=str,
Gilles Peskineb6ce2342019-07-04 19:00:31 +0200516 help=("path to file containing symbols and types to skip "
517 "(typically \"-s identifiers\" after running "
518 "\"tests/scripts/list-identifiers.sh --internal\")")
Darryl Greenc2883a22019-02-20 15:01:56 +0000519 )
Darryl Greene62f9bb2019-02-21 13:09:26 +0000520 parser.add_argument(
Gilles Peskinec76ab852021-04-23 16:32:32 +0200521 "--check-abi",
522 action='store_true', default=True,
523 help="Perform ABI comparison (default: yes)"
524 )
525 parser.add_argument("--no-check-abi", action='store_false', dest='check_abi')
526 parser.add_argument(
527 "--check-api",
528 action='store_true', default=True,
529 help="Perform API comparison (default: yes)"
530 )
531 parser.add_argument("--no-check-api", action='store_false', dest='check_api')
532 parser.add_argument(
Gilles Peskine92165362021-04-23 16:37:12 +0200533 "--check-storage",
534 action='store_true', default=True,
535 help="Perform storage tests comparison (default: yes)"
536 )
537 parser.add_argument("--no-check-storage", action='store_false', dest='check_storage')
538 parser.add_argument(
Darryl Greene62f9bb2019-02-21 13:09:26 +0000539 "-b", "--brief", action="store_true",
540 help="output only the list of issues to stdout, instead of a full report",
541 )
Darryl Green7c2dd582018-03-01 14:53:49 +0000542 abi_args = parser.parse_args()
Darryl Green492bc402019-04-11 15:50:41 +0100543 if os.path.isfile(abi_args.report_dir):
544 print("Error: {} is not a directory".format(abi_args.report_dir))
545 parser.exit()
Darryl Green0d1ca512019-04-09 09:14:17 +0100546 old_version = SimpleNamespace(
547 version="old",
548 repository=abi_args.old_repo,
549 revision=abi_args.old_rev,
Gilles Peskine3e2da4a2019-07-04 19:01:22 +0200550 commit=None,
Darryl Green0d1ca512019-04-09 09:14:17 +0100551 crypto_repository=abi_args.old_crypto_repo,
552 crypto_revision=abi_args.old_crypto_rev,
553 abi_dumps={},
Gilles Peskine92165362021-04-23 16:37:12 +0200554 storage_tests={},
Darryl Green0d1ca512019-04-09 09:14:17 +0100555 modules={}
Darryl Green8184df52019-04-05 17:06:17 +0100556 )
Darryl Green0d1ca512019-04-09 09:14:17 +0100557 new_version = SimpleNamespace(
558 version="new",
559 repository=abi_args.new_repo,
560 revision=abi_args.new_rev,
Gilles Peskine3e2da4a2019-07-04 19:01:22 +0200561 commit=None,
Darryl Green0d1ca512019-04-09 09:14:17 +0100562 crypto_repository=abi_args.new_crypto_repo,
563 crypto_revision=abi_args.new_crypto_rev,
564 abi_dumps={},
Gilles Peskine92165362021-04-23 16:37:12 +0200565 storage_tests={},
Darryl Green0d1ca512019-04-09 09:14:17 +0100566 modules={}
Darryl Green8184df52019-04-05 17:06:17 +0100567 )
Darryl Green0d1ca512019-04-09 09:14:17 +0100568 configuration = SimpleNamespace(
569 verbose=abi_args.verbose,
570 report_dir=abi_args.report_dir,
571 keep_all_reports=abi_args.keep_all_reports,
572 brief=abi_args.brief,
Gilles Peskinec76ab852021-04-23 16:32:32 +0200573 check_abi=abi_args.check_abi,
574 check_api=abi_args.check_api,
Gilles Peskine92165362021-04-23 16:37:12 +0200575 check_storage=abi_args.check_storage,
Darryl Green0d1ca512019-04-09 09:14:17 +0100576 skip_file=abi_args.skip_file
Darryl Green7c2dd582018-03-01 14:53:49 +0000577 )
Darryl Green0d1ca512019-04-09 09:14:17 +0100578 abi_check = AbiChecker(old_version, new_version, configuration)
Darryl Green7c2dd582018-03-01 14:53:49 +0000579 return_code = abi_check.check_for_abi_changes()
580 sys.exit(return_code)
Gilles Peskinee915d532019-02-25 21:39:42 +0100581 except Exception: # pylint: disable=broad-except
582 # Print the backtrace and exit explicitly so as to exit with
583 # status 2, not 1.
Darryl Greena6f430f2018-03-15 10:12:06 +0000584 traceback.print_exc()
Darryl Green7c2dd582018-03-01 14:53:49 +0000585 sys.exit(2)
586
587
588if __name__ == "__main__":
589 run_main()