scripts: add script to help distros use global Rust packages

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 ...

Reviewed-by: Neal Gompa <ngompa@fedoraproject.org>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
This commit is contained in:
Paolo Bonzini
2025-07-22 09:50:01 +02:00
parent f63000d943
commit fbc8fb36e3
3 changed files with 199 additions and 0 deletions

View File

@@ -3511,6 +3511,7 @@ S: Maintained
F: rust/qemu-api
F: rust/qemu-api-macros
F: rust/rustfmt.toml
F: scripts/get-wraps-from-cargo-registry.py
Rust-related patches CC here
L: qemu-rust@nongnu.org

View File

@@ -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.

View File

@@ -0,0 +1,190 @@
#!/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()