blob: 0a80d98d19ff00434a34a1e72fa77f7d7a863712 [file] [log] [blame]
Darryl Green7c2dd582018-03-01 14:53:49 +00001#!/usr/bin/env python3
Gilles Peskine4b9f7a22022-06-20 18:51:18 +02002"""This script compares the interfaces of two versions of Mbed TLS, looking
Gilles Peskinecfd4fae2021-04-23 16:37:12 +02003for backward incompatibilities between two different Git revisions within
4an Mbed TLS repository. It must be run from the root of a Git working tree.
5
6For the source (API) and runtime (ABI) interface compatibility, this script
7is a small wrapper around the abi-compliance-checker and abi-dumper tools,
8applying them to compare the header and library files.
9
10For the storage format, this script compares the automatically generated
Gilles Peskine2eae8d72022-02-22 19:02:44 +010011storage tests and the manual read tests, and complains if there is a
Gilles Peskine1177f372022-03-04 19:59:55 +010012reduction in coverage. A change in test data will be signaled as a
Gilles Peskine2eae8d72022-02-22 19:02:44 +010013coverage reduction since the old test data is no longer present. A change in
Gilles Peskine1177f372022-03-04 19:59:55 +010014how test data is presented will be signaled as well; this would be a false
Gilles Peskine2eae8d72022-02-22 19:02:44 +010015positive.
Gilles Peskinecfd4fae2021-04-23 16:37:12 +020016
Gilles Peskine2eae8d72022-02-22 19:02:44 +010017The results of the API/ABI comparison are either formatted as HTML and stored
18at a configurable location, or are given as a brief list of problems.
19Returns 0 on success, 1 on non-compliance, and 2 if there is an error
Gilles Peskinecfd4fae2021-04-23 16:37:12 +020020while running the script.
Gilles Peskine56354592022-03-03 10:23:09 +010021
Darryl Green78696802018-04-06 11:23:22 +010022"""
Darryl Green7c2dd582018-03-01 14:53:49 +000023
Bence Szépkúti1e148272020-08-07 13:07:28 +020024# Copyright The Mbed TLS Contributors
Bence Szépkútic7da1fe2020-05-26 01:54:15 +020025# SPDX-License-Identifier: Apache-2.0
26#
27# Licensed under the Apache License, Version 2.0 (the "License"); you may
28# not use this file except in compliance with the License.
29# You may obtain a copy of the License at
30#
31# http://www.apache.org/licenses/LICENSE-2.0
32#
33# Unless required by applicable law or agreed to in writing, software
34# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
35# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
36# See the License for the specific language governing permissions and
37# limitations under the License.
Bence Szépkútic7da1fe2020-05-26 01:54:15 +020038
Gilles Peskine2eae8d72022-02-22 19:02:44 +010039import glob
Darryl Green7c2dd582018-03-01 14:53:49 +000040import os
Gilles Peskinecfd4fae2021-04-23 16:37:12 +020041import re
Darryl Green7c2dd582018-03-01 14:53:49 +000042import sys
43import traceback
44import shutil
45import subprocess
46import argparse
47import logging
48import tempfile
Darryl Green9f357d62019-02-25 11:35:05 +000049import fnmatch
Darryl Green0d1ca512019-04-09 09:14:17 +010050from types import SimpleNamespace
Darryl Green7c2dd582018-03-01 14:53:49 +000051
Darryl Greene62f9bb2019-02-21 13:09:26 +000052import xml.etree.ElementTree as ET
53
Darryl Green7c2dd582018-03-01 14:53:49 +000054
Gilles Peskine184c0962020-03-24 18:25:17 +010055class AbiChecker:
Gilles Peskine712afa72019-02-25 20:36:52 +010056 """API and ABI checker."""
Darryl Green7c2dd582018-03-01 14:53:49 +000057
Darryl Green0d1ca512019-04-09 09:14:17 +010058 def __init__(self, old_version, new_version, configuration):
Gilles Peskine712afa72019-02-25 20:36:52 +010059 """Instantiate the API/ABI checker.
60
Darryl Green7c1a7332019-03-05 16:25:38 +000061 old_version: RepoVersion containing details to compare against
62 new_version: RepoVersion containing details to check
Darryl Greenf67e3492019-04-12 15:17:02 +010063 configuration.report_dir: directory for output files
64 configuration.keep_all_reports: if false, delete old reports
65 configuration.brief: if true, output shorter report to stdout
Gilles Peskine1177f372022-03-04 19:59:55 +010066 configuration.check_abi: if true, compare ABIs
Gilles Peskine793778f2021-04-23 16:32:32 +020067 configuration.check_api: if true, compare APIs
Gilles Peskinecfd4fae2021-04-23 16:37:12 +020068 configuration.check_storage: if true, compare storage format tests
Darryl Greenf67e3492019-04-12 15:17:02 +010069 configuration.skip_file: path to file containing symbols and types to skip
Gilles Peskine712afa72019-02-25 20:36:52 +010070 """
Darryl Green7c2dd582018-03-01 14:53:49 +000071 self.repo_path = "."
72 self.log = None
Darryl Green0d1ca512019-04-09 09:14:17 +010073 self.verbose = configuration.verbose
Darryl Green3a5f6c82019-03-05 16:30:39 +000074 self._setup_logger()
Darryl Green0d1ca512019-04-09 09:14:17 +010075 self.report_dir = os.path.abspath(configuration.report_dir)
76 self.keep_all_reports = configuration.keep_all_reports
Darryl Green492bc402019-04-11 15:50:41 +010077 self.can_remove_report_dir = not (os.path.exists(self.report_dir) or
Darryl Green0d1ca512019-04-09 09:14:17 +010078 self.keep_all_reports)
Darryl Green7c1a7332019-03-05 16:25:38 +000079 self.old_version = old_version
80 self.new_version = new_version
Darryl Green0d1ca512019-04-09 09:14:17 +010081 self.skip_file = configuration.skip_file
Gilles Peskine793778f2021-04-23 16:32:32 +020082 self.check_abi = configuration.check_abi
83 self.check_api = configuration.check_api
84 if self.check_abi != self.check_api:
85 raise Exception('Checking API without ABI or vice versa is not supported')
Gilles Peskinecfd4fae2021-04-23 16:37:12 +020086 self.check_storage_tests = configuration.check_storage
Darryl Green0d1ca512019-04-09 09:14:17 +010087 self.brief = configuration.brief
Darryl Green7c2dd582018-03-01 14:53:49 +000088 self.git_command = "git"
89 self.make_command = "make"
90
Gilles Peskine712afa72019-02-25 20:36:52 +010091 @staticmethod
92 def check_repo_path():
Gilles Peskine6aa32cc2019-07-04 18:59:36 +020093 if not all(os.path.isdir(d) for d in ["include", "library", "tests"]):
Darryl Green7c2dd582018-03-01 14:53:49 +000094 raise Exception("Must be run from Mbed TLS root")
95
Darryl Green3a5f6c82019-03-05 16:30:39 +000096 def _setup_logger(self):
Darryl Green7c2dd582018-03-01 14:53:49 +000097 self.log = logging.getLogger()
Darryl Green3c3da792019-03-08 11:30:04 +000098 if self.verbose:
99 self.log.setLevel(logging.DEBUG)
100 else:
101 self.log.setLevel(logging.INFO)
Darryl Green7c2dd582018-03-01 14:53:49 +0000102 self.log.addHandler(logging.StreamHandler())
103
Gilles Peskine712afa72019-02-25 20:36:52 +0100104 @staticmethod
105 def check_abi_tools_are_installed():
Darryl Green7c2dd582018-03-01 14:53:49 +0000106 for command in ["abi-dumper", "abi-compliance-checker"]:
107 if not shutil.which(command):
108 raise Exception("{} not installed, aborting".format(command))
109
Darryl Green3a5f6c82019-03-05 16:30:39 +0000110 def _get_clean_worktree_for_git_revision(self, version):
Darryl Green7c1a7332019-03-05 16:25:38 +0000111 """Make a separate worktree with version.revision checked out.
Gilles Peskine712afa72019-02-25 20:36:52 +0100112 Do not modify the current worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000113 git_worktree_path = tempfile.mkdtemp()
Darryl Green7c1a7332019-03-05 16:25:38 +0000114 if version.repository:
Darryl Green3c3da792019-03-08 11:30:04 +0000115 self.log.debug(
Darryl Greenda84e322019-02-19 16:59:33 +0000116 "Checking out git worktree for revision {} from {}".format(
Darryl Green7c1a7332019-03-05 16:25:38 +0000117 version.revision, version.repository
Darryl Greenda84e322019-02-19 16:59:33 +0000118 )
119 )
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100120 fetch_output = subprocess.check_output(
Darryl Green7c1a7332019-03-05 16:25:38 +0000121 [self.git_command, "fetch",
122 version.repository, version.revision],
Darryl Greenda84e322019-02-19 16:59:33 +0000123 cwd=self.repo_path,
Darryl Greenda84e322019-02-19 16:59:33 +0000124 stderr=subprocess.STDOUT
125 )
Darryl Green3c3da792019-03-08 11:30:04 +0000126 self.log.debug(fetch_output.decode("utf-8"))
Darryl Greenda84e322019-02-19 16:59:33 +0000127 worktree_rev = "FETCH_HEAD"
128 else:
Darryl Green3c3da792019-03-08 11:30:04 +0000129 self.log.debug("Checking out git worktree for revision {}".format(
Darryl Green7c1a7332019-03-05 16:25:38 +0000130 version.revision
131 ))
132 worktree_rev = version.revision
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100133 worktree_output = subprocess.check_output(
Darryl Greenda84e322019-02-19 16:59:33 +0000134 [self.git_command, "worktree", "add", "--detach",
135 git_worktree_path, worktree_rev],
Darryl Green7c2dd582018-03-01 14:53:49 +0000136 cwd=self.repo_path,
Darryl Green7c2dd582018-03-01 14:53:49 +0000137 stderr=subprocess.STDOUT
138 )
Darryl Green3c3da792019-03-08 11:30:04 +0000139 self.log.debug(worktree_output.decode("utf-8"))
Gilles Peskine3e2da4a2019-07-04 19:01:22 +0200140 version.commit = subprocess.check_output(
Darryl Green762351b2019-07-25 14:33:33 +0100141 [self.git_command, "rev-parse", "HEAD"],
Gilles Peskine3e2da4a2019-07-04 19:01:22 +0200142 cwd=git_worktree_path,
143 stderr=subprocess.STDOUT
144 ).decode("ascii").rstrip()
145 self.log.debug("Commit is {}".format(version.commit))
Darryl Green7c2dd582018-03-01 14:53:49 +0000146 return git_worktree_path
147
Darryl Green3a5f6c82019-03-05 16:30:39 +0000148 def _update_git_submodules(self, git_worktree_path, version):
Darryl Green8184df52019-04-05 17:06:17 +0100149 """If the crypto submodule is present, initialize it.
150 if version.crypto_revision exists, update it to that revision,
151 otherwise update it to the default revision"""
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100152 update_output = subprocess.check_output(
Jaeden Ameroffeb1b82018-11-02 16:35:09 +0000153 [self.git_command, "submodule", "update", "--init", '--recursive'],
154 cwd=git_worktree_path,
Jaeden Ameroffeb1b82018-11-02 16:35:09 +0000155 stderr=subprocess.STDOUT
156 )
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100157 self.log.debug(update_output.decode("utf-8"))
Darryl Greene29ce702019-03-05 15:23:25 +0000158 if not (os.path.exists(os.path.join(git_worktree_path, "crypto"))
Darryl Green7c1a7332019-03-05 16:25:38 +0000159 and version.crypto_revision):
Darryl Greene29ce702019-03-05 15:23:25 +0000160 return
161
Darryl Green7c1a7332019-03-05 16:25:38 +0000162 if version.crypto_repository:
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100163 fetch_output = subprocess.check_output(
Darryl Green1d95c532019-03-08 11:12:19 +0000164 [self.git_command, "fetch", version.crypto_repository,
165 version.crypto_revision],
Darryl Greene29ce702019-03-05 15:23:25 +0000166 cwd=os.path.join(git_worktree_path, "crypto"),
Darryl Greene29ce702019-03-05 15:23:25 +0000167 stderr=subprocess.STDOUT
168 )
Darryl Green3c3da792019-03-08 11:30:04 +0000169 self.log.debug(fetch_output.decode("utf-8"))
Darryl Green1d95c532019-03-08 11:12:19 +0000170 crypto_rev = "FETCH_HEAD"
171 else:
172 crypto_rev = version.crypto_revision
173
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100174 checkout_output = subprocess.check_output(
Darryl Green1d95c532019-03-08 11:12:19 +0000175 [self.git_command, "checkout", crypto_rev],
176 cwd=os.path.join(git_worktree_path, "crypto"),
Darryl Green1d95c532019-03-08 11:12:19 +0000177 stderr=subprocess.STDOUT
178 )
Darryl Green3c3da792019-03-08 11:30:04 +0000179 self.log.debug(checkout_output.decode("utf-8"))
Jaeden Ameroffeb1b82018-11-02 16:35:09 +0000180
Darryl Green3a5f6c82019-03-05 16:30:39 +0000181 def _build_shared_libraries(self, git_worktree_path, version):
Gilles Peskine712afa72019-02-25 20:36:52 +0100182 """Build the shared libraries in the specified worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000183 my_environment = os.environ.copy()
184 my_environment["CFLAGS"] = "-g -Og"
185 my_environment["SHARED"] = "1"
Darryl Greend2dba362019-05-09 13:03:05 +0100186 if os.path.exists(os.path.join(git_worktree_path, "crypto")):
187 my_environment["USE_CRYPTO_SUBMODULE"] = "1"
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100188 make_output = subprocess.check_output(
Darryl Greenddf25a62019-02-28 11:52:39 +0000189 [self.make_command, "lib"],
Darryl Green7c2dd582018-03-01 14:53:49 +0000190 env=my_environment,
191 cwd=git_worktree_path,
Darryl Green7c2dd582018-03-01 14:53:49 +0000192 stderr=subprocess.STDOUT
193 )
Darryl Green3c3da792019-03-08 11:30:04 +0000194 self.log.debug(make_output.decode("utf-8"))
Darryl Greenf025d532019-04-12 15:18:02 +0100195 for root, _dirs, files in os.walk(git_worktree_path):
Darryl Green9f357d62019-02-25 11:35:05 +0000196 for file in fnmatch.filter(files, "*.so"):
Darryl Green7c1a7332019-03-05 16:25:38 +0000197 version.modules[os.path.splitext(file)[0]] = (
Darryl Green3e7a9802019-02-27 16:53:40 +0000198 os.path.join(root, file)
Darryl Green9f357d62019-02-25 11:35:05 +0000199 )
Darryl Green7c2dd582018-03-01 14:53:49 +0000200
Gilles Peskine3e2da4a2019-07-04 19:01:22 +0200201 @staticmethod
202 def _pretty_revision(version):
203 if version.revision == version.commit:
204 return version.revision
205 else:
206 return "{} ({})".format(version.revision, version.commit)
207
Darryl Green8184df52019-04-05 17:06:17 +0100208 def _get_abi_dumps_from_shared_libraries(self, version):
Gilles Peskine712afa72019-02-25 20:36:52 +0100209 """Generate the ABI dumps for the specified git revision.
Darryl Green8184df52019-04-05 17:06:17 +0100210 The shared libraries must have been built and the module paths
211 present in version.modules."""
Darryl Green7c1a7332019-03-05 16:25:38 +0000212 for mbed_module, module_path in version.modules.items():
Darryl Green7c2dd582018-03-01 14:53:49 +0000213 output_path = os.path.join(
Darryl Greenfe9a6752019-04-04 14:39:33 +0100214 self.report_dir, "{}-{}-{}.dump".format(
215 mbed_module, version.revision, version.version
Darryl Green3e7a9802019-02-27 16:53:40 +0000216 )
Darryl Green7c2dd582018-03-01 14:53:49 +0000217 )
218 abi_dump_command = [
219 "abi-dumper",
Darryl Green9f357d62019-02-25 11:35:05 +0000220 module_path,
Darryl Green7c2dd582018-03-01 14:53:49 +0000221 "-o", output_path,
Gilles Peskine3e2da4a2019-07-04 19:01:22 +0200222 "-lver", self._pretty_revision(version),
Darryl Green7c2dd582018-03-01 14:53:49 +0000223 ]
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100224 abi_dump_output = subprocess.check_output(
Darryl Green7c2dd582018-03-01 14:53:49 +0000225 abi_dump_command,
Darryl Green7c2dd582018-03-01 14:53:49 +0000226 stderr=subprocess.STDOUT
227 )
Darryl Green3c3da792019-03-08 11:30:04 +0000228 self.log.debug(abi_dump_output.decode("utf-8"))
Darryl Green7c1a7332019-03-05 16:25:38 +0000229 version.abi_dumps[mbed_module] = output_path
Darryl Green7c2dd582018-03-01 14:53:49 +0000230
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200231 @staticmethod
232 def _normalize_storage_test_case_data(line):
233 """Eliminate cosmetic or irrelevant details in storage format test cases."""
234 line = re.sub(r'\s+', r'', line)
235 return line
236
Gilles Peskine2eae8d72022-02-22 19:02:44 +0100237 def _read_storage_tests(self,
238 directory,
239 filename,
240 is_generated,
241 storage_tests):
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200242 """Record storage tests from the given file.
243
244 Populate the storage_tests dictionary with test cases read from
245 filename under directory.
246 """
247 at_paragraph_start = True
248 description = None
249 full_path = os.path.join(directory, filename)
Gilles Peskineaeb8d662022-03-04 20:02:00 +0100250 with open(full_path) as fd:
251 for line_number, line in enumerate(fd, 1):
252 line = line.strip()
253 if not line:
254 at_paragraph_start = True
Gilles Peskine2eae8d72022-02-22 19:02:44 +0100255 continue
Gilles Peskineaeb8d662022-03-04 20:02:00 +0100256 if line.startswith('#'):
257 continue
258 if at_paragraph_start:
259 description = line.strip()
260 at_paragraph_start = False
261 continue
262 if line.startswith('depends_on:'):
263 continue
264 # We've reached a test case data line
265 test_case_data = self._normalize_storage_test_case_data(line)
266 if not is_generated:
267 # In manual test data, only look at read tests.
268 function_name = test_case_data.split(':', 1)[0]
269 if 'read' not in function_name.split('_'):
270 continue
271 metadata = SimpleNamespace(
272 filename=filename,
273 line_number=line_number,
274 description=description
275 )
276 storage_tests[test_case_data] = metadata
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200277
Gilles Peskine2eae8d72022-02-22 19:02:44 +0100278 @staticmethod
279 def _list_generated_test_data_files(git_worktree_path):
280 """List the generated test data files."""
281 output = subprocess.check_output(
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200282 ['tests/scripts/generate_psa_tests.py', '--list'],
283 cwd=git_worktree_path,
284 ).decode('ascii')
Gilles Peskine2eae8d72022-02-22 19:02:44 +0100285 return [line for line in output.split('\n') if line]
286
287 def _get_storage_format_tests(self, version, git_worktree_path):
288 """Record the storage format tests for the specified git version.
289
290 The storage format tests are the test suite data files whose name
291 contains "storage_format".
292
293 The version must be checked out at git_worktree_path.
294
295 This function creates or updates the generated data files.
296 """
297 # Existing test data files. This may be missing some automatically
298 # generated files if they haven't been generated yet.
299 storage_data_files = set(glob.glob(
300 'tests/suites/test_suite_*storage_format*.data'
301 ))
302 # Discover and (re)generate automatically generated data files.
303 to_be_generated = set()
304 for filename in self._list_generated_test_data_files(git_worktree_path):
305 if 'storage_format' in filename:
306 storage_data_files.add(filename)
307 to_be_generated.add(filename)
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200308 subprocess.check_call(
Gilles Peskine2eae8d72022-02-22 19:02:44 +0100309 ['tests/scripts/generate_psa_tests.py'] + sorted(to_be_generated),
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200310 cwd=git_worktree_path,
311 )
Gilles Peskine2eae8d72022-02-22 19:02:44 +0100312 for test_file in sorted(storage_data_files):
313 self._read_storage_tests(git_worktree_path,
314 test_file,
315 test_file in to_be_generated,
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200316 version.storage_tests)
317
Darryl Green3a5f6c82019-03-05 16:30:39 +0000318 def _cleanup_worktree(self, git_worktree_path):
Gilles Peskine712afa72019-02-25 20:36:52 +0100319 """Remove the specified git worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000320 shutil.rmtree(git_worktree_path)
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100321 worktree_output = subprocess.check_output(
Darryl Green7c2dd582018-03-01 14:53:49 +0000322 [self.git_command, "worktree", "prune"],
323 cwd=self.repo_path,
Darryl Green7c2dd582018-03-01 14:53:49 +0000324 stderr=subprocess.STDOUT
325 )
Darryl Green3c3da792019-03-08 11:30:04 +0000326 self.log.debug(worktree_output.decode("utf-8"))
Darryl Green7c2dd582018-03-01 14:53:49 +0000327
Darryl Green3a5f6c82019-03-05 16:30:39 +0000328 def _get_abi_dump_for_ref(self, version):
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200329 """Generate the interface information for the specified git revision."""
Darryl Green3a5f6c82019-03-05 16:30:39 +0000330 git_worktree_path = self._get_clean_worktree_for_git_revision(version)
331 self._update_git_submodules(git_worktree_path, version)
Gilles Peskine793778f2021-04-23 16:32:32 +0200332 if self.check_abi:
333 self._build_shared_libraries(git_worktree_path, version)
334 self._get_abi_dumps_from_shared_libraries(version)
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200335 if self.check_storage_tests:
336 self._get_storage_format_tests(version, git_worktree_path)
Darryl Green3a5f6c82019-03-05 16:30:39 +0000337 self._cleanup_worktree(git_worktree_path)
Darryl Green7c2dd582018-03-01 14:53:49 +0000338
Darryl Green3a5f6c82019-03-05 16:30:39 +0000339 def _remove_children_with_tag(self, parent, tag):
Darryl Greene62f9bb2019-02-21 13:09:26 +0000340 children = parent.getchildren()
341 for child in children:
342 if child.tag == tag:
343 parent.remove(child)
344 else:
Darryl Green3a5f6c82019-03-05 16:30:39 +0000345 self._remove_children_with_tag(child, tag)
Darryl Greene62f9bb2019-02-21 13:09:26 +0000346
Darryl Green3a5f6c82019-03-05 16:30:39 +0000347 def _remove_extra_detail_from_report(self, report_root):
Darryl Greene62f9bb2019-02-21 13:09:26 +0000348 for tag in ['test_info', 'test_results', 'problem_summary',
Darryl Greenc6f874b2019-06-05 12:57:50 +0100349 'added_symbols', 'affected']:
Darryl Green3a5f6c82019-03-05 16:30:39 +0000350 self._remove_children_with_tag(report_root, tag)
Darryl Greene62f9bb2019-02-21 13:09:26 +0000351
352 for report in report_root:
353 for problems in report.getchildren()[:]:
354 if not problems.getchildren():
355 report.remove(problems)
356
Gilles Peskineada828f2019-07-04 19:17:40 +0200357 def _abi_compliance_command(self, mbed_module, output_path):
358 """Build the command to run to analyze the library mbed_module.
359 The report will be placed in output_path."""
360 abi_compliance_command = [
361 "abi-compliance-checker",
362 "-l", mbed_module,
363 "-old", self.old_version.abi_dumps[mbed_module],
364 "-new", self.new_version.abi_dumps[mbed_module],
365 "-strict",
366 "-report-path", output_path,
367 ]
368 if self.skip_file:
369 abi_compliance_command += ["-skip-symbols", self.skip_file,
370 "-skip-types", self.skip_file]
371 if self.brief:
372 abi_compliance_command += ["-report-format", "xml",
373 "-stdout"]
374 return abi_compliance_command
375
376 def _is_library_compatible(self, mbed_module, compatibility_report):
377 """Test if the library mbed_module has remained compatible.
378 Append a message regarding compatibility to compatibility_report."""
379 output_path = os.path.join(
380 self.report_dir, "{}-{}-{}.html".format(
381 mbed_module, self.old_version.revision,
382 self.new_version.revision
383 )
384 )
385 try:
386 subprocess.check_output(
387 self._abi_compliance_command(mbed_module, output_path),
388 stderr=subprocess.STDOUT
389 )
390 except subprocess.CalledProcessError as err:
391 if err.returncode != 1:
392 raise err
393 if self.brief:
394 self.log.info(
395 "Compatibility issues found for {}".format(mbed_module)
396 )
397 report_root = ET.fromstring(err.output.decode("utf-8"))
398 self._remove_extra_detail_from_report(report_root)
399 self.log.info(ET.tostring(report_root).decode("utf-8"))
400 else:
401 self.can_remove_report_dir = False
402 compatibility_report.append(
403 "Compatibility issues found for {}, "
404 "for details see {}".format(mbed_module, output_path)
405 )
406 return False
407 compatibility_report.append(
408 "No compatibility issues for {}".format(mbed_module)
409 )
410 if not (self.keep_all_reports or self.brief):
411 os.remove(output_path)
412 return True
413
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200414 @staticmethod
415 def _is_storage_format_compatible(old_tests, new_tests,
416 compatibility_report):
417 """Check whether all tests present in old_tests are also in new_tests.
418
419 Append a message regarding compatibility to compatibility_report.
420 """
421 missing = frozenset(old_tests.keys()).difference(new_tests.keys())
422 for test_data in sorted(missing):
423 metadata = old_tests[test_data]
424 compatibility_report.append(
425 'Test case from {} line {} "{}" has disappeared: {}'.format(
426 metadata.filename, metadata.line_number,
427 metadata.description, test_data
428 )
429 )
430 compatibility_report.append(
431 'FAIL: {}/{} storage format test cases have changed or disappeared.'.format(
432 len(missing), len(old_tests)
433 ) if missing else
434 'PASS: All {} storage format test cases are preserved.'.format(
435 len(old_tests)
436 )
437 )
438 compatibility_report.append(
439 'Info: number of storage format tests cases: {} -> {}.'.format(
440 len(old_tests), len(new_tests)
441 )
442 )
443 return not missing
444
Darryl Green7c2dd582018-03-01 14:53:49 +0000445 def get_abi_compatibility_report(self):
Gilles Peskine712afa72019-02-25 20:36:52 +0100446 """Generate a report of the differences between the reference ABI
Darryl Green8184df52019-04-05 17:06:17 +0100447 and the new ABI. ABI dumps from self.old_version and self.new_version
448 must be available."""
Gilles Peskineada828f2019-07-04 19:17:40 +0200449 compatibility_report = ["Checking evolution from {} to {}".format(
Gilles Peskine3e2da4a2019-07-04 19:01:22 +0200450 self._pretty_revision(self.old_version),
451 self._pretty_revision(self.new_version)
Gilles Peskineada828f2019-07-04 19:17:40 +0200452 )]
Darryl Green7c2dd582018-03-01 14:53:49 +0000453 compliance_return_code = 0
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200454
Gilles Peskine793778f2021-04-23 16:32:32 +0200455 if self.check_abi:
456 shared_modules = list(set(self.old_version.modules.keys()) &
457 set(self.new_version.modules.keys()))
458 for mbed_module in shared_modules:
459 if not self._is_library_compatible(mbed_module,
460 compatibility_report):
461 compliance_return_code = 1
462
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200463 if self.check_storage_tests:
464 if not self._is_storage_format_compatible(
465 self.old_version.storage_tests,
466 self.new_version.storage_tests,
467 compatibility_report):
Gilles Peskineada828f2019-07-04 19:17:40 +0200468 compliance_return_code = 1
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200469
Darryl Greenf2688e22019-05-29 11:29:08 +0100470 for version in [self.old_version, self.new_version]:
471 for mbed_module, mbed_module_dump in version.abi_dumps.items():
472 os.remove(mbed_module_dump)
Darryl Green3d3d5522019-02-25 17:01:55 +0000473 if self.can_remove_report_dir:
Darryl Green7c2dd582018-03-01 14:53:49 +0000474 os.rmdir(self.report_dir)
Gilles Peskineada828f2019-07-04 19:17:40 +0200475 self.log.info("\n".join(compatibility_report))
Darryl Green7c2dd582018-03-01 14:53:49 +0000476 return compliance_return_code
477
478 def check_for_abi_changes(self):
Gilles Peskine712afa72019-02-25 20:36:52 +0100479 """Generate a report of ABI differences
480 between self.old_rev and self.new_rev."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000481 self.check_repo_path()
Gilles Peskinef548a0c2022-03-03 10:22:36 +0100482 if self.check_api or self.check_abi:
483 self.check_abi_tools_are_installed()
Darryl Green3a5f6c82019-03-05 16:30:39 +0000484 self._get_abi_dump_for_ref(self.old_version)
485 self._get_abi_dump_for_ref(self.new_version)
Darryl Green7c2dd582018-03-01 14:53:49 +0000486 return self.get_abi_compatibility_report()
487
488
489def run_main():
490 try:
491 parser = argparse.ArgumentParser(
Gilles Peskine56354592022-03-03 10:23:09 +0100492 description=__doc__
Darryl Green7c2dd582018-03-01 14:53:49 +0000493 )
494 parser.add_argument(
Darryl Green3c3da792019-03-08 11:30:04 +0000495 "-v", "--verbose", action="store_true",
496 help="set verbosity level",
497 )
498 parser.add_argument(
Darryl Green418527b2018-04-16 12:02:29 +0100499 "-r", "--report-dir", type=str, default="reports",
Darryl Green7c2dd582018-03-01 14:53:49 +0000500 help="directory where reports are stored, default is reports",
501 )
502 parser.add_argument(
Darryl Green418527b2018-04-16 12:02:29 +0100503 "-k", "--keep-all-reports", action="store_true",
Darryl Green7c2dd582018-03-01 14:53:49 +0000504 help="keep all reports, even if there are no compatibility issues",
505 )
506 parser.add_argument(
Darryl Greenc5132ff2019-03-01 09:54:44 +0000507 "-o", "--old-rev", type=str, help="revision for old version.",
508 required=True,
Darryl Green7c2dd582018-03-01 14:53:49 +0000509 )
510 parser.add_argument(
Darryl Greenc5132ff2019-03-01 09:54:44 +0000511 "-or", "--old-repo", type=str, help="repository for old version."
Darryl Green9f357d62019-02-25 11:35:05 +0000512 )
513 parser.add_argument(
Darryl Greenc5132ff2019-03-01 09:54:44 +0000514 "-oc", "--old-crypto-rev", type=str,
515 help="revision for old crypto submodule."
Darryl Green7c2dd582018-03-01 14:53:49 +0000516 )
Darryl Greenc2883a22019-02-20 15:01:56 +0000517 parser.add_argument(
Darryl Greenc5132ff2019-03-01 09:54:44 +0000518 "-ocr", "--old-crypto-repo", type=str,
519 help="repository for old crypto submodule."
520 )
521 parser.add_argument(
522 "-n", "--new-rev", type=str, help="revision for new version",
523 required=True,
524 )
525 parser.add_argument(
526 "-nr", "--new-repo", type=str, help="repository for new version."
527 )
528 parser.add_argument(
529 "-nc", "--new-crypto-rev", type=str,
530 help="revision for new crypto version"
531 )
532 parser.add_argument(
533 "-ncr", "--new-crypto-repo", type=str,
534 help="repository for new crypto submodule."
Darryl Green9f357d62019-02-25 11:35:05 +0000535 )
536 parser.add_argument(
Darryl Greenc2883a22019-02-20 15:01:56 +0000537 "-s", "--skip-file", type=str,
Gilles Peskineb6ce2342019-07-04 19:00:31 +0200538 help=("path to file containing symbols and types to skip "
539 "(typically \"-s identifiers\" after running "
540 "\"tests/scripts/list-identifiers.sh --internal\")")
Darryl Greenc2883a22019-02-20 15:01:56 +0000541 )
Darryl Greene62f9bb2019-02-21 13:09:26 +0000542 parser.add_argument(
Gilles Peskine793778f2021-04-23 16:32:32 +0200543 "--check-abi",
544 action='store_true', default=True,
545 help="Perform ABI comparison (default: yes)"
546 )
547 parser.add_argument("--no-check-abi", action='store_false', dest='check_abi')
548 parser.add_argument(
549 "--check-api",
550 action='store_true', default=True,
551 help="Perform API comparison (default: yes)"
552 )
553 parser.add_argument("--no-check-api", action='store_false', dest='check_api')
554 parser.add_argument(
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200555 "--check-storage",
556 action='store_true', default=True,
557 help="Perform storage tests comparison (default: yes)"
558 )
559 parser.add_argument("--no-check-storage", action='store_false', dest='check_storage')
560 parser.add_argument(
Darryl Greene62f9bb2019-02-21 13:09:26 +0000561 "-b", "--brief", action="store_true",
562 help="output only the list of issues to stdout, instead of a full report",
563 )
Darryl Green7c2dd582018-03-01 14:53:49 +0000564 abi_args = parser.parse_args()
Darryl Green492bc402019-04-11 15:50:41 +0100565 if os.path.isfile(abi_args.report_dir):
566 print("Error: {} is not a directory".format(abi_args.report_dir))
567 parser.exit()
Darryl Green0d1ca512019-04-09 09:14:17 +0100568 old_version = SimpleNamespace(
569 version="old",
570 repository=abi_args.old_repo,
571 revision=abi_args.old_rev,
Gilles Peskine3e2da4a2019-07-04 19:01:22 +0200572 commit=None,
Darryl Green0d1ca512019-04-09 09:14:17 +0100573 crypto_repository=abi_args.old_crypto_repo,
574 crypto_revision=abi_args.old_crypto_rev,
575 abi_dumps={},
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200576 storage_tests={},
Darryl Green0d1ca512019-04-09 09:14:17 +0100577 modules={}
Darryl Green8184df52019-04-05 17:06:17 +0100578 )
Darryl Green0d1ca512019-04-09 09:14:17 +0100579 new_version = SimpleNamespace(
580 version="new",
581 repository=abi_args.new_repo,
582 revision=abi_args.new_rev,
Gilles Peskine3e2da4a2019-07-04 19:01:22 +0200583 commit=None,
Darryl Green0d1ca512019-04-09 09:14:17 +0100584 crypto_repository=abi_args.new_crypto_repo,
585 crypto_revision=abi_args.new_crypto_rev,
586 abi_dumps={},
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200587 storage_tests={},
Darryl Green0d1ca512019-04-09 09:14:17 +0100588 modules={}
Darryl Green8184df52019-04-05 17:06:17 +0100589 )
Darryl Green0d1ca512019-04-09 09:14:17 +0100590 configuration = SimpleNamespace(
591 verbose=abi_args.verbose,
592 report_dir=abi_args.report_dir,
593 keep_all_reports=abi_args.keep_all_reports,
594 brief=abi_args.brief,
Gilles Peskine793778f2021-04-23 16:32:32 +0200595 check_abi=abi_args.check_abi,
596 check_api=abi_args.check_api,
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200597 check_storage=abi_args.check_storage,
Darryl Green0d1ca512019-04-09 09:14:17 +0100598 skip_file=abi_args.skip_file
Darryl Green7c2dd582018-03-01 14:53:49 +0000599 )
Darryl Green0d1ca512019-04-09 09:14:17 +0100600 abi_check = AbiChecker(old_version, new_version, configuration)
Darryl Green7c2dd582018-03-01 14:53:49 +0000601 return_code = abi_check.check_for_abi_changes()
602 sys.exit(return_code)
Gilles Peskinee915d532019-02-25 21:39:42 +0100603 except Exception: # pylint: disable=broad-except
604 # Print the backtrace and exit explicitly so as to exit with
605 # status 2, not 1.
Darryl Greena6f430f2018-03-15 10:12:06 +0000606 traceback.print_exc()
Darryl Green7c2dd582018-03-01 14:53:49 +0000607 sys.exit(2)
608
609
610if __name__ == "__main__":
611 run_main()