blob: ac1d60ffd0081d357a8dc075770cc9c99bf6e9e2 [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
Gilles Peskinef4be01f2022-06-20 18:51:44 +02006### How the script works ###
7
Gilles Peskinecfd4fae2021-04-23 16:37:12 +02008For the source (API) and runtime (ABI) interface compatibility, this script
9is a small wrapper around the abi-compliance-checker and abi-dumper tools,
10applying them to compare the header and library files.
11
12For the storage format, this script compares the automatically generated
Gilles Peskine2eae8d72022-02-22 19:02:44 +010013storage tests and the manual read tests, and complains if there is a
Gilles Peskine1177f372022-03-04 19:59:55 +010014reduction in coverage. A change in test data will be signaled as a
Gilles Peskine2eae8d72022-02-22 19:02:44 +010015coverage reduction since the old test data is no longer present. A change in
Gilles Peskine1177f372022-03-04 19:59:55 +010016how test data is presented will be signaled as well; this would be a false
Gilles Peskine2eae8d72022-02-22 19:02:44 +010017positive.
Gilles Peskinecfd4fae2021-04-23 16:37:12 +020018
Gilles Peskine2eae8d72022-02-22 19:02:44 +010019The results of the API/ABI comparison are either formatted as HTML and stored
20at a configurable location, or are given as a brief list of problems.
21Returns 0 on success, 1 on non-compliance, and 2 if there is an error
Gilles Peskinecfd4fae2021-04-23 16:37:12 +020022while running the script.
Gilles Peskine56354592022-03-03 10:23:09 +010023
Gilles Peskinef4be01f2022-06-20 18:51:44 +020024### How to interpret non-compliance ###
25
26This script has relatively common false positives. In many scenarios, it only
27reports a pass if there is a strict textual match between the old version and
28the new version, and it reports problems where there is a sufficient semantic
29match but not a textual match. This section lists some common false positives.
30This is not an exhaustive list: in the end what matters is whether we are
31breaking a backward compatibility goal.
32
33**API**: the goal is that if an application works with the old version of the
34library, it can be recompiled against the new version and will still work.
35This is normally validated by comparing the declarations in `include/*/*.h`.
36A failure is a declaration that has disappeared or that now has a different
37type.
38
39 * It's ok to change or remove macros and functions that are documented as
40 for internal use only or as experimental.
41 * It's ok to rename function or macro parameters as long as the semantics
42 has not changed.
43 * It's ok to change or remove structure fields that are documented as
44 private.
45 * It's ok to add fields to a structure that already had private fields
46 or was documented as extensible.
47
48**ABI**: the goal is that if an application was built against the old version
49of the library, the same binary will work when linked against the new version.
50This is normally validated by comparing the symbols exported by `libmbed*.so`.
51A failure is a symbol that is no longer exported by the same library or that
52now has a different type.
53
54 * All ABI changes are acceptable if the library version is bumped
55 (see `scripts/bump_version.sh`).
56 * ABI changes that concern functions which are declared only inside the
57 library directory, and not in `include/*/*.h`, are acceptable only if
58 the function was only ever used inside the same library (libmbedcrypto,
59 libmbedx509, libmbedtls). As a counter example, if the old version
60 of libmbedtls calls mbedtls_foo() from libmbedcrypto, and the new version
61 of libmbedcrypto no longer has a compatible mbedtls_foo(), this does
62 require a version bump for libmbedcrypto.
63
64**Storage format**: the goal is to check that persistent keys stored by the
65old version can be read by the new version. This is normally validated by
66comparing the `*read*` test cases in `test_suite*storage_format*.data`.
67A failure is a storage read test case that is no longer present with the same
68function name and parameter list.
69
70 * It's ok if the same test data is present, but its presentation has changed,
71 for example if a test function is renamed or has different parameters.
72 * It's ok if redundant tests are removed.
73
74**Generated test coverage**: the goal is to check that automatically
75generated tests have as much coverage as before. This is normally validated
76by comparing the test cases that are automatically generated by a script.
77A failure is a generated test case that is no longer present with the same
78function name and parameter list.
79
80 * It's ok if the same test data is present, but its presentation has changed,
81 for example if a test function is renamed or has different parameters.
82 * It's ok if redundant tests are removed.
83
Darryl Green78696802018-04-06 11:23:22 +010084"""
Darryl Green7c2dd582018-03-01 14:53:49 +000085
Bence Szépkúti1e148272020-08-07 13:07:28 +020086# Copyright The Mbed TLS Contributors
Bence Szépkútic7da1fe2020-05-26 01:54:15 +020087# SPDX-License-Identifier: Apache-2.0
88#
89# Licensed under the Apache License, Version 2.0 (the "License"); you may
90# not use this file except in compliance with the License.
91# You may obtain a copy of the License at
92#
93# http://www.apache.org/licenses/LICENSE-2.0
94#
95# Unless required by applicable law or agreed to in writing, software
96# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
97# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
98# See the License for the specific language governing permissions and
99# limitations under the License.
Bence Szépkútic7da1fe2020-05-26 01:54:15 +0200100
Gilles Peskine2eae8d72022-02-22 19:02:44 +0100101import glob
Darryl Green7c2dd582018-03-01 14:53:49 +0000102import os
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200103import re
Darryl Green7c2dd582018-03-01 14:53:49 +0000104import sys
105import traceback
106import shutil
107import subprocess
108import argparse
109import logging
110import tempfile
Darryl Green9f357d62019-02-25 11:35:05 +0000111import fnmatch
Darryl Green0d1ca512019-04-09 09:14:17 +0100112from types import SimpleNamespace
Darryl Green7c2dd582018-03-01 14:53:49 +0000113
Darryl Greene62f9bb2019-02-21 13:09:26 +0000114import xml.etree.ElementTree as ET
115
Gilles Peskine7ff47662022-09-18 21:17:09 +0200116from mbedtls_dev import build_tree
117
Darryl Green7c2dd582018-03-01 14:53:49 +0000118
Gilles Peskine184c0962020-03-24 18:25:17 +0100119class AbiChecker:
Gilles Peskine712afa72019-02-25 20:36:52 +0100120 """API and ABI checker."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000121
Darryl Green0d1ca512019-04-09 09:14:17 +0100122 def __init__(self, old_version, new_version, configuration):
Gilles Peskine712afa72019-02-25 20:36:52 +0100123 """Instantiate the API/ABI checker.
124
Darryl Green7c1a7332019-03-05 16:25:38 +0000125 old_version: RepoVersion containing details to compare against
126 new_version: RepoVersion containing details to check
Darryl Greenf67e3492019-04-12 15:17:02 +0100127 configuration.report_dir: directory for output files
128 configuration.keep_all_reports: if false, delete old reports
129 configuration.brief: if true, output shorter report to stdout
Gilles Peskine1177f372022-03-04 19:59:55 +0100130 configuration.check_abi: if true, compare ABIs
Gilles Peskine793778f2021-04-23 16:32:32 +0200131 configuration.check_api: if true, compare APIs
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200132 configuration.check_storage: if true, compare storage format tests
Darryl Greenf67e3492019-04-12 15:17:02 +0100133 configuration.skip_file: path to file containing symbols and types to skip
Gilles Peskine712afa72019-02-25 20:36:52 +0100134 """
Darryl Green7c2dd582018-03-01 14:53:49 +0000135 self.repo_path = "."
136 self.log = None
Darryl Green0d1ca512019-04-09 09:14:17 +0100137 self.verbose = configuration.verbose
Darryl Green3a5f6c82019-03-05 16:30:39 +0000138 self._setup_logger()
Darryl Green0d1ca512019-04-09 09:14:17 +0100139 self.report_dir = os.path.abspath(configuration.report_dir)
140 self.keep_all_reports = configuration.keep_all_reports
Darryl Green492bc402019-04-11 15:50:41 +0100141 self.can_remove_report_dir = not (os.path.exists(self.report_dir) or
Darryl Green0d1ca512019-04-09 09:14:17 +0100142 self.keep_all_reports)
Darryl Green7c1a7332019-03-05 16:25:38 +0000143 self.old_version = old_version
144 self.new_version = new_version
Darryl Green0d1ca512019-04-09 09:14:17 +0100145 self.skip_file = configuration.skip_file
Gilles Peskine793778f2021-04-23 16:32:32 +0200146 self.check_abi = configuration.check_abi
147 self.check_api = configuration.check_api
148 if self.check_abi != self.check_api:
149 raise Exception('Checking API without ABI or vice versa is not supported')
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200150 self.check_storage_tests = configuration.check_storage
Darryl Green0d1ca512019-04-09 09:14:17 +0100151 self.brief = configuration.brief
Darryl Green7c2dd582018-03-01 14:53:49 +0000152 self.git_command = "git"
153 self.make_command = "make"
154
Darryl Green3a5f6c82019-03-05 16:30:39 +0000155 def _setup_logger(self):
Darryl Green7c2dd582018-03-01 14:53:49 +0000156 self.log = logging.getLogger()
Darryl Green3c3da792019-03-08 11:30:04 +0000157 if self.verbose:
158 self.log.setLevel(logging.DEBUG)
159 else:
160 self.log.setLevel(logging.INFO)
Darryl Green7c2dd582018-03-01 14:53:49 +0000161 self.log.addHandler(logging.StreamHandler())
162
Gilles Peskine712afa72019-02-25 20:36:52 +0100163 @staticmethod
164 def check_abi_tools_are_installed():
Darryl Green7c2dd582018-03-01 14:53:49 +0000165 for command in ["abi-dumper", "abi-compliance-checker"]:
166 if not shutil.which(command):
167 raise Exception("{} not installed, aborting".format(command))
168
Darryl Green3a5f6c82019-03-05 16:30:39 +0000169 def _get_clean_worktree_for_git_revision(self, version):
Darryl Green7c1a7332019-03-05 16:25:38 +0000170 """Make a separate worktree with version.revision checked out.
Gilles Peskine712afa72019-02-25 20:36:52 +0100171 Do not modify the current worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000172 git_worktree_path = tempfile.mkdtemp()
Darryl Green7c1a7332019-03-05 16:25:38 +0000173 if version.repository:
Darryl Green3c3da792019-03-08 11:30:04 +0000174 self.log.debug(
Darryl Greenda84e322019-02-19 16:59:33 +0000175 "Checking out git worktree for revision {} from {}".format(
Darryl Green7c1a7332019-03-05 16:25:38 +0000176 version.revision, version.repository
Darryl Greenda84e322019-02-19 16:59:33 +0000177 )
178 )
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100179 fetch_output = subprocess.check_output(
Darryl Green7c1a7332019-03-05 16:25:38 +0000180 [self.git_command, "fetch",
181 version.repository, version.revision],
Darryl Greenda84e322019-02-19 16:59:33 +0000182 cwd=self.repo_path,
Darryl Greenda84e322019-02-19 16:59:33 +0000183 stderr=subprocess.STDOUT
184 )
Darryl Green3c3da792019-03-08 11:30:04 +0000185 self.log.debug(fetch_output.decode("utf-8"))
Darryl Greenda84e322019-02-19 16:59:33 +0000186 worktree_rev = "FETCH_HEAD"
187 else:
Darryl Green3c3da792019-03-08 11:30:04 +0000188 self.log.debug("Checking out git worktree for revision {}".format(
Darryl Green7c1a7332019-03-05 16:25:38 +0000189 version.revision
190 ))
191 worktree_rev = version.revision
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100192 worktree_output = subprocess.check_output(
Darryl Greenda84e322019-02-19 16:59:33 +0000193 [self.git_command, "worktree", "add", "--detach",
194 git_worktree_path, worktree_rev],
Darryl Green7c2dd582018-03-01 14:53:49 +0000195 cwd=self.repo_path,
Darryl Green7c2dd582018-03-01 14:53:49 +0000196 stderr=subprocess.STDOUT
197 )
Darryl Green3c3da792019-03-08 11:30:04 +0000198 self.log.debug(worktree_output.decode("utf-8"))
Gilles Peskine3e2da4a2019-07-04 19:01:22 +0200199 version.commit = subprocess.check_output(
Darryl Green762351b2019-07-25 14:33:33 +0100200 [self.git_command, "rev-parse", "HEAD"],
Gilles Peskine3e2da4a2019-07-04 19:01:22 +0200201 cwd=git_worktree_path,
202 stderr=subprocess.STDOUT
203 ).decode("ascii").rstrip()
204 self.log.debug("Commit is {}".format(version.commit))
Darryl Green7c2dd582018-03-01 14:53:49 +0000205 return git_worktree_path
206
Darryl Green3a5f6c82019-03-05 16:30:39 +0000207 def _update_git_submodules(self, git_worktree_path, version):
Darryl Green8184df52019-04-05 17:06:17 +0100208 """If the crypto submodule is present, initialize it.
209 if version.crypto_revision exists, update it to that revision,
210 otherwise update it to the default revision"""
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100211 update_output = subprocess.check_output(
Jaeden Ameroffeb1b82018-11-02 16:35:09 +0000212 [self.git_command, "submodule", "update", "--init", '--recursive'],
213 cwd=git_worktree_path,
Jaeden Ameroffeb1b82018-11-02 16:35:09 +0000214 stderr=subprocess.STDOUT
215 )
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100216 self.log.debug(update_output.decode("utf-8"))
Darryl Greene29ce702019-03-05 15:23:25 +0000217 if not (os.path.exists(os.path.join(git_worktree_path, "crypto"))
Darryl Green7c1a7332019-03-05 16:25:38 +0000218 and version.crypto_revision):
Darryl Greene29ce702019-03-05 15:23:25 +0000219 return
220
Darryl Green7c1a7332019-03-05 16:25:38 +0000221 if version.crypto_repository:
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100222 fetch_output = subprocess.check_output(
Darryl Green1d95c532019-03-08 11:12:19 +0000223 [self.git_command, "fetch", version.crypto_repository,
224 version.crypto_revision],
Darryl Greene29ce702019-03-05 15:23:25 +0000225 cwd=os.path.join(git_worktree_path, "crypto"),
Darryl Greene29ce702019-03-05 15:23:25 +0000226 stderr=subprocess.STDOUT
227 )
Darryl Green3c3da792019-03-08 11:30:04 +0000228 self.log.debug(fetch_output.decode("utf-8"))
Darryl Green1d95c532019-03-08 11:12:19 +0000229 crypto_rev = "FETCH_HEAD"
230 else:
231 crypto_rev = version.crypto_revision
232
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100233 checkout_output = subprocess.check_output(
Darryl Green1d95c532019-03-08 11:12:19 +0000234 [self.git_command, "checkout", crypto_rev],
235 cwd=os.path.join(git_worktree_path, "crypto"),
Darryl Green1d95c532019-03-08 11:12:19 +0000236 stderr=subprocess.STDOUT
237 )
Darryl Green3c3da792019-03-08 11:30:04 +0000238 self.log.debug(checkout_output.decode("utf-8"))
Jaeden Ameroffeb1b82018-11-02 16:35:09 +0000239
Darryl Green3a5f6c82019-03-05 16:30:39 +0000240 def _build_shared_libraries(self, git_worktree_path, version):
Gilles Peskine712afa72019-02-25 20:36:52 +0100241 """Build the shared libraries in the specified worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000242 my_environment = os.environ.copy()
243 my_environment["CFLAGS"] = "-g -Og"
244 my_environment["SHARED"] = "1"
Darryl Greend2dba362019-05-09 13:03:05 +0100245 if os.path.exists(os.path.join(git_worktree_path, "crypto")):
246 my_environment["USE_CRYPTO_SUBMODULE"] = "1"
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100247 make_output = subprocess.check_output(
Darryl Greenddf25a62019-02-28 11:52:39 +0000248 [self.make_command, "lib"],
Darryl Green7c2dd582018-03-01 14:53:49 +0000249 env=my_environment,
250 cwd=git_worktree_path,
Darryl Green7c2dd582018-03-01 14:53:49 +0000251 stderr=subprocess.STDOUT
252 )
Darryl Green3c3da792019-03-08 11:30:04 +0000253 self.log.debug(make_output.decode("utf-8"))
Darryl Greenf025d532019-04-12 15:18:02 +0100254 for root, _dirs, files in os.walk(git_worktree_path):
Darryl Green9f357d62019-02-25 11:35:05 +0000255 for file in fnmatch.filter(files, "*.so"):
Darryl Green7c1a7332019-03-05 16:25:38 +0000256 version.modules[os.path.splitext(file)[0]] = (
Darryl Green3e7a9802019-02-27 16:53:40 +0000257 os.path.join(root, file)
Darryl Green9f357d62019-02-25 11:35:05 +0000258 )
Darryl Green7c2dd582018-03-01 14:53:49 +0000259
Gilles Peskine3e2da4a2019-07-04 19:01:22 +0200260 @staticmethod
261 def _pretty_revision(version):
262 if version.revision == version.commit:
263 return version.revision
264 else:
265 return "{} ({})".format(version.revision, version.commit)
266
Darryl Green8184df52019-04-05 17:06:17 +0100267 def _get_abi_dumps_from_shared_libraries(self, version):
Gilles Peskine712afa72019-02-25 20:36:52 +0100268 """Generate the ABI dumps for the specified git revision.
Darryl Green8184df52019-04-05 17:06:17 +0100269 The shared libraries must have been built and the module paths
270 present in version.modules."""
Darryl Green7c1a7332019-03-05 16:25:38 +0000271 for mbed_module, module_path in version.modules.items():
Darryl Green7c2dd582018-03-01 14:53:49 +0000272 output_path = os.path.join(
Darryl Greenfe9a6752019-04-04 14:39:33 +0100273 self.report_dir, "{}-{}-{}.dump".format(
274 mbed_module, version.revision, version.version
Darryl Green3e7a9802019-02-27 16:53:40 +0000275 )
Darryl Green7c2dd582018-03-01 14:53:49 +0000276 )
277 abi_dump_command = [
278 "abi-dumper",
Darryl Green9f357d62019-02-25 11:35:05 +0000279 module_path,
Darryl Green7c2dd582018-03-01 14:53:49 +0000280 "-o", output_path,
Gilles Peskine3e2da4a2019-07-04 19:01:22 +0200281 "-lver", self._pretty_revision(version),
Darryl Green7c2dd582018-03-01 14:53:49 +0000282 ]
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100283 abi_dump_output = subprocess.check_output(
Darryl Green7c2dd582018-03-01 14:53:49 +0000284 abi_dump_command,
Darryl Green7c2dd582018-03-01 14:53:49 +0000285 stderr=subprocess.STDOUT
286 )
Darryl Green3c3da792019-03-08 11:30:04 +0000287 self.log.debug(abi_dump_output.decode("utf-8"))
Darryl Green7c1a7332019-03-05 16:25:38 +0000288 version.abi_dumps[mbed_module] = output_path
Darryl Green7c2dd582018-03-01 14:53:49 +0000289
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200290 @staticmethod
291 def _normalize_storage_test_case_data(line):
292 """Eliminate cosmetic or irrelevant details in storage format test cases."""
293 line = re.sub(r'\s+', r'', line)
294 return line
295
Gilles Peskine2eae8d72022-02-22 19:02:44 +0100296 def _read_storage_tests(self,
297 directory,
298 filename,
299 is_generated,
300 storage_tests):
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200301 """Record storage tests from the given file.
302
303 Populate the storage_tests dictionary with test cases read from
304 filename under directory.
305 """
306 at_paragraph_start = True
307 description = None
308 full_path = os.path.join(directory, filename)
Gilles Peskineaeb8d662022-03-04 20:02:00 +0100309 with open(full_path) as fd:
310 for line_number, line in enumerate(fd, 1):
311 line = line.strip()
312 if not line:
313 at_paragraph_start = True
Gilles Peskine2eae8d72022-02-22 19:02:44 +0100314 continue
Gilles Peskineaeb8d662022-03-04 20:02:00 +0100315 if line.startswith('#'):
316 continue
317 if at_paragraph_start:
318 description = line.strip()
319 at_paragraph_start = False
320 continue
321 if line.startswith('depends_on:'):
322 continue
323 # We've reached a test case data line
324 test_case_data = self._normalize_storage_test_case_data(line)
325 if not is_generated:
326 # In manual test data, only look at read tests.
327 function_name = test_case_data.split(':', 1)[0]
328 if 'read' not in function_name.split('_'):
329 continue
330 metadata = SimpleNamespace(
331 filename=filename,
332 line_number=line_number,
333 description=description
334 )
335 storage_tests[test_case_data] = metadata
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200336
Gilles Peskine2eae8d72022-02-22 19:02:44 +0100337 @staticmethod
338 def _list_generated_test_data_files(git_worktree_path):
339 """List the generated test data files."""
340 output = subprocess.check_output(
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200341 ['tests/scripts/generate_psa_tests.py', '--list'],
342 cwd=git_worktree_path,
343 ).decode('ascii')
Gilles Peskine2eae8d72022-02-22 19:02:44 +0100344 return [line for line in output.split('\n') if line]
345
346 def _get_storage_format_tests(self, version, git_worktree_path):
347 """Record the storage format tests for the specified git version.
348
349 The storage format tests are the test suite data files whose name
350 contains "storage_format".
351
352 The version must be checked out at git_worktree_path.
353
354 This function creates or updates the generated data files.
355 """
356 # Existing test data files. This may be missing some automatically
357 # generated files if they haven't been generated yet.
358 storage_data_files = set(glob.glob(
359 'tests/suites/test_suite_*storage_format*.data'
360 ))
361 # Discover and (re)generate automatically generated data files.
362 to_be_generated = set()
363 for filename in self._list_generated_test_data_files(git_worktree_path):
364 if 'storage_format' in filename:
365 storage_data_files.add(filename)
366 to_be_generated.add(filename)
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200367 subprocess.check_call(
Gilles Peskine2eae8d72022-02-22 19:02:44 +0100368 ['tests/scripts/generate_psa_tests.py'] + sorted(to_be_generated),
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200369 cwd=git_worktree_path,
370 )
Gilles Peskine2eae8d72022-02-22 19:02:44 +0100371 for test_file in sorted(storage_data_files):
372 self._read_storage_tests(git_worktree_path,
373 test_file,
374 test_file in to_be_generated,
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200375 version.storage_tests)
376
Darryl Green3a5f6c82019-03-05 16:30:39 +0000377 def _cleanup_worktree(self, git_worktree_path):
Gilles Peskine712afa72019-02-25 20:36:52 +0100378 """Remove the specified git worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000379 shutil.rmtree(git_worktree_path)
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100380 worktree_output = subprocess.check_output(
Darryl Green7c2dd582018-03-01 14:53:49 +0000381 [self.git_command, "worktree", "prune"],
382 cwd=self.repo_path,
Darryl Green7c2dd582018-03-01 14:53:49 +0000383 stderr=subprocess.STDOUT
384 )
Darryl Green3c3da792019-03-08 11:30:04 +0000385 self.log.debug(worktree_output.decode("utf-8"))
Darryl Green7c2dd582018-03-01 14:53:49 +0000386
Darryl Green3a5f6c82019-03-05 16:30:39 +0000387 def _get_abi_dump_for_ref(self, version):
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200388 """Generate the interface information for the specified git revision."""
Darryl Green3a5f6c82019-03-05 16:30:39 +0000389 git_worktree_path = self._get_clean_worktree_for_git_revision(version)
390 self._update_git_submodules(git_worktree_path, version)
Gilles Peskine793778f2021-04-23 16:32:32 +0200391 if self.check_abi:
392 self._build_shared_libraries(git_worktree_path, version)
393 self._get_abi_dumps_from_shared_libraries(version)
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200394 if self.check_storage_tests:
395 self._get_storage_format_tests(version, git_worktree_path)
Darryl Green3a5f6c82019-03-05 16:30:39 +0000396 self._cleanup_worktree(git_worktree_path)
Darryl Green7c2dd582018-03-01 14:53:49 +0000397
Darryl Green3a5f6c82019-03-05 16:30:39 +0000398 def _remove_children_with_tag(self, parent, tag):
Darryl Greene62f9bb2019-02-21 13:09:26 +0000399 children = parent.getchildren()
400 for child in children:
401 if child.tag == tag:
402 parent.remove(child)
403 else:
Darryl Green3a5f6c82019-03-05 16:30:39 +0000404 self._remove_children_with_tag(child, tag)
Darryl Greene62f9bb2019-02-21 13:09:26 +0000405
Darryl Green3a5f6c82019-03-05 16:30:39 +0000406 def _remove_extra_detail_from_report(self, report_root):
Darryl Greene62f9bb2019-02-21 13:09:26 +0000407 for tag in ['test_info', 'test_results', 'problem_summary',
Darryl Greenc6f874b2019-06-05 12:57:50 +0100408 'added_symbols', 'affected']:
Darryl Green3a5f6c82019-03-05 16:30:39 +0000409 self._remove_children_with_tag(report_root, tag)
Darryl Greene62f9bb2019-02-21 13:09:26 +0000410
411 for report in report_root:
412 for problems in report.getchildren()[:]:
413 if not problems.getchildren():
414 report.remove(problems)
415
Gilles Peskineada828f2019-07-04 19:17:40 +0200416 def _abi_compliance_command(self, mbed_module, output_path):
417 """Build the command to run to analyze the library mbed_module.
418 The report will be placed in output_path."""
419 abi_compliance_command = [
420 "abi-compliance-checker",
421 "-l", mbed_module,
422 "-old", self.old_version.abi_dumps[mbed_module],
423 "-new", self.new_version.abi_dumps[mbed_module],
424 "-strict",
425 "-report-path", output_path,
426 ]
427 if self.skip_file:
428 abi_compliance_command += ["-skip-symbols", self.skip_file,
429 "-skip-types", self.skip_file]
430 if self.brief:
431 abi_compliance_command += ["-report-format", "xml",
432 "-stdout"]
433 return abi_compliance_command
434
435 def _is_library_compatible(self, mbed_module, compatibility_report):
436 """Test if the library mbed_module has remained compatible.
437 Append a message regarding compatibility to compatibility_report."""
438 output_path = os.path.join(
439 self.report_dir, "{}-{}-{}.html".format(
440 mbed_module, self.old_version.revision,
441 self.new_version.revision
442 )
443 )
444 try:
445 subprocess.check_output(
446 self._abi_compliance_command(mbed_module, output_path),
447 stderr=subprocess.STDOUT
448 )
449 except subprocess.CalledProcessError as err:
450 if err.returncode != 1:
451 raise err
452 if self.brief:
453 self.log.info(
454 "Compatibility issues found for {}".format(mbed_module)
455 )
456 report_root = ET.fromstring(err.output.decode("utf-8"))
457 self._remove_extra_detail_from_report(report_root)
458 self.log.info(ET.tostring(report_root).decode("utf-8"))
459 else:
460 self.can_remove_report_dir = False
461 compatibility_report.append(
462 "Compatibility issues found for {}, "
463 "for details see {}".format(mbed_module, output_path)
464 )
465 return False
466 compatibility_report.append(
467 "No compatibility issues for {}".format(mbed_module)
468 )
469 if not (self.keep_all_reports or self.brief):
470 os.remove(output_path)
471 return True
472
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200473 @staticmethod
474 def _is_storage_format_compatible(old_tests, new_tests,
475 compatibility_report):
476 """Check whether all tests present in old_tests are also in new_tests.
477
478 Append a message regarding compatibility to compatibility_report.
479 """
480 missing = frozenset(old_tests.keys()).difference(new_tests.keys())
481 for test_data in sorted(missing):
482 metadata = old_tests[test_data]
483 compatibility_report.append(
484 'Test case from {} line {} "{}" has disappeared: {}'.format(
485 metadata.filename, metadata.line_number,
486 metadata.description, test_data
487 )
488 )
489 compatibility_report.append(
490 'FAIL: {}/{} storage format test cases have changed or disappeared.'.format(
491 len(missing), len(old_tests)
492 ) if missing else
493 'PASS: All {} storage format test cases are preserved.'.format(
494 len(old_tests)
495 )
496 )
497 compatibility_report.append(
498 'Info: number of storage format tests cases: {} -> {}.'.format(
499 len(old_tests), len(new_tests)
500 )
501 )
502 return not missing
503
Darryl Green7c2dd582018-03-01 14:53:49 +0000504 def get_abi_compatibility_report(self):
Gilles Peskine712afa72019-02-25 20:36:52 +0100505 """Generate a report of the differences between the reference ABI
Darryl Green8184df52019-04-05 17:06:17 +0100506 and the new ABI. ABI dumps from self.old_version and self.new_version
507 must be available."""
Gilles Peskineada828f2019-07-04 19:17:40 +0200508 compatibility_report = ["Checking evolution from {} to {}".format(
Gilles Peskine3e2da4a2019-07-04 19:01:22 +0200509 self._pretty_revision(self.old_version),
510 self._pretty_revision(self.new_version)
Gilles Peskineada828f2019-07-04 19:17:40 +0200511 )]
Darryl Green7c2dd582018-03-01 14:53:49 +0000512 compliance_return_code = 0
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200513
Gilles Peskine793778f2021-04-23 16:32:32 +0200514 if self.check_abi:
515 shared_modules = list(set(self.old_version.modules.keys()) &
516 set(self.new_version.modules.keys()))
517 for mbed_module in shared_modules:
518 if not self._is_library_compatible(mbed_module,
519 compatibility_report):
520 compliance_return_code = 1
521
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200522 if self.check_storage_tests:
523 if not self._is_storage_format_compatible(
524 self.old_version.storage_tests,
525 self.new_version.storage_tests,
526 compatibility_report):
Gilles Peskineada828f2019-07-04 19:17:40 +0200527 compliance_return_code = 1
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200528
Darryl Greenf2688e22019-05-29 11:29:08 +0100529 for version in [self.old_version, self.new_version]:
530 for mbed_module, mbed_module_dump in version.abi_dumps.items():
531 os.remove(mbed_module_dump)
Darryl Green3d3d5522019-02-25 17:01:55 +0000532 if self.can_remove_report_dir:
Darryl Green7c2dd582018-03-01 14:53:49 +0000533 os.rmdir(self.report_dir)
Gilles Peskineada828f2019-07-04 19:17:40 +0200534 self.log.info("\n".join(compatibility_report))
Darryl Green7c2dd582018-03-01 14:53:49 +0000535 return compliance_return_code
536
537 def check_for_abi_changes(self):
Gilles Peskine712afa72019-02-25 20:36:52 +0100538 """Generate a report of ABI differences
539 between self.old_rev and self.new_rev."""
Gilles Peskine7ff47662022-09-18 21:17:09 +0200540 build_tree.check_repo_path()
Gilles Peskinef548a0c2022-03-03 10:22:36 +0100541 if self.check_api or self.check_abi:
542 self.check_abi_tools_are_installed()
Darryl Green3a5f6c82019-03-05 16:30:39 +0000543 self._get_abi_dump_for_ref(self.old_version)
544 self._get_abi_dump_for_ref(self.new_version)
Darryl Green7c2dd582018-03-01 14:53:49 +0000545 return self.get_abi_compatibility_report()
546
547
548def run_main():
549 try:
550 parser = argparse.ArgumentParser(
Gilles Peskine56354592022-03-03 10:23:09 +0100551 description=__doc__
Darryl Green7c2dd582018-03-01 14:53:49 +0000552 )
553 parser.add_argument(
Darryl Green3c3da792019-03-08 11:30:04 +0000554 "-v", "--verbose", action="store_true",
555 help="set verbosity level",
556 )
557 parser.add_argument(
Darryl Green418527b2018-04-16 12:02:29 +0100558 "-r", "--report-dir", type=str, default="reports",
Darryl Green7c2dd582018-03-01 14:53:49 +0000559 help="directory where reports are stored, default is reports",
560 )
561 parser.add_argument(
Darryl Green418527b2018-04-16 12:02:29 +0100562 "-k", "--keep-all-reports", action="store_true",
Darryl Green7c2dd582018-03-01 14:53:49 +0000563 help="keep all reports, even if there are no compatibility issues",
564 )
565 parser.add_argument(
Darryl Greenc5132ff2019-03-01 09:54:44 +0000566 "-o", "--old-rev", type=str, help="revision for old version.",
567 required=True,
Darryl Green7c2dd582018-03-01 14:53:49 +0000568 )
569 parser.add_argument(
Darryl Greenc5132ff2019-03-01 09:54:44 +0000570 "-or", "--old-repo", type=str, help="repository for old version."
Darryl Green9f357d62019-02-25 11:35:05 +0000571 )
572 parser.add_argument(
Darryl Greenc5132ff2019-03-01 09:54:44 +0000573 "-oc", "--old-crypto-rev", type=str,
574 help="revision for old crypto submodule."
Darryl Green7c2dd582018-03-01 14:53:49 +0000575 )
Darryl Greenc2883a22019-02-20 15:01:56 +0000576 parser.add_argument(
Darryl Greenc5132ff2019-03-01 09:54:44 +0000577 "-ocr", "--old-crypto-repo", type=str,
578 help="repository for old crypto submodule."
579 )
580 parser.add_argument(
581 "-n", "--new-rev", type=str, help="revision for new version",
582 required=True,
583 )
584 parser.add_argument(
585 "-nr", "--new-repo", type=str, help="repository for new version."
586 )
587 parser.add_argument(
588 "-nc", "--new-crypto-rev", type=str,
589 help="revision for new crypto version"
590 )
591 parser.add_argument(
592 "-ncr", "--new-crypto-repo", type=str,
593 help="repository for new crypto submodule."
Darryl Green9f357d62019-02-25 11:35:05 +0000594 )
595 parser.add_argument(
Darryl Greenc2883a22019-02-20 15:01:56 +0000596 "-s", "--skip-file", type=str,
Gilles Peskineb6ce2342019-07-04 19:00:31 +0200597 help=("path to file containing symbols and types to skip "
598 "(typically \"-s identifiers\" after running "
599 "\"tests/scripts/list-identifiers.sh --internal\")")
Darryl Greenc2883a22019-02-20 15:01:56 +0000600 )
Darryl Greene62f9bb2019-02-21 13:09:26 +0000601 parser.add_argument(
Gilles Peskine793778f2021-04-23 16:32:32 +0200602 "--check-abi",
603 action='store_true', default=True,
604 help="Perform ABI comparison (default: yes)"
605 )
606 parser.add_argument("--no-check-abi", action='store_false', dest='check_abi')
607 parser.add_argument(
608 "--check-api",
609 action='store_true', default=True,
610 help="Perform API comparison (default: yes)"
611 )
612 parser.add_argument("--no-check-api", action='store_false', dest='check_api')
613 parser.add_argument(
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200614 "--check-storage",
615 action='store_true', default=True,
616 help="Perform storage tests comparison (default: yes)"
617 )
618 parser.add_argument("--no-check-storage", action='store_false', dest='check_storage')
619 parser.add_argument(
Darryl Greene62f9bb2019-02-21 13:09:26 +0000620 "-b", "--brief", action="store_true",
621 help="output only the list of issues to stdout, instead of a full report",
622 )
Darryl Green7c2dd582018-03-01 14:53:49 +0000623 abi_args = parser.parse_args()
Darryl Green492bc402019-04-11 15:50:41 +0100624 if os.path.isfile(abi_args.report_dir):
625 print("Error: {} is not a directory".format(abi_args.report_dir))
626 parser.exit()
Darryl Green0d1ca512019-04-09 09:14:17 +0100627 old_version = SimpleNamespace(
628 version="old",
629 repository=abi_args.old_repo,
630 revision=abi_args.old_rev,
Gilles Peskine3e2da4a2019-07-04 19:01:22 +0200631 commit=None,
Darryl Green0d1ca512019-04-09 09:14:17 +0100632 crypto_repository=abi_args.old_crypto_repo,
633 crypto_revision=abi_args.old_crypto_rev,
634 abi_dumps={},
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200635 storage_tests={},
Darryl Green0d1ca512019-04-09 09:14:17 +0100636 modules={}
Darryl Green8184df52019-04-05 17:06:17 +0100637 )
Darryl Green0d1ca512019-04-09 09:14:17 +0100638 new_version = SimpleNamespace(
639 version="new",
640 repository=abi_args.new_repo,
641 revision=abi_args.new_rev,
Gilles Peskine3e2da4a2019-07-04 19:01:22 +0200642 commit=None,
Darryl Green0d1ca512019-04-09 09:14:17 +0100643 crypto_repository=abi_args.new_crypto_repo,
644 crypto_revision=abi_args.new_crypto_rev,
645 abi_dumps={},
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200646 storage_tests={},
Darryl Green0d1ca512019-04-09 09:14:17 +0100647 modules={}
Darryl Green8184df52019-04-05 17:06:17 +0100648 )
Darryl Green0d1ca512019-04-09 09:14:17 +0100649 configuration = SimpleNamespace(
650 verbose=abi_args.verbose,
651 report_dir=abi_args.report_dir,
652 keep_all_reports=abi_args.keep_all_reports,
653 brief=abi_args.brief,
Gilles Peskine793778f2021-04-23 16:32:32 +0200654 check_abi=abi_args.check_abi,
655 check_api=abi_args.check_api,
Gilles Peskinecfd4fae2021-04-23 16:37:12 +0200656 check_storage=abi_args.check_storage,
Darryl Green0d1ca512019-04-09 09:14:17 +0100657 skip_file=abi_args.skip_file
Darryl Green7c2dd582018-03-01 14:53:49 +0000658 )
Darryl Green0d1ca512019-04-09 09:14:17 +0100659 abi_check = AbiChecker(old_version, new_version, configuration)
Darryl Green7c2dd582018-03-01 14:53:49 +0000660 return_code = abi_check.check_for_abi_changes()
661 sys.exit(return_code)
Gilles Peskinee915d532019-02-25 21:39:42 +0100662 except Exception: # pylint: disable=broad-except
663 # Print the backtrace and exit explicitly so as to exit with
664 # status 2, not 1.
Darryl Greena6f430f2018-03-15 10:12:06 +0000665 traceback.print_exc()
Darryl Green7c2dd582018-03-01 14:53:49 +0000666 sys.exit(2)
667
668
669if __name__ == "__main__":
670 run_main()