1#!/bin/env python3 2# 3# Copyright © 2024 Institute of Software, CAS. All rights reserved. 4# 5# SPDX-License-Identifier: Apache-2.0 6# 7 8import subprocess 9import json 10from argparse import ArgumentParser 11from collections import defaultdict 12 13def get_cargo_metadata(): 14 result = subprocess.run( 15 ['cargo', 'metadata', '--format-version=1'], 16 capture_output=True, 17 text=True 18 ) 19 if result.returncode != 0: 20 exit(1) 21 22 metadata = json.loads(result.stdout) 23 return metadata 24 25def find_dependents_of_package(metadata, package_source): 26 """Find dependencies based on the provided source identifier and return related package info.""" 27 packages = defaultdict(list) 28 direct_dependents = defaultdict(list) 29 30 # Identify packages from the given package source and record version 31 for pkg in metadata['packages']: 32 repository = pkg['repository'] or '' 33 if package_source in repository: 34 packages[pkg['name']].append(pkg['version']) 35 36 # Find packages that immediately depend on the identified source packages 37 for node in metadata['resolve']['nodes']: 38 current_pkg = next(pkg for pkg in metadata['packages'] if pkg['id'] == node['id']) 39 current_pkg_name = current_pkg['name'] 40 current_pkg_version = current_pkg['version'] 41 42 for dep_id in node['dependencies']: 43 dep_pkg = next(pkg for pkg in metadata['packages'] if pkg['id'] == dep_id) 44 dep_name = dep_pkg['name'] 45 dep_version = dep_pkg['version'] 46 47 if dep_name in packages: 48 direct_dependents[(dep_name, dep_version)].append((current_pkg_name, current_pkg_version)) 49 50 return packages, direct_dependents 51 52def check_for_version_conflicts(packages, direct_dependents): 53 """Check if there are multiple versions of dependencies, and return True if conflicts are found.""" 54 has_conflicts = False 55 56 for pkg_name, versions in packages.items(): 57 if len(set(versions)) > 1: 58 has_conflicts = True 59 print(f"Error: Multiple versions detected for {pkg_name}: {set(versions)}") 60 for version in set(versions): 61 print(f" Version {version} used by:") 62 for dependent, dep_version in direct_dependents[(pkg_name, version)]: 63 print(f" - {dependent} v{dep_version}") 64 65 return has_conflicts 66 67if __name__ == '__main__': 68 parser = ArgumentParser(description='Cargo dependency conflict checker.') 69 parser.add_argument('package_source', type=str, help='A keyword used to match the repository URL field') 70 args = parser.parse_args() 71 72 metadata = get_cargo_metadata() 73 if metadata is None: 74 print("Error: Metadata is empty") 75 exit(1) 76 77 packages, direct_dependents = find_dependents_of_package(metadata, args.package_source) 78 79 has_conflicts = check_for_version_conflicts(packages, direct_dependents) 80 81 if has_conflicts: 82 exit(1) 83 84