blob: 1ef6d663bf6d4d63ec07c610133ceff9eb2fbfc9 [file] [log] [blame]
Darryl Green7c2dd582018-03-01 14:53:49 +00001#!/usr/bin/env python3
Darryl Green78696802018-04-06 11:23:22 +01002"""
Gilles Peskine92165362021-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 Peskineca586a52022-02-22 19:02:44 +010012storage tests and the manual read tests, and complains if there is a
13reduction in coverage. A change in test data will be signalled as a
14coverage reduction since the old test data is no longer present. A change in
15how test data is presented will be signalled as well; this would be a false
16positive.
Gilles Peskine92165362021-04-23 16:37:12 +020017
Gilles Peskineca586a52022-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 Peskine92165362021-04-23 16:37:12 +020021while running the script.
Gilles Peskine644b3f62022-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 Peskineca586a52022-02-22 19:02:44 +010041import glob
Darryl Green7c2dd582018-03-01 14:53:49 +000042import os
Gilles Peskine92165362021-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 Peskinec76ab852021-04-23 16:32:32 +020068 configuration.check_api: if true, compare ABIs
69 configuration.check_api: if true, compare APIs
Gilles Peskine92165362021-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 Peskinec76ab852021-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 Peskine92165362021-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 Peskine92165362021-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 Peskineca586a52022-02-22 19:02:44 +0100239 def _read_storage_tests(self,
240 directory,
241 filename,
242 is_generated,
243 storage_tests):
Gilles Peskine92165362021-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)
252 for line_number, line in enumerate(open(full_path), 1):
253 line = line.strip()
254 if not line:
255 at_paragraph_start = True
256 continue
257 if line.startswith('#'):
258 continue
259 if at_paragraph_start:
260 description = line.strip()
261 at_paragraph_start = False
262 continue
263 if line.startswith('depends_on:'):
264 continue
265 # We've reached a test case data line
266 test_case_data = self._normalize_storage_test_case_data(line)
Gilles Peskineca586a52022-02-22 19:02:44 +0100267 if not is_generated:
268 # In manual test data, only look at read tests.
269 function_name = test_case_data.split(':', 1)[0]
270 if 'read' not in function_name.split('_'):
271 continue
Gilles Peskine92165362021-04-23 16:37:12 +0200272 metadata = SimpleNamespace(
273 filename=filename,
274 line_number=line_number,
275 description=description
276 )
277 storage_tests[test_case_data] = metadata
278
Gilles Peskineca586a52022-02-22 19:02:44 +0100279 @staticmethod
280 def _list_generated_test_data_files(git_worktree_path):
281 """List the generated test data files."""
282 output = subprocess.check_output(
Gilles Peskine92165362021-04-23 16:37:12 +0200283 ['tests/scripts/generate_psa_tests.py', '--list'],
284 cwd=git_worktree_path,
285 ).decode('ascii')
Gilles Peskineca586a52022-02-22 19:02:44 +0100286 return [line for line in output.split('\n') if line]
287
288 def _get_storage_format_tests(self, version, git_worktree_path):
289 """Record the storage format tests for the specified git version.
290
291 The storage format tests are the test suite data files whose name
292 contains "storage_format".
293
294 The version must be checked out at git_worktree_path.
295
296 This function creates or updates the generated data files.
297 """
298 # Existing test data files. This may be missing some automatically
299 # generated files if they haven't been generated yet.
300 storage_data_files = set(glob.glob(
301 'tests/suites/test_suite_*storage_format*.data'
302 ))
303 # Discover and (re)generate automatically generated data files.
304 to_be_generated = set()
305 for filename in self._list_generated_test_data_files(git_worktree_path):
306 if 'storage_format' in filename:
307 storage_data_files.add(filename)
308 to_be_generated.add(filename)
Gilles Peskine92165362021-04-23 16:37:12 +0200309 subprocess.check_call(
Gilles Peskineca586a52022-02-22 19:02:44 +0100310 ['tests/scripts/generate_psa_tests.py'] + sorted(to_be_generated),
Gilles Peskine92165362021-04-23 16:37:12 +0200311 cwd=git_worktree_path,
312 )
Gilles Peskineca586a52022-02-22 19:02:44 +0100313 for test_file in sorted(storage_data_files):
314 self._read_storage_tests(git_worktree_path,
315 test_file,
316 test_file in to_be_generated,
Gilles Peskine92165362021-04-23 16:37:12 +0200317 version.storage_tests)
318
Darryl Green3a5f6c82019-03-05 16:30:39 +0000319 def _cleanup_worktree(self, git_worktree_path):
Gilles Peskine712afa72019-02-25 20:36:52 +0100320 """Remove the specified git worktree."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000321 shutil.rmtree(git_worktree_path)
Darryl Greenb2ee0b82019-04-12 16:24:25 +0100322 worktree_output = subprocess.check_output(
Darryl Green7c2dd582018-03-01 14:53:49 +0000323 [self.git_command, "worktree", "prune"],
324 cwd=self.repo_path,
Darryl Green7c2dd582018-03-01 14:53:49 +0000325 stderr=subprocess.STDOUT
326 )
Darryl Green3c3da792019-03-08 11:30:04 +0000327 self.log.debug(worktree_output.decode("utf-8"))
Darryl Green7c2dd582018-03-01 14:53:49 +0000328
Darryl Green3a5f6c82019-03-05 16:30:39 +0000329 def _get_abi_dump_for_ref(self, version):
Gilles Peskine92165362021-04-23 16:37:12 +0200330 """Generate the interface information for the specified git revision."""
Darryl Green3a5f6c82019-03-05 16:30:39 +0000331 git_worktree_path = self._get_clean_worktree_for_git_revision(version)
332 self._update_git_submodules(git_worktree_path, version)
Gilles Peskinec76ab852021-04-23 16:32:32 +0200333 if self.check_abi:
334 self._build_shared_libraries(git_worktree_path, version)
335 self._get_abi_dumps_from_shared_libraries(version)
Gilles Peskine92165362021-04-23 16:37:12 +0200336 if self.check_storage_tests:
337 self._get_storage_format_tests(version, git_worktree_path)
Darryl Green3a5f6c82019-03-05 16:30:39 +0000338 self._cleanup_worktree(git_worktree_path)
Darryl Green7c2dd582018-03-01 14:53:49 +0000339
Darryl Green3a5f6c82019-03-05 16:30:39 +0000340 def _remove_children_with_tag(self, parent, tag):
Darryl Greene62f9bb2019-02-21 13:09:26 +0000341 children = parent.getchildren()
342 for child in children:
343 if child.tag == tag:
344 parent.remove(child)
345 else:
Darryl Green3a5f6c82019-03-05 16:30:39 +0000346 self._remove_children_with_tag(child, tag)
Darryl Greene62f9bb2019-02-21 13:09:26 +0000347
Darryl Green3a5f6c82019-03-05 16:30:39 +0000348 def _remove_extra_detail_from_report(self, report_root):
Darryl Greene62f9bb2019-02-21 13:09:26 +0000349 for tag in ['test_info', 'test_results', 'problem_summary',
Darryl Greenc6f874b2019-06-05 12:57:50 +0100350 'added_symbols', 'affected']:
Darryl Green3a5f6c82019-03-05 16:30:39 +0000351 self._remove_children_with_tag(report_root, tag)
Darryl Greene62f9bb2019-02-21 13:09:26 +0000352
353 for report in report_root:
354 for problems in report.getchildren()[:]:
355 if not problems.getchildren():
356 report.remove(problems)
357
Gilles Peskineada828f2019-07-04 19:17:40 +0200358 def _abi_compliance_command(self, mbed_module, output_path):
359 """Build the command to run to analyze the library mbed_module.
360 The report will be placed in output_path."""
361 abi_compliance_command = [
362 "abi-compliance-checker",
363 "-l", mbed_module,
364 "-old", self.old_version.abi_dumps[mbed_module],
365 "-new", self.new_version.abi_dumps[mbed_module],
366 "-strict",
367 "-report-path", output_path,
368 ]
369 if self.skip_file:
370 abi_compliance_command += ["-skip-symbols", self.skip_file,
371 "-skip-types", self.skip_file]
372 if self.brief:
373 abi_compliance_command += ["-report-format", "xml",
374 "-stdout"]
375 return abi_compliance_command
376
377 def _is_library_compatible(self, mbed_module, compatibility_report):
378 """Test if the library mbed_module has remained compatible.
379 Append a message regarding compatibility to compatibility_report."""
380 output_path = os.path.join(
381 self.report_dir, "{}-{}-{}.html".format(
382 mbed_module, self.old_version.revision,
383 self.new_version.revision
384 )
385 )
386 try:
387 subprocess.check_output(
388 self._abi_compliance_command(mbed_module, output_path),
389 stderr=subprocess.STDOUT
390 )
391 except subprocess.CalledProcessError as err:
392 if err.returncode != 1:
393 raise err
394 if self.brief:
395 self.log.info(
396 "Compatibility issues found for {}".format(mbed_module)
397 )
398 report_root = ET.fromstring(err.output.decode("utf-8"))
399 self._remove_extra_detail_from_report(report_root)
400 self.log.info(ET.tostring(report_root).decode("utf-8"))
401 else:
402 self.can_remove_report_dir = False
403 compatibility_report.append(
404 "Compatibility issues found for {}, "
405 "for details see {}".format(mbed_module, output_path)
406 )
407 return False
408 compatibility_report.append(
409 "No compatibility issues for {}".format(mbed_module)
410 )
411 if not (self.keep_all_reports or self.brief):
412 os.remove(output_path)
413 return True
414
Gilles Peskine92165362021-04-23 16:37:12 +0200415 @staticmethod
416 def _is_storage_format_compatible(old_tests, new_tests,
417 compatibility_report):
418 """Check whether all tests present in old_tests are also in new_tests.
419
420 Append a message regarding compatibility to compatibility_report.
421 """
422 missing = frozenset(old_tests.keys()).difference(new_tests.keys())
423 for test_data in sorted(missing):
424 metadata = old_tests[test_data]
425 compatibility_report.append(
426 'Test case from {} line {} "{}" has disappeared: {}'.format(
427 metadata.filename, metadata.line_number,
428 metadata.description, test_data
429 )
430 )
431 compatibility_report.append(
432 'FAIL: {}/{} storage format test cases have changed or disappeared.'.format(
433 len(missing), len(old_tests)
434 ) if missing else
435 'PASS: All {} storage format test cases are preserved.'.format(
436 len(old_tests)
437 )
438 )
439 compatibility_report.append(
440 'Info: number of storage format tests cases: {} -> {}.'.format(
441 len(old_tests), len(new_tests)
442 )
443 )
444 return not missing
445
Darryl Green7c2dd582018-03-01 14:53:49 +0000446 def get_abi_compatibility_report(self):
Gilles Peskine712afa72019-02-25 20:36:52 +0100447 """Generate a report of the differences between the reference ABI
Darryl Green8184df52019-04-05 17:06:17 +0100448 and the new ABI. ABI dumps from self.old_version and self.new_version
449 must be available."""
Gilles Peskineada828f2019-07-04 19:17:40 +0200450 compatibility_report = ["Checking evolution from {} to {}".format(
Gilles Peskine3e2da4a2019-07-04 19:01:22 +0200451 self._pretty_revision(self.old_version),
452 self._pretty_revision(self.new_version)
Gilles Peskineada828f2019-07-04 19:17:40 +0200453 )]
Darryl Green7c2dd582018-03-01 14:53:49 +0000454 compliance_return_code = 0
Gilles Peskine92165362021-04-23 16:37:12 +0200455
Gilles Peskinec76ab852021-04-23 16:32:32 +0200456 if self.check_abi:
457 shared_modules = list(set(self.old_version.modules.keys()) &
458 set(self.new_version.modules.keys()))
459 for mbed_module in shared_modules:
460 if not self._is_library_compatible(mbed_module,
461 compatibility_report):
462 compliance_return_code = 1
463
Gilles Peskine92165362021-04-23 16:37:12 +0200464 if self.check_storage_tests:
465 if not self._is_storage_format_compatible(
466 self.old_version.storage_tests,
467 self.new_version.storage_tests,
468 compatibility_report):
Gilles Peskineada828f2019-07-04 19:17:40 +0200469 compliance_return_code = 1
Gilles Peskine92165362021-04-23 16:37:12 +0200470
Darryl Greenf2688e22019-05-29 11:29:08 +0100471 for version in [self.old_version, self.new_version]:
472 for mbed_module, mbed_module_dump in version.abi_dumps.items():
473 os.remove(mbed_module_dump)
Darryl Green3d3d5522019-02-25 17:01:55 +0000474 if self.can_remove_report_dir:
Darryl Green7c2dd582018-03-01 14:53:49 +0000475 os.rmdir(self.report_dir)
Gilles Peskineada828f2019-07-04 19:17:40 +0200476 self.log.info("\n".join(compatibility_report))
Darryl Green7c2dd582018-03-01 14:53:49 +0000477 return compliance_return_code
478
479 def check_for_abi_changes(self):
Gilles Peskine712afa72019-02-25 20:36:52 +0100480 """Generate a report of ABI differences
481 between self.old_rev and self.new_rev."""
Darryl Green7c2dd582018-03-01 14:53:49 +0000482 self.check_repo_path()
Gilles Peskine93c2a422022-03-03 10:22:36 +0100483 if self.check_api or self.check_abi:
484 self.check_abi_tools_are_installed()
Darryl Green3a5f6c82019-03-05 16:30:39 +0000485 self._get_abi_dump_for_ref(self.old_version)
486 self._get_abi_dump_for_ref(self.new_version)
Darryl Green7c2dd582018-03-01 14:53:49 +0000487 return self.get_abi_compatibility_report()
488
489
490def run_main():
491 try:
492 parser = argparse.ArgumentParser(
Gilles Peskine644b3f62022-03-03 10:23:09 +0100493 description=__doc__
Darryl Green7c2dd582018-03-01 14:53:49 +0000494 )
495 parser.add_argument(
Darryl Green3c3da792019-03-08 11:30:04 +0000496 "-v", "--verbose", action="store_true",
497 help="set verbosity level",
498 )
499 parser.add_argument(
Darryl Green418527b2018-04-16 12:02:29 +0100500 "-r", "--report-dir", type=str, default="reports",
Darryl Green7c2dd582018-03-01 14:53:49 +0000501 help="directory where reports are stored, default is reports",
502 )
503 parser.add_argument(
Darryl Green418527b2018-04-16 12:02:29 +0100504 "-k", "--keep-all-reports", action="store_true",
Darryl Green7c2dd582018-03-01 14:53:49 +0000505 help="keep all reports, even if there are no compatibility issues",
506 )
507 parser.add_argument(
Darryl Greenc5132ff2019-03-01 09:54:44 +0000508 "-o", "--old-rev", type=str, help="revision for old version.",
509 required=True,
Darryl Green7c2dd582018-03-01 14:53:49 +0000510 )
511 parser.add_argument(
Darryl Greenc5132ff2019-03-01 09:54:44 +0000512 "-or", "--old-repo", type=str, help="repository for old version."
Darryl Green9f357d62019-02-25 11:35:05 +0000513 )
514 parser.add_argument(
Darryl Greenc5132ff2019-03-01 09:54:44 +0000515 "-oc", "--old-crypto-rev", type=str,
516 help="revision for old crypto submodule."
Darryl Green7c2dd582018-03-01 14:53:49 +0000517 )
Darryl Greenc2883a22019-02-20 15:01:56 +0000518 parser.add_argument(
Darryl Greenc5132ff2019-03-01 09:54:44 +0000519 "-ocr", "--old-crypto-repo", type=str,
520 help="repository for old crypto submodule."
521 )
522 parser.add_argument(
523 "-n", "--new-rev", type=str, help="revision for new version",
524 required=True,
525 )
526 parser.add_argument(
527 "-nr", "--new-repo", type=str, help="repository for new version."
528 )
529 parser.add_argument(
530 "-nc", "--new-crypto-rev", type=str,
531 help="revision for new crypto version"
532 )
533 parser.add_argument(
534 "-ncr", "--new-crypto-repo", type=str,
535 help="repository for new crypto submodule."
Darryl Green9f357d62019-02-25 11:35:05 +0000536 )
537 parser.add_argument(
Darryl Greenc2883a22019-02-20 15:01:56 +0000538 "-s", "--skip-file", type=str,
Gilles Peskineb6ce2342019-07-04 19:00:31 +0200539 help=("path to file containing symbols and types to skip "
540 "(typically \"-s identifiers\" after running "
541 "\"tests/scripts/list-identifiers.sh --internal\")")
Darryl Greenc2883a22019-02-20 15:01:56 +0000542 )
Darryl Greene62f9bb2019-02-21 13:09:26 +0000543 parser.add_argument(
Gilles Peskinec76ab852021-04-23 16:32:32 +0200544 "--check-abi",
545 action='store_true', default=True,
546 help="Perform ABI comparison (default: yes)"
547 )
548 parser.add_argument("--no-check-abi", action='store_false', dest='check_abi')
549 parser.add_argument(
550 "--check-api",
551 action='store_true', default=True,
552 help="Perform API comparison (default: yes)"
553 )
554 parser.add_argument("--no-check-api", action='store_false', dest='check_api')
555 parser.add_argument(
Gilles Peskine92165362021-04-23 16:37:12 +0200556 "--check-storage",
557 action='store_true', default=True,
558 help="Perform storage tests comparison (default: yes)"
559 )
560 parser.add_argument("--no-check-storage", action='store_false', dest='check_storage')
561 parser.add_argument(
Darryl Greene62f9bb2019-02-21 13:09:26 +0000562 "-b", "--brief", action="store_true",
563 help="output only the list of issues to stdout, instead of a full report",
564 )
Darryl Green7c2dd582018-03-01 14:53:49 +0000565 abi_args = parser.parse_args()
Darryl Green492bc402019-04-11 15:50:41 +0100566 if os.path.isfile(abi_args.report_dir):
567 print("Error: {} is not a directory".format(abi_args.report_dir))
568 parser.exit()
Darryl Green0d1ca512019-04-09 09:14:17 +0100569 old_version = SimpleNamespace(
570 version="old",
571 repository=abi_args.old_repo,
572 revision=abi_args.old_rev,
Gilles Peskine3e2da4a2019-07-04 19:01:22 +0200573 commit=None,
Darryl Green0d1ca512019-04-09 09:14:17 +0100574 crypto_repository=abi_args.old_crypto_repo,
575 crypto_revision=abi_args.old_crypto_rev,
576 abi_dumps={},
Gilles Peskine92165362021-04-23 16:37:12 +0200577 storage_tests={},
Darryl Green0d1ca512019-04-09 09:14:17 +0100578 modules={}
Darryl Green8184df52019-04-05 17:06:17 +0100579 )
Darryl Green0d1ca512019-04-09 09:14:17 +0100580 new_version = SimpleNamespace(
581 version="new",
582 repository=abi_args.new_repo,
583 revision=abi_args.new_rev,
Gilles Peskine3e2da4a2019-07-04 19:01:22 +0200584 commit=None,
Darryl Green0d1ca512019-04-09 09:14:17 +0100585 crypto_repository=abi_args.new_crypto_repo,
586 crypto_revision=abi_args.new_crypto_rev,
587 abi_dumps={},
Gilles Peskine92165362021-04-23 16:37:12 +0200588 storage_tests={},
Darryl Green0d1ca512019-04-09 09:14:17 +0100589 modules={}
Darryl Green8184df52019-04-05 17:06:17 +0100590 )
Darryl Green0d1ca512019-04-09 09:14:17 +0100591 configuration = SimpleNamespace(
592 verbose=abi_args.verbose,
593 report_dir=abi_args.report_dir,
594 keep_all_reports=abi_args.keep_all_reports,
595 brief=abi_args.brief,
Gilles Peskinec76ab852021-04-23 16:32:32 +0200596 check_abi=abi_args.check_abi,
597 check_api=abi_args.check_api,
Gilles Peskine92165362021-04-23 16:37:12 +0200598 check_storage=abi_args.check_storage,
Darryl Green0d1ca512019-04-09 09:14:17 +0100599 skip_file=abi_args.skip_file
Darryl Green7c2dd582018-03-01 14:53:49 +0000600 )
Darryl Green0d1ca512019-04-09 09:14:17 +0100601 abi_check = AbiChecker(old_version, new_version, configuration)
Darryl Green7c2dd582018-03-01 14:53:49 +0000602 return_code = abi_check.check_for_abi_changes()
603 sys.exit(return_code)
Gilles Peskinee915d532019-02-25 21:39:42 +0100604 except Exception: # pylint: disable=broad-except
605 # Print the backtrace and exit explicitly so as to exit with
606 # status 2, not 1.
Darryl Greena6f430f2018-03-15 10:12:06 +0000607 traceback.print_exc()
Darryl Green7c2dd582018-03-01 14:53:49 +0000608 sys.exit(2)
609
610
611if __name__ == "__main__":
612 run_main()