blob: f11cdf25928f7db2afb3bdf9c8582ca9f8c1a4dc [file] [log] [blame]
Darryl Green7c2dd582018-03-01 14:53:49 +00001#!/usr/bin/env python3
Darryl Green78696802018-04-06 11:23:22 +01002"""
Gilles Peskinecfd4fae2021-04-23 16:37:12 +02003This script compares the interfaces of two versions of Mbed TLS, looking
4for backward incompatibilities between two different Git revisions within
5an Mbed TLS repository. It must be run from the root of a Git working tree.
6
7For the source (API) and runtime (ABI) interface compatibility, this script
8is a small wrapper around the abi-compliance-checker and abi-dumper tools,
9applying them to compare the header and library files.
10
11For the storage format, this script compares the automatically generated
Gilles Peskine2eae8d72022-02-22 19:02:44 +010012storage tests and the manual read tests, and complains if there is a
Gilles Peskine1177f372022-03-04 19:59:55 +010013reduction in coverage. A change in test data will be signaled as a
Gilles Peskine2eae8d72022-02-22 19:02:44 +010014coverage reduction since the old test data is no longer present. A change in
Gilles Peskine1177f372022-03-04 19:59:55 +010015how test data is presented will be signaled as well; this would be a false
Gilles Peskine2eae8d72022-02-22 19:02:44 +010016positive.
Gilles Peskinecfd4fae2021-04-23 16:37:12 +020017
Gilles Peskine2eae8d72022-02-22 19:02:44 +010018The results of the API/ABI comparison are either formatted as HTML and stored
19at a configurable location, or are given as a brief list of problems.
20Returns 0 on success, 1 on non-compliance, and 2 if there is an error
Gilles Peskinecfd4fae2021-04-23 16:37:12 +020021while running the script.
Gilles Peskine56354592022-03-03 10:23:09 +010022
23You must run this test from an Mbed TLS root.
Darryl Green78696802018-04-06 11:23:22 +010024"""
Darryl Green7c2dd582018-03-01 14:53:49 +000025
Bence Szépkúti1e148272020-08-07 13:07:28 +020026# Copyright The Mbed TLS Contributors
Bence Szépkútic7da1fe2020-05-26 01:54:15 +020027# SPDX-License-Identifier: Apache-2.0
28#
29# Licensed under the Apache License, Version 2.0 (the "License"); you may
30# not use this file except in compliance with the License.
31# You may obtain a copy of the License at
32#
33# http://www.apache.org/licenses/LICENSE-2.0
34#
35# Unless required by applicable law or agreed to in writing, software
36# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
37# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
38# See the License for the specific language governing permissions and
39# limitations under the License.
Bence Szépkútic7da1fe2020-05-26 01:54:15 +020040
Gilles Peskine2eae8d72022-02-22 19:02:44 +010041import glob
Darryl Green7c2dd582018-03-01 14:53:49 +000042import os
Gilles Peskinecfd4fae2021-04-23 16:37:12 +020043import re
Darryl Green7c2dd582018-03-01 14:53:49 +000044import sys
45import traceback
46import shutil
47import subprocess
48import argparse
49import logging
50import tempfile
Darryl Green9f357d62019-02-25 11:35:05 +000051import fnmatch
Darryl Green0d1ca512019-04-09 09:14:17 +010052from types import SimpleNamespace
Darryl Green7c2dd582018-03-01 14:53:49 +000053
Darryl Greene62f9bb2019-02-21 13:09:26 +000054import xml.etree.ElementTree as ET
55
Darryl Green7c2dd582018-03-01 14:53:49 +000056
Gilles Peskine184c0962020-03-24 18:25:17 +010057class AbiChecker:
Gilles Peskine712afa72019-02-25 20:36:52 +010058 """API and ABI checker."""
Darryl Green7c2dd582018-03-01 14:53:49 +000059
Darryl Green0d1ca512019-04-09 09:14:17 +010060 def __init__(self, old_version, new_version, configuration):
Gilles Peskine712afa72019-02-25 20:36:52 +010061 """Instantiate the API/ABI checker.
62
Darryl Green7c1a7332019-03-05 16:25:38 +000063 old_version: RepoVersion containing details to compare against
64 new_version: RepoVersion containing details to check
Darryl Greenf67e3492019-04-12 15:17:02 +010065 configuration.report_dir: directory for output files
66 configuration.keep_all_reports: if false, delete old reports
67 configuration.brief: if true, output shorter report to stdout
Gilles Peskine1177f372022-03-04 19:59:55 +010068 configuration.check_abi: if true, compare ABIs
Gilles Peskine793778f2021-04-23 16:32:32 +020069 configuration.check_api: if true, compare APIs
Gilles Peskinecfd4fae2021-04-23 16:37:12 +020070 configuration.check_storage: if true, compare storage format tests
Darryl Greenf67e3492019-04-12 15:17:02 +010071 configuration.skip_file: path to file containing symbols and types to skip
Gilles Peskine712afa72019-02-25 20:36:52 +010072 """
Darryl Green7c2dd582018-03-01 14:53:49 +000073 self.repo_path = "."
74 self.log = None
Darryl Green0d1ca512019-04-09 09:14:17 +010075 self.verbose = configuration.verbose
Darryl Green3a5f6c82019-03-05 16:30:39 +000076 self._setup_logger()
Darryl Green0d1ca512019-04-09 09:14:17 +010077 self.report_dir = os.path.abspath(configuration.report_dir)
78 self.keep_all_reports = configuration.keep_all_reports
Darryl Green492bc402019-04-11 15:50:41 +010079 self.can_remove_report_dir = not (os.path.exists(self.report_dir) or
Darryl Green0d1ca512019-04-09 09:14:17 +010080 self.keep_all_reports)
Darryl Green7c1a7332019-03-05 16:25:38 +000081 self.old_version = old_version
82 self.new_version = new_version
Darryl Green0d1ca512019-04-09 09:14:17 +010083 self.skip_file = configuration.skip_file
Gilles Peskine793778f2021-04-23 16:32:32 +020084 self.check_abi = configuration.check_abi
85 self.check_api = configuration.check_api
86 if self.check_abi != self.check_api:
87 raise Exception('Checking API without ABI or vice versa is not supported')
Gilles Peskinecfd4fae2021-04-23 16:37:12 +020088 self.check_storage_tests = configuration.check_storage
Darryl Green0d1ca512019-04-09 09:14:17 +010089 self.brief = configuration.brief
Darryl Green7c2dd582018-03-01 14:53:49 +000090 self.git_command = "git"
91 self.make_command = "make"
92
Gilles Peskine712afa72019-02-25 20:36:52 +010093 @staticmethod
94 def check_repo_path():
Gilles Peskine6aa32cc2019-07-04 18:59:36 +020095 if not all(os.path.isdir(d) for d in ["include", "library", "tests"]):
Darryl Green7c2dd582018-03-01 14:53:49 +000096 raise Exception("Must be run from Mbed TLS root")
97
Darryl Green3a5f6c82019-03-05 16:30:39 +000098 def _setup_logger(self):
Darryl Green7c2dd582018-03-01 14:53:49 +000099 self.log = logging.getLogger()
Darryl Green3c3da792019-03-08 11:30:04 +0000100 if self.verbose:
101 self.log.setLevel(logging.DEBUG)
102 else:
103 self.log.setLevel(logging.INFO)
Darryl Green7c2dd582018-03-01 14:53:49 +0000104 self.log.addHandler(logging.StreamHandler())
105
Gilles Peskine712afa72019-02-25 20:36:52 +0100106 @staticmethod
107 def check_abi_tools_are_installed():
Darryl Green7c2dd582018-03-01 14:53:49 +0000108 for command in ["abi-dumper", "abi-compliance-checker"]:
109 if not shutil.which(command):
110 raise Exception("{} not installed, aborting".format(command))
111
Darryl Green3a5f6c82019-03-05 16:30:39 +0000112 def _get_clean_worktree_for_git_revision(self, version):
Darryl Green7c1a7332019-03-05 16:25:38 +0000113 """Make a separate worktree with version.revision checked out.
Gilles Peskine712afa72019-02-25 20:36:52 +0100114 Do not modify the current worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000115 git_worktree_path = tempfile.mkdtemp()
Darryl Green7c1a7332019-03-05 16:25:38 +0000116 if version.repository:
Darryl Green3c3da792019-03-08 11:30:04 +0000117 self.log.debug(
Darryl Greenda84e322019-02-19 16:59:33 +0000118 "Checking out git worktree for revision {} from {}".format(
Darryl Green7c1a7332019-03-05 16:25:38 +0000119 version.revision, version.repository
Darryl Greenda84e322019-02-19 16:59:33 +0000120 )
121 )
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100122 fetch_output = subprocess.check_output(
Darryl Green7c1a7332019-03-05 16:25:38 +0000123 [self.git_command, "fetch",
124 version.repository, version.revision],
Darryl Greenda84e322019-02-19 16:59:33 +0000125 cwd=self.repo_path,
Darryl Greenda84e322019-02-19 16:59:33 +0000126 stderr=subprocess.STDOUT
127 )
Darryl Green3c3da792019-03-08 11:30:04 +0000128 self.log.debug(fetch_output.decode("utf-8"))
Darryl Greenda84e322019-02-19 16:59:33 +0000129 worktree_rev = "FETCH_HEAD"
130 else:
Darryl Green3c3da792019-03-08 11:30:04 +0000131 self.log.debug("Checking out git worktree for revision {}".format(
Darryl Green7c1a7332019-03-05 16:25:38 +0000132 version.revision
133 ))
134 worktree_rev = version.revision
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100135 worktree_output = subprocess.check_output(
Darryl Greenda84e322019-02-19 16:59:33 +0000136 [self.git_command, "worktree", "add", "--detach",
137 git_worktree_path, worktree_rev],
Darryl Green7c2dd582018-03-01 14:53:49 +0000138 cwd=self.repo_path,
Darryl Green7c2dd582018-03-01 14:53:49 +0000139 stderr=subprocess.STDOUT
140 )
Darryl Green3c3da792019-03-08 11:30:04 +0000141 self.log.debug(worktree_output.decode("utf-8"))
Gilles Peskine3e2da4a2019-07-04 19:01:22 +0200142 version.commit = subprocess.check_output(
Darryl Green762351b2019-07-25 14:33:33 +0100143 [self.git_command, "rev-parse", "HEAD"],
Gilles Peskine3e2da4a2019-07-04 19:01:22 +0200144 cwd=git_worktree_path,
145 stderr=subprocess.STDOUT
146 ).decode("ascii").rstrip()
147 self.log.debug("Commit is {}".format(version.commit))
Darryl Green7c2dd582018-03-01 14:53:49 +0000148 return git_worktree_path
149
Darryl Green3a5f6c82019-03-05 16:30:39 +0000150 def _update_git_submodules(self, git_worktree_path, version):
Darryl Green8184df52019-04-05 17:06:17 +0100151 """If the crypto submodule is present, initialize it.
152 if version.crypto_revision exists, update it to that revision,
153 otherwise update it to the default revision"""
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100154 update_output = subprocess.check_output(
Jaeden Ameroffeb1b82018-11-02 16:35:09 +0000155 [self.git_command, "submodule", "update", "--init", '--recursive'],
156 cwd=git_worktree_path,
Jaeden Ameroffeb1b82018-11-02 16:35:09 +0000157 stderr=subprocess.STDOUT
158 )
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100159 self.log.debug(update_output.decode("utf-8"))
Darryl Greene29ce702019-03-05 15:23:25 +0000160 if not (os.path.exists(os.path.join(git_worktree_path, "crypto"))
Darryl Green7c1a7332019-03-05 16:25:38 +0000161 and version.crypto_revision):
Darryl Greene29ce702019-03-05 15:23:25 +0000162 return
163
Darryl Green7c1a7332019-03-05 16:25:38 +0000164 if version.crypto_repository:
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100165 fetch_output = subprocess.check_output(
Darryl Green1d95c532019-03-08 11:12:19 +0000166 [self.git_command, "fetch", version.crypto_repository,
167 version.crypto_revision],
Darryl Greene29ce702019-03-05 15:23:25 +0000168 cwd=os.path.join(git_worktree_path, "crypto"),
Darryl Greene29ce702019-03-05 15:23:25 +0000169 stderr=subprocess.STDOUT
170 )
Darryl Green3c3da792019-03-08 11:30:04 +0000171 self.log.debug(fetch_output.decode("utf-8"))
Darryl Green1d95c532019-03-08 11:12:19 +0000172 crypto_rev = "FETCH_HEAD"
173 else:
174 crypto_rev = version.crypto_revision
175
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100176 checkout_output = subprocess.check_output(
Darryl Green1d95c532019-03-08 11:12:19 +0000177 [self.git_command, "checkout", crypto_rev],
178 cwd=os.path.join(git_worktree_path, "crypto"),
Darryl Green1d95c532019-03-08 11:12:19 +0000179 stderr=subprocess.STDOUT
180 )
Darryl Green3c3da792019-03-08 11:30:04 +0000181 self.log.debug(checkout_output.decode("utf-8"))
Jaeden Ameroffeb1b82018-11-02 16:35:09 +0000182
Darryl Green3a5f6c82019-03-05 16:30:39 +0000183 def _build_shared_libraries(self, git_worktree_path, version):
Gilles Peskine712afa72019-02-25 20:36:52 +0100184 """Build the shared libraries in the specified worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000185 my_environment = os.environ.copy()
186 my_environment["CFLAGS"] = "-g -Og"
187 my_environment["SHARED"] = "1"
Darryl Greend2dba362019-05-09 13:03:05 +0100188 if os.path.exists(os.path.join(git_worktree_path, "crypto")):
189 my_environment["USE_CRYPTO_SUBMODULE"] = "1"
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100190 make_output = subprocess.check_output(
Darryl Greenddf25a62019-02-28 11:52:39 +0000191 [self.make_command, "lib"],
Darryl Green7c2dd582018-03-01 14:53:49 +0000192 env=my_environment,
193 cwd=git_worktree_path,
Darryl Green7c2dd582018-03-01 14:53:49 +0000194 stderr=subprocess.STDOUT
195 )
Darryl Green3c3da792019-03-08 11:30:04 +0000196 self.log.debug(make_output.decode("utf-8"))
Darryl Greenf025d532019-04-12 15:18:02 +0100197 for root, _dirs, files in os.walk(git_worktree_path):
Darryl Green9f357d62019-02-25 11:35:05 +0000198 for file in fnmatch.filter(files, "*.so"):
Darryl Green7c1a7332019-03-05 16:25:38 +0000199 version.modules[os.path.splitext(file)[0]] = (
Darryl Green3e7a9802019-02-27 16:53:40 +0000200 os.path.join(root, file)
Darryl Green9f357d62019-02-25 11:35:05 +0000201 )
Darryl Green7c2dd582018-03-01 14:53:49 +0000202
Gilles Peskine3e2da4a2019-07-04 19:01:22 +0200203 @staticmethod
204 def _pretty_revision(version):
205 if version.revision == version.commit:
206 return version.revision
207 else:
208 return "{} ({})".format(version.revision, version.commit)
209
Darryl Green8184df52019-04-05 17:06:17 +0100210 def _get_abi_dumps_from_shared_libraries(self, version):
Gilles Peskine712afa72019-02-25 20:36:52 +0100211 """Generate the ABI dumps for the specified git revision.
Darryl Green8184df52019-04-05 17:06:17 +0100212 The shared libraries must have been built and the module paths
213 present in version.modules."""
Darryl Green7c1a7332019-03-05 16:25:38 +0000214 for mbed_module, module_path in version.modules.items():
Darryl Green7c2dd582018-03-01 14:53:49 +0000215 output_path = os.path.join(
Darryl Greenfe9a6752019-04-04 14:39:33 +0100216 self.report_dir, "{}-{}-{}.dump".format(
217 mbed_module, version.revision, version.version
Darryl Green3e7a9802019-02-27 16:53:40 +0000218 )
Darryl Green7c2dd582018-03-01 14:53:49 +0000219 )
220 abi_dump_command = [
221 "abi-dumper",
Darryl Green9f357d62019-02-25 11:35:05 +0000222 module_path,
Darryl Green7c2dd582018-03-01 14:53:49 +0000223 "-o", output_path,
Gilles Peskine3e2da4a2019-07-04 19:01:22 +0200224 "-lver", self._pretty_revision(version),
Darryl Green7c2dd582018-03-01 14:53:49 +0000225 ]
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100226 abi_dump_output = subprocess.check_output(
Darryl Green7c2dd582018-03-01 14:53:49 +0000227 abi_dump_command,
Darryl Green7c2dd582018-03-01 14:53:49 +0000228 stderr=subprocess.STDOUT
229 )
Darryl Green3c3da792019-03-08 11:30:04 +0000230 self.log.debug(abi_dump_output.decode("utf-8"))
Darryl Green7c1a7332019-03-05 16:25:38 +0000231 version.abi_dumps[mbed_module] = output_path
Darryl Green7c2dd582018-03-01 14:53:49 +0000232
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200233 @staticmethod
234 def _normalize_storage_test_case_data(line):
235 """Eliminate cosmetic or irrelevant details in storage format test cases."""
236 line = re.sub(r'\s+', r'', line)
237 return line
238
Gilles Peskine2eae8d72022-02-22 19:02:44 +0100239 def _read_storage_tests(self,
240 directory,
241 filename,
242 is_generated,
243 storage_tests):
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200244 """Record storage tests from the given file.
245
246 Populate the storage_tests dictionary with test cases read from
247 filename under directory.
248 """
249 at_paragraph_start = True
250 description = None
251 full_path = os.path.join(directory, filename)
Gilles Peskineaeb8d662022-03-04 20:02:00 +0100252 with open(full_path) as fd:
253 for line_number, line in enumerate(fd, 1):
254 line = line.strip()
255 if not line:
256 at_paragraph_start = True
Gilles Peskine2eae8d72022-02-22 19:02:44 +0100257 continue
Gilles Peskineaeb8d662022-03-04 20:02:00 +0100258 if line.startswith('#'):
259 continue
260 if at_paragraph_start:
261 description = line.strip()
262 at_paragraph_start = False
263 continue
264 if line.startswith('depends_on:'):
265 continue
266 # We've reached a test case data line
267 test_case_data = self._normalize_storage_test_case_data(line)
268 if not is_generated:
269 # In manual test data, only look at read tests.
270 function_name = test_case_data.split(':', 1)[0]
271 if 'read' not in function_name.split('_'):
272 continue
273 metadata = SimpleNamespace(
274 filename=filename,
275 line_number=line_number,
276 description=description
277 )
278 storage_tests[test_case_data] = metadata
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200279
Gilles Peskine2eae8d72022-02-22 19:02:44 +0100280 @staticmethod
281 def _list_generated_test_data_files(git_worktree_path):
282 """List the generated test data files."""
283 output = subprocess.check_output(
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200284 ['tests/scripts/generate_psa_tests.py', '--list'],
285 cwd=git_worktree_path,
286 ).decode('ascii')
Gilles Peskine2eae8d72022-02-22 19:02:44 +0100287 return [line for line in output.split('\n') if line]
288
289 def _get_storage_format_tests(self, version, git_worktree_path):
290 """Record the storage format tests for the specified git version.
291
292 The storage format tests are the test suite data files whose name
293 contains "storage_format".
294
295 The version must be checked out at git_worktree_path.
296
297 This function creates or updates the generated data files.
298 """
299 # Existing test data files. This may be missing some automatically
300 # generated files if they haven't been generated yet.
301 storage_data_files = set(glob.glob(
302 'tests/suites/test_suite_*storage_format*.data'
303 ))
304 # Discover and (re)generate automatically generated data files.
305 to_be_generated = set()
306 for filename in self._list_generated_test_data_files(git_worktree_path):
307 if 'storage_format' in filename:
308 storage_data_files.add(filename)
309 to_be_generated.add(filename)
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200310 subprocess.check_call(
Gilles Peskine2eae8d72022-02-22 19:02:44 +0100311 ['tests/scripts/generate_psa_tests.py'] + sorted(to_be_generated),
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200312 cwd=git_worktree_path,
313 )
Gilles Peskine2eae8d72022-02-22 19:02:44 +0100314 for test_file in sorted(storage_data_files):
315 self._read_storage_tests(git_worktree_path,
316 test_file,
317 test_file in to_be_generated,
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200318 version.storage_tests)
319
Darryl Green3a5f6c82019-03-05 16:30:39 +0000320 def _cleanup_worktree(self, git_worktree_path):
Gilles Peskine712afa72019-02-25 20:36:52 +0100321 """Remove the specified git worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000322 shutil.rmtree(git_worktree_path)
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100323 worktree_output = subprocess.check_output(
Darryl Green7c2dd582018-03-01 14:53:49 +0000324 [self.git_command, "worktree", "prune"],
325 cwd=self.repo_path,
Darryl Green7c2dd582018-03-01 14:53:49 +0000326 stderr=subprocess.STDOUT
327 )
Darryl Green3c3da792019-03-08 11:30:04 +0000328 self.log.debug(worktree_output.decode("utf-8"))
Darryl Green7c2dd582018-03-01 14:53:49 +0000329
Darryl Green3a5f6c82019-03-05 16:30:39 +0000330 def _get_abi_dump_for_ref(self, version):
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200331 """Generate the interface information for the specified git revision."""
Darryl Green3a5f6c82019-03-05 16:30:39 +0000332 git_worktree_path = self._get_clean_worktree_for_git_revision(version)
333 self._update_git_submodules(git_worktree_path, version)
Gilles Peskine793778f2021-04-23 16:32:32 +0200334 if self.check_abi:
335 self._build_shared_libraries(git_worktree_path, version)
336 self._get_abi_dumps_from_shared_libraries(version)
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200337 if self.check_storage_tests:
338 self._get_storage_format_tests(version, git_worktree_path)
Darryl Green3a5f6c82019-03-05 16:30:39 +0000339 self._cleanup_worktree(git_worktree_path)
Darryl Green7c2dd582018-03-01 14:53:49 +0000340
Darryl Green3a5f6c82019-03-05 16:30:39 +0000341 def _remove_children_with_tag(self, parent, tag):
Darryl Greene62f9bb2019-02-21 13:09:26 +0000342 children = parent.getchildren()
343 for child in children:
344 if child.tag == tag:
345 parent.remove(child)
346 else:
Darryl Green3a5f6c82019-03-05 16:30:39 +0000347 self._remove_children_with_tag(child, tag)
Darryl Greene62f9bb2019-02-21 13:09:26 +0000348
Darryl Green3a5f6c82019-03-05 16:30:39 +0000349 def _remove_extra_detail_from_report(self, report_root):
Darryl Greene62f9bb2019-02-21 13:09:26 +0000350 for tag in ['test_info', 'test_results', 'problem_summary',
Darryl Greenc6f874b2019-06-05 12:57:50 +0100351 'added_symbols', 'affected']:
Darryl Green3a5f6c82019-03-05 16:30:39 +0000352 self._remove_children_with_tag(report_root, tag)
Darryl Greene62f9bb2019-02-21 13:09:26 +0000353
354 for report in report_root:
355 for problems in report.getchildren()[:]:
356 if not problems.getchildren():
357 report.remove(problems)
358
Gilles Peskineada828f2019-07-04 19:17:40 +0200359 def _abi_compliance_command(self, mbed_module, output_path):
360 """Build the command to run to analyze the library mbed_module.
361 The report will be placed in output_path."""
362 abi_compliance_command = [
363 "abi-compliance-checker",
364 "-l", mbed_module,
365 "-old", self.old_version.abi_dumps[mbed_module],
366 "-new", self.new_version.abi_dumps[mbed_module],
367 "-strict",
368 "-report-path", output_path,
369 ]
370 if self.skip_file:
371 abi_compliance_command += ["-skip-symbols", self.skip_file,
372 "-skip-types", self.skip_file]
373 if self.brief:
374 abi_compliance_command += ["-report-format", "xml",
375 "-stdout"]
376 return abi_compliance_command
377
378 def _is_library_compatible(self, mbed_module, compatibility_report):
379 """Test if the library mbed_module has remained compatible.
380 Append a message regarding compatibility to compatibility_report."""
381 output_path = os.path.join(
382 self.report_dir, "{}-{}-{}.html".format(
383 mbed_module, self.old_version.revision,
384 self.new_version.revision
385 )
386 )
387 try:
388 subprocess.check_output(
389 self._abi_compliance_command(mbed_module, output_path),
390 stderr=subprocess.STDOUT
391 )
392 except subprocess.CalledProcessError as err:
393 if err.returncode != 1:
394 raise err
395 if self.brief:
396 self.log.info(
397 "Compatibility issues found for {}".format(mbed_module)
398 )
399 report_root = ET.fromstring(err.output.decode("utf-8"))
400 self._remove_extra_detail_from_report(report_root)
401 self.log.info(ET.tostring(report_root).decode("utf-8"))
402 else:
403 self.can_remove_report_dir = False
404 compatibility_report.append(
405 "Compatibility issues found for {}, "
406 "for details see {}".format(mbed_module, output_path)
407 )
408 return False
409 compatibility_report.append(
410 "No compatibility issues for {}".format(mbed_module)
411 )
412 if not (self.keep_all_reports or self.brief):
413 os.remove(output_path)
414 return True
415
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200416 @staticmethod
417 def _is_storage_format_compatible(old_tests, new_tests,
418 compatibility_report):
419 """Check whether all tests present in old_tests are also in new_tests.
420
421 Append a message regarding compatibility to compatibility_report.
422 """
423 missing = frozenset(old_tests.keys()).difference(new_tests.keys())
424 for test_data in sorted(missing):
425 metadata = old_tests[test_data]
426 compatibility_report.append(
427 'Test case from {} line {} "{}" has disappeared: {}'.format(
428 metadata.filename, metadata.line_number,
429 metadata.description, test_data
430 )
431 )
432 compatibility_report.append(
433 'FAIL: {}/{} storage format test cases have changed or disappeared.'.format(
434 len(missing), len(old_tests)
435 ) if missing else
436 'PASS: All {} storage format test cases are preserved.'.format(
437 len(old_tests)
438 )
439 )
440 compatibility_report.append(
441 'Info: number of storage format tests cases: {} -> {}.'.format(
442 len(old_tests), len(new_tests)
443 )
444 )
445 return not missing
446
Darryl Green7c2dd582018-03-01 14:53:49 +0000447 def get_abi_compatibility_report(self):
Gilles Peskine712afa72019-02-25 20:36:52 +0100448 """Generate a report of the differences between the reference ABI
Darryl Green8184df52019-04-05 17:06:17 +0100449 and the new ABI. ABI dumps from self.old_version and self.new_version
450 must be available."""
Gilles Peskineada828f2019-07-04 19:17:40 +0200451 compatibility_report = ["Checking evolution from {} to {}".format(
Gilles Peskine3e2da4a2019-07-04 19:01:22 +0200452 self._pretty_revision(self.old_version),
453 self._pretty_revision(self.new_version)
Gilles Peskineada828f2019-07-04 19:17:40 +0200454 )]
Darryl Green7c2dd582018-03-01 14:53:49 +0000455 compliance_return_code = 0
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200456
Gilles Peskine793778f2021-04-23 16:32:32 +0200457 if self.check_abi:
458 shared_modules = list(set(self.old_version.modules.keys()) &
459 set(self.new_version.modules.keys()))
460 for mbed_module in shared_modules:
461 if not self._is_library_compatible(mbed_module,
462 compatibility_report):
463 compliance_return_code = 1
464
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200465 if self.check_storage_tests:
466 if not self._is_storage_format_compatible(
467 self.old_version.storage_tests,
468 self.new_version.storage_tests,
469 compatibility_report):
Gilles Peskineada828f2019-07-04 19:17:40 +0200470 compliance_return_code = 1
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200471
Darryl Greenf2688e22019-05-29 11:29:08 +0100472 for version in [self.old_version, self.new_version]:
473 for mbed_module, mbed_module_dump in version.abi_dumps.items():
474 os.remove(mbed_module_dump)
Darryl Green3d3d5522019-02-25 17:01:55 +0000475 if self.can_remove_report_dir:
Darryl Green7c2dd582018-03-01 14:53:49 +0000476 os.rmdir(self.report_dir)
Gilles Peskineada828f2019-07-04 19:17:40 +0200477 self.log.info("\n".join(compatibility_report))
Darryl Green7c2dd582018-03-01 14:53:49 +0000478 return compliance_return_code
479
480 def check_for_abi_changes(self):
Gilles Peskine712afa72019-02-25 20:36:52 +0100481 """Generate a report of ABI differences
482 between self.old_rev and self.new_rev."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000483 self.check_repo_path()
Gilles Peskinef548a0c2022-03-03 10:22:36 +0100484 if self.check_api or self.check_abi:
485 self.check_abi_tools_are_installed()
Darryl Green3a5f6c82019-03-05 16:30:39 +0000486 self._get_abi_dump_for_ref(self.old_version)
487 self._get_abi_dump_for_ref(self.new_version)
Darryl Green7c2dd582018-03-01 14:53:49 +0000488 return self.get_abi_compatibility_report()
489
490
491def run_main():
492 try:
493 parser = argparse.ArgumentParser(
Gilles Peskine56354592022-03-03 10:23:09 +0100494 description=__doc__
Darryl Green7c2dd582018-03-01 14:53:49 +0000495 )
496 parser.add_argument(
Darryl Green3c3da792019-03-08 11:30:04 +0000497 "-v", "--verbose", action="store_true",
498 help="set verbosity level",
499 )
500 parser.add_argument(
Darryl Green418527b2018-04-16 12:02:29 +0100501 "-r", "--report-dir", type=str, default="reports",
Darryl Green7c2dd582018-03-01 14:53:49 +0000502 help="directory where reports are stored, default is reports",
503 )
504 parser.add_argument(
Darryl Green418527b2018-04-16 12:02:29 +0100505 "-k", "--keep-all-reports", action="store_true",
Darryl Green7c2dd582018-03-01 14:53:49 +0000506 help="keep all reports, even if there are no compatibility issues",
507 )
508 parser.add_argument(
Darryl Greenc5132ff2019-03-01 09:54:44 +0000509 "-o", "--old-rev", type=str, help="revision for old version.",
510 required=True,
Darryl Green7c2dd582018-03-01 14:53:49 +0000511 )
512 parser.add_argument(
Darryl Greenc5132ff2019-03-01 09:54:44 +0000513 "-or", "--old-repo", type=str, help="repository for old version."
Darryl Green9f357d62019-02-25 11:35:05 +0000514 )
515 parser.add_argument(
Darryl Greenc5132ff2019-03-01 09:54:44 +0000516 "-oc", "--old-crypto-rev", type=str,
517 help="revision for old crypto submodule."
Darryl Green7c2dd582018-03-01 14:53:49 +0000518 )
Darryl Greenc2883a22019-02-20 15:01:56 +0000519 parser.add_argument(
Darryl Greenc5132ff2019-03-01 09:54:44 +0000520 "-ocr", "--old-crypto-repo", type=str,
521 help="repository for old crypto submodule."
522 )
523 parser.add_argument(
524 "-n", "--new-rev", type=str, help="revision for new version",
525 required=True,
526 )
527 parser.add_argument(
528 "-nr", "--new-repo", type=str, help="repository for new version."
529 )
530 parser.add_argument(
531 "-nc", "--new-crypto-rev", type=str,
532 help="revision for new crypto version"
533 )
534 parser.add_argument(
535 "-ncr", "--new-crypto-repo", type=str,
536 help="repository for new crypto submodule."
Darryl Green9f357d62019-02-25 11:35:05 +0000537 )
538 parser.add_argument(
Darryl Greenc2883a22019-02-20 15:01:56 +0000539 "-s", "--skip-file", type=str,
Gilles Peskineb6ce2342019-07-04 19:00:31 +0200540 help=("path to file containing symbols and types to skip "
541 "(typically \"-s identifiers\" after running "
542 "\"tests/scripts/list-identifiers.sh --internal\")")
Darryl Greenc2883a22019-02-20 15:01:56 +0000543 )
Darryl Greene62f9bb2019-02-21 13:09:26 +0000544 parser.add_argument(
Gilles Peskine793778f2021-04-23 16:32:32 +0200545 "--check-abi",
546 action='store_true', default=True,
547 help="Perform ABI comparison (default: yes)"
548 )
549 parser.add_argument("--no-check-abi", action='store_false', dest='check_abi')
550 parser.add_argument(
551 "--check-api",
552 action='store_true', default=True,
553 help="Perform API comparison (default: yes)"
554 )
555 parser.add_argument("--no-check-api", action='store_false', dest='check_api')
556 parser.add_argument(
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200557 "--check-storage",
558 action='store_true', default=True,
559 help="Perform storage tests comparison (default: yes)"
560 )
561 parser.add_argument("--no-check-storage", action='store_false', dest='check_storage')
562 parser.add_argument(
Darryl Greene62f9bb2019-02-21 13:09:26 +0000563 "-b", "--brief", action="store_true",
564 help="output only the list of issues to stdout, instead of a full report",
565 )
Darryl Green7c2dd582018-03-01 14:53:49 +0000566 abi_args = parser.parse_args()
Darryl Green492bc402019-04-11 15:50:41 +0100567 if os.path.isfile(abi_args.report_dir):
568 print("Error: {} is not a directory".format(abi_args.report_dir))
569 parser.exit()
Darryl Green0d1ca512019-04-09 09:14:17 +0100570 old_version = SimpleNamespace(
571 version="old",
572 repository=abi_args.old_repo,
573 revision=abi_args.old_rev,
Gilles Peskine3e2da4a2019-07-04 19:01:22 +0200574 commit=None,
Darryl Green0d1ca512019-04-09 09:14:17 +0100575 crypto_repository=abi_args.old_crypto_repo,
576 crypto_revision=abi_args.old_crypto_rev,
577 abi_dumps={},
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200578 storage_tests={},
Darryl Green0d1ca512019-04-09 09:14:17 +0100579 modules={}
Darryl Green8184df52019-04-05 17:06:17 +0100580 )
Darryl Green0d1ca512019-04-09 09:14:17 +0100581 new_version = SimpleNamespace(
582 version="new",
583 repository=abi_args.new_repo,
584 revision=abi_args.new_rev,
Gilles Peskine3e2da4a2019-07-04 19:01:22 +0200585 commit=None,
Darryl Green0d1ca512019-04-09 09:14:17 +0100586 crypto_repository=abi_args.new_crypto_repo,
587 crypto_revision=abi_args.new_crypto_rev,
588 abi_dumps={},
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200589 storage_tests={},
Darryl Green0d1ca512019-04-09 09:14:17 +0100590 modules={}
Darryl Green8184df52019-04-05 17:06:17 +0100591 )
Darryl Green0d1ca512019-04-09 09:14:17 +0100592 configuration = SimpleNamespace(
593 verbose=abi_args.verbose,
594 report_dir=abi_args.report_dir,
595 keep_all_reports=abi_args.keep_all_reports,
596 brief=abi_args.brief,
Gilles Peskine793778f2021-04-23 16:32:32 +0200597 check_abi=abi_args.check_abi,
598 check_api=abi_args.check_api,
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200599 check_storage=abi_args.check_storage,
Darryl Green0d1ca512019-04-09 09:14:17 +0100600 skip_file=abi_args.skip_file
Darryl Green7c2dd582018-03-01 14:53:49 +0000601 )
Darryl Green0d1ca512019-04-09 09:14:17 +0100602 abi_check = AbiChecker(old_version, new_version, configuration)
Darryl Green7c2dd582018-03-01 14:53:49 +0000603 return_code = abi_check.check_for_abi_changes()
604 sys.exit(return_code)
Gilles Peskinee915d532019-02-25 21:39:42 +0100605 except Exception: # pylint: disable=broad-except
606 # Print the backtrace and exit explicitly so as to exit with
607 # status 2, not 1.
Darryl Greena6f430f2018-03-15 10:12:06 +0000608 traceback.print_exc()
Darryl Green7c2dd582018-03-01 14:53:49 +0000609 sys.exit(2)
610
611
612if __name__ == "__main__":
613 run_main()