| #!/usr/bin/env python3 |
| |
| # SPDX-License-Identifier: GPL-2.0-or-later |
| |
| """ |
| get-wraps-from-cargo-registry.py - Update Meson subprojects from a global registry |
| """ |
| |
| # Copyright (C) 2025 Red Hat, Inc. |
| # |
| # Author: Paolo Bonzini <pbonzini@redhat.com> |
| |
| import argparse |
| import configparser |
| import filecmp |
| import glob |
| import os |
| import subprocess |
| import sys |
| |
| |
| def get_name_and_semver(namever: str) -> tuple[str, str]: |
| """Split a subproject name into its name and semantic version parts""" |
| parts = namever.rsplit("-", 1) |
| if len(parts) != 2: |
| return namever, "" |
| |
| return parts[0], parts[1] |
| |
| |
| class UpdateSubprojects: |
| cargo_registry: str |
| top_srcdir: str |
| dry_run: bool |
| changes: int = 0 |
| |
| def find_installed_crate(self, namever: str) -> str | None: |
| """Find installed crate matching name and semver prefix""" |
| name, semver = get_name_and_semver(namever) |
| |
| # exact version match |
| path = os.path.join(self.cargo_registry, f"{name}-{semver}") |
| if os.path.exists(path): |
| return f"{name}-{semver}" |
| |
| # semver match |
| matches = sorted(glob.glob(f"{path}.*")) |
| return os.path.basename(matches[0]) if matches else None |
| |
| def compare_build_rs(self, orig_dir: str, registry_namever: str) -> None: |
| """Warn if the build.rs in the original directory differs from the registry version.""" |
| orig_build_rs = os.path.join(orig_dir, "build.rs") |
| new_build_rs = os.path.join(self.cargo_registry, registry_namever, "build.rs") |
| |
| msg = None |
| if os.path.isfile(orig_build_rs) != os.path.isfile(new_build_rs): |
| if os.path.isfile(orig_build_rs): |
| msg = f"build.rs removed in {registry_namever}" |
| if os.path.isfile(new_build_rs): |
| msg = f"build.rs added in {registry_namever}" |
| |
| elif os.path.isfile(orig_build_rs) and not filecmp.cmp(orig_build_rs, new_build_rs): |
| msg = f"build.rs changed from {orig_dir} to {registry_namever}" |
| |
| if msg: |
| print(f"⚠️ Warning: {msg}") |
| print(" This may affect the build process - please review the differences.") |
| |
| def update_subproject(self, wrap_file: str, registry_namever: str) -> None: |
| """Modify [wrap-file] section to point to self.cargo_registry.""" |
| assert wrap_file.endswith("-rs.wrap") |
| wrap_name = wrap_file[:-5] |
| |
| env = os.environ.copy() |
| env["MESON_PACKAGE_CACHE_DIR"] = self.cargo_registry |
| |
| config = configparser.ConfigParser() |
| config.read(wrap_file) |
| if "wrap-file" not in config: |
| return |
| |
| # do not download the wrap, always use the local copy |
| orig_dir = config["wrap-file"]["directory"] |
| if os.path.exists(orig_dir) and orig_dir != registry_namever: |
| self.compare_build_rs(orig_dir, registry_namever) |
| |
| if self.dry_run: |
| if orig_dir == registry_namever: |
| print(f"Will install {orig_dir} from registry.") |
| else: |
| print(f"Will replace {orig_dir} with {registry_namever}.") |
| self.changes += 1 |
| return |
| |
| config["wrap-file"]["directory"] = registry_namever |
| for key in list(config["wrap-file"].keys()): |
| if key.startswith("source"): |
| del config["wrap-file"][key] |
| |
| # replace existing directory with installed version |
| if os.path.exists(orig_dir): |
| subprocess.run( |
| ["meson", "subprojects", "purge", "--confirm", wrap_name], |
| cwd=self.top_srcdir, |
| env=env, |
| check=True, |
| ) |
| |
| with open(wrap_file, "w") as f: |
| config.write(f) |
| |
| if orig_dir == registry_namever: |
| print(f"Installing {orig_dir} from registry.") |
| else: |
| print(f"Replacing {orig_dir} with {registry_namever}.") |
| patch_dir = config["wrap-file"]["patch_directory"] |
| patch_dir = os.path.join("packagefiles", patch_dir) |
| _, ver = registry_namever.rsplit("-", 1) |
| subprocess.run( |
| ["meson", "rewrite", "kwargs", "set", "project", "/", "version", ver], |
| cwd=patch_dir, |
| env=env, |
| check=True, |
| ) |
| |
| subprocess.run( |
| ["meson", "subprojects", "download", wrap_name], |
| cwd=self.top_srcdir, |
| env=env, |
| check=True, |
| ) |
| self.changes += 1 |
| |
| @staticmethod |
| def parse_cmdline() -> argparse.Namespace: |
| parser = argparse.ArgumentParser( |
| description="Replace Meson subprojects with packages in a Cargo registry" |
| ) |
| parser.add_argument( |
| "--cargo-registry", |
| default=os.environ.get("CARGO_REGISTRY"), |
| help="Path to Cargo registry (default: CARGO_REGISTRY env var)", |
| ) |
| parser.add_argument( |
| "--dry-run", |
| action="store_true", |
| default=False, |
| help="Do not actually replace anything", |
| ) |
| |
| args = parser.parse_args() |
| if not args.cargo_registry: |
| print("error: CARGO_REGISTRY environment variable not set and --cargo-registry not provided") |
| sys.exit(1) |
| |
| return args |
| |
| def __init__(self, args: argparse.Namespace): |
| self.cargo_registry = args.cargo_registry |
| self.dry_run = args.dry_run |
| self.top_srcdir = os.getcwd() |
| |
| def main(self) -> None: |
| if not os.path.exists("subprojects"): |
| print("'subprojects' directory not found, nothing to do.") |
| return |
| |
| os.chdir("subprojects") |
| for wrap_file in sorted(glob.glob("*-rs.wrap")): |
| namever = wrap_file[:-8] # Remove '-rs.wrap' |
| |
| registry_namever = self.find_installed_crate(namever) |
| if not registry_namever: |
| print(f"No installed crate found for {wrap_file}") |
| continue |
| |
| self.update_subproject(wrap_file, registry_namever) |
| |
| if self.changes: |
| if self.dry_run: |
| print("Rerun without --dry-run to apply changes.") |
| else: |
| print(f"✨ {self.changes} subproject(s) updated!") |
| else: |
| print("No changes.") |
| |
| |
| if __name__ == "__main__": |
| args = UpdateSubprojects.parse_cmdline() |
| UpdateSubprojects(args).main() |