diff --git a/scripts/package-consistency-check.py b/scripts/package-consistency-check.py new file mode 100755 index 000000000..4bd533ace --- /dev/null +++ b/scripts/package-consistency-check.py @@ -0,0 +1,83 @@ +#!/bin/env python3 +# +# Copyright © 2024 Institute of Software, CAS. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import subprocess +import json +from argparse import ArgumentParser +from collections import defaultdict + +def get_cargo_metadata(): + result = subprocess.run( + ['cargo', 'metadata', '--format-version=1'], + capture_output=True, + text=True + ) + if result.returncode != 0: + exit(1) + + metadata = json.loads(result.stdout) + return metadata + +def find_dependents_of_package(metadata, package_source): + """Find dependencies based on the provided source identifier and return related package info.""" + packages = defaultdict(list) + direct_dependents = defaultdict(list) + + # Identify packages from the given package source and record version + for pkg in metadata['packages']: + repository = pkg['repository'] or '' + if package_source in repository: + packages[pkg['name']].append(pkg['version']) + + # Find packages that immediately depend on the identified source packages + for node in metadata['resolve']['nodes']: + current_pkg = next(pkg for pkg in metadata['packages'] if pkg['id'] == node['id']) + current_pkg_name = current_pkg['name'] + current_pkg_version = current_pkg['version'] + + for dep_id in node['dependencies']: + dep_pkg = next(pkg for pkg in metadata['packages'] if pkg['id'] == dep_id) + dep_name = dep_pkg['name'] + dep_version = dep_pkg['version'] + + if dep_name in packages: + direct_dependents[(dep_name, dep_version)].append((current_pkg_name, current_pkg_version)) + + return packages, direct_dependents + +def check_for_version_conflicts(packages, direct_dependents): + """Check if there are multiple versions of dependencies, and return True if conflicts are found.""" + has_conflicts = False + + for pkg_name, versions in packages.items(): + if len(set(versions)) > 1: + has_conflicts = True + print(f"Error: Multiple versions detected for {pkg_name}: {set(versions)}") + for version in set(versions): + print(f" Version {version} used by:") + for dependent, dep_version in direct_dependents[(pkg_name, version)]: + print(f" - {dependent} v{dep_version}") + + return has_conflicts + +if __name__ == '__main__': + parser = ArgumentParser(description='Cargo dependency conflict checker.') + parser.add_argument('package_source', type=str, help='A keyword used to match the repository URL field') + args = parser.parse_args() + + metadata = get_cargo_metadata() + if metadata is None: + print("Error: Metadata is empty") + exit(1) + + packages, direct_dependents = find_dependents_of_package(metadata, args.package_source) + + has_conflicts = check_for_version_conflicts(packages, direct_dependents) + + if has_conflicts: + exit(1) +