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