xref: /cloud-hypervisor/scripts/package-consistency-check.py (revision 80b2c98a68d4c68f372f849e8d26f7cae5867000)
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