On Tue, Jul 22, 2025 at 11:36 AM Paolo Bonzini <pbonz...@redhat.com> wrote:
>
> Some distros prefer to avoid vendored crate sources, and instead use
> local sources from e.g. ``/usr/share/cargo/registry``.  Add a
> script, inspired by the Mesa spec file(*), that automatically
> performs this task.  The script is meant to be invoked after unpacking
> the QEMU tarball.
>
> (*) This is the hack that Mesa uses:
>
>     export MESON_PACKAGE_CACHE_DIR="%{cargo_registry}/"
>     %define inst_crate_nameversion() %(basename %{cargo_registry}/%{1}-*)
>     %define rewrite_wrap_file() sed -e "/source.*/d" -e 
> "s/%{1}-.*/%{inst_crate_nameversion %{1}}/" -i subprojects/%{1}.wrap
>     %rewrite_wrap_file proc-macro2
>     ... more %rewrite_wrap_file invocations follow ...
>
> Signed-off-by: Paolo Bonzini <pbonz...@redhat.com>
> ---
>  docs/about/build-platforms.rst           |   8 +
>  scripts/get-wraps-from-cargo-registry.py | 191 +++++++++++++++++++++++
>  2 files changed, 199 insertions(+)
>  create mode 100755 scripts/get-wraps-from-cargo-registry.py
>
> diff --git a/docs/about/build-platforms.rst b/docs/about/build-platforms.rst
> index 8ecbd6b26f7..8671c3be9cd 100644
> --- a/docs/about/build-platforms.rst
> +++ b/docs/about/build-platforms.rst
> @@ -127,6 +127,14 @@ Rust build dependencies
>    (or newer) package.  The path to ``rustc`` and ``rustdoc`` must be
>    provided manually to the configure script.
>
> +  Some distros prefer to avoid vendored crate sources, and instead use
> +  local sources from e.g. ``/usr/share/cargo/registry``.  QEMU includes a
> +  script, ``scripts/get-wraps-from-cargo-registry.py``, that automatically
> +  performs this task.  The script is meant to be invoked after unpacking
> +  the QEMU tarball.  QEMU also includes ``rust/Cargo.toml`` and
> +  ``rust/Cargo.lock`` files that can be used to compute QEMU's build
> +  dependencies, e.g. using ``cargo2rpm -p rust/Cargo.toml buildrequires``.
> +
>  Optional build dependencies
>    Build components whose absence does not affect the ability to build QEMU
>    may not be available in distros, or may be too old for our requirements.
> diff --git a/scripts/get-wraps-from-cargo-registry.py 
> b/scripts/get-wraps-from-cargo-registry.py
> new file mode 100755
> index 00000000000..6b76d00a6d9
> --- /dev/null
> +++ b/scripts/get-wraps-from-cargo-registry.py
> @@ -0,0 +1,191 @@
> +#!/usr/bin/env python3
> +
> +"""
> +get-wraps-from-cargo-registry.py - Update Meson subprojects from a global 
> registry
> +"""
> +
> +# Copyright (C) 2025 Red Hat, Inc.
> +#
> +# Author: Paolo Bonzini <pbonz...@redhat.com>
> +#
> +# This work is licensed under the terms of the GNU GPL, version 2 or
> +# later. See the COPYING file in the top-level directory.
> +

Nit, missing:

# SPDX-License-Identifier:

> +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}.")
> +
> +        if orig_dir != 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,
> +        )
> +
> +    @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()
> --
> 2.50.1
>
>

Reply via email to