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