xref: /openbmc/qemu/scripts/get-wraps-from-cargo-registry.py (revision 26453a7f3572068d2731c9f712b26ca2f74097e0)
1#!/usr/bin/env python3
2
3# SPDX-License-Identifier: GPL-2.0-or-later
4
5"""
6get-wraps-from-cargo-registry.py - Update Meson subprojects from a global registry
7"""
8
9# Copyright (C) 2025 Red Hat, Inc.
10#
11# Author: Paolo Bonzini <pbonzini@redhat.com>
12
13import argparse
14import configparser
15import filecmp
16import glob
17import os
18import subprocess
19import sys
20
21
22def get_name_and_semver(namever: str) -> tuple[str, str]:
23    """Split a subproject name into its name and semantic version parts"""
24    parts = namever.rsplit("-", 1)
25    if len(parts) != 2:
26        return namever, ""
27
28    return parts[0], parts[1]
29
30
31class UpdateSubprojects:
32    cargo_registry: str
33    top_srcdir: str
34    dry_run: bool
35    changes: int = 0
36
37    def find_installed_crate(self, namever: str) -> str | None:
38        """Find installed crate matching name and semver prefix"""
39        name, semver = get_name_and_semver(namever)
40
41        # exact version match
42        path = os.path.join(self.cargo_registry, f"{name}-{semver}")
43        if os.path.exists(path):
44            return f"{name}-{semver}"
45
46        # semver match
47        matches = sorted(glob.glob(f"{path}.*"))
48        return os.path.basename(matches[0]) if matches else None
49
50    def compare_build_rs(self, orig_dir: str, registry_namever: str) -> None:
51        """Warn if the build.rs in the original directory differs from the registry version."""
52        orig_build_rs = os.path.join(orig_dir, "build.rs")
53        new_build_rs = os.path.join(self.cargo_registry, registry_namever, "build.rs")
54
55        msg = None
56        if os.path.isfile(orig_build_rs) != os.path.isfile(new_build_rs):
57            if os.path.isfile(orig_build_rs):
58                msg = f"build.rs removed in {registry_namever}"
59            if os.path.isfile(new_build_rs):
60                msg = f"build.rs added in {registry_namever}"
61
62        elif os.path.isfile(orig_build_rs) and not filecmp.cmp(orig_build_rs, new_build_rs):
63            msg = f"build.rs changed from {orig_dir} to {registry_namever}"
64
65        if msg:
66            print(f"⚠️  Warning: {msg}")
67            print("   This may affect the build process - please review the differences.")
68
69    def update_subproject(self, wrap_file: str, registry_namever: str) -> None:
70        """Modify [wrap-file] section to point to self.cargo_registry."""
71        assert wrap_file.endswith("-rs.wrap")
72        wrap_name = wrap_file[:-5]
73
74        env = os.environ.copy()
75        env["MESON_PACKAGE_CACHE_DIR"] = self.cargo_registry
76
77        config = configparser.ConfigParser()
78        config.read(wrap_file)
79        if "wrap-file" not in config:
80            return
81
82        # do not download the wrap, always use the local copy
83        orig_dir = config["wrap-file"]["directory"]
84        if os.path.exists(orig_dir) and orig_dir != registry_namever:
85            self.compare_build_rs(orig_dir, registry_namever)
86
87        if self.dry_run:
88            if orig_dir == registry_namever:
89                print(f"Will install {orig_dir} from registry.")
90            else:
91                print(f"Will replace {orig_dir} with {registry_namever}.")
92            self.changes += 1
93            return
94
95        config["wrap-file"]["directory"] = registry_namever
96        for key in list(config["wrap-file"].keys()):
97            if key.startswith("source"):
98                del config["wrap-file"][key]
99
100        # replace existing directory with installed version
101        if os.path.exists(orig_dir):
102            subprocess.run(
103                ["meson", "subprojects", "purge", "--confirm", wrap_name],
104                cwd=self.top_srcdir,
105                env=env,
106                check=True,
107            )
108
109        with open(wrap_file, "w") as f:
110            config.write(f)
111
112        if orig_dir == registry_namever:
113            print(f"Installing {orig_dir} from registry.")
114        else:
115            print(f"Replacing {orig_dir} with {registry_namever}.")
116            patch_dir = config["wrap-file"]["patch_directory"]
117            patch_dir = os.path.join("packagefiles", patch_dir)
118            _, ver = registry_namever.rsplit("-", 1)
119            subprocess.run(
120                ["meson", "rewrite", "kwargs", "set", "project", "/", "version", ver],
121                cwd=patch_dir,
122                env=env,
123                check=True,
124            )
125
126        subprocess.run(
127            ["meson", "subprojects", "download", wrap_name],
128            cwd=self.top_srcdir,
129            env=env,
130            check=True,
131        )
132        self.changes += 1
133
134    @staticmethod
135    def parse_cmdline() -> argparse.Namespace:
136        parser = argparse.ArgumentParser(
137            description="Replace Meson subprojects with packages in a Cargo registry"
138        )
139        parser.add_argument(
140            "--cargo-registry",
141            default=os.environ.get("CARGO_REGISTRY"),
142            help="Path to Cargo registry (default: CARGO_REGISTRY env var)",
143        )
144        parser.add_argument(
145            "--dry-run",
146            action="store_true",
147            default=False,
148            help="Do not actually replace anything",
149        )
150
151        args = parser.parse_args()
152        if not args.cargo_registry:
153            print("error: CARGO_REGISTRY environment variable not set and --cargo-registry not provided")
154            sys.exit(1)
155
156        return args
157
158    def __init__(self, args: argparse.Namespace):
159        self.cargo_registry = args.cargo_registry
160        self.dry_run = args.dry_run
161        self.top_srcdir = os.getcwd()
162
163    def main(self) -> None:
164        if not os.path.exists("subprojects"):
165            print("'subprojects' directory not found, nothing to do.")
166            return
167
168        os.chdir("subprojects")
169        for wrap_file in sorted(glob.glob("*-rs.wrap")):
170            namever = wrap_file[:-8]  # Remove '-rs.wrap'
171
172            registry_namever = self.find_installed_crate(namever)
173            if not registry_namever:
174                print(f"No installed crate found for {wrap_file}")
175                continue
176
177            self.update_subproject(wrap_file, registry_namever)
178
179        if self.changes:
180            if self.dry_run:
181                print("Rerun without --dry-run to apply changes.")
182            else:
183                print(f"✨ {self.changes} subproject(s) updated!")
184        else:
185            print("No changes.")
186
187
188if __name__ == "__main__":
189    args = UpdateSubprojects.parse_cmdline()
190    UpdateSubprojects(args).main()
191