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