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