@samitouri / QOSamiQemu / commits / c0228b5293

scripts: add functionality to update wrap files from Cargo.lock

.wrap files and Cargo.lock contain pretty much the same information. QEMU uses the former to make it easier for distros to use a local registry, and also because Meson <1.13 does not use Cargo.lock for the "meson subprojects" command. This however introduces an inconvenience when running "cargo update", in that the wrap files have to be updated by hand. Add the possibility to get new versions from Cargo.lock and not just from a local registry, thus automating the update. For now, only existing wrap files are modified. This means that semver-breaking updates need to be done (at least partly) by hand; it is enough to rename the wrap file prior to running the script. While the need for this script is a lot smaller with Meson 1.13, it would still remain useful for packages that have a patch directory. Perhaps a bit overengineered, but since we have it... Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>

Paolo Bonzini committed Jul 24, 2026 at 16:18 UTC c0228b52931afb68439326f0e923f4d7d9cf5224
1 file changed +69 -8
scripts/get-wraps-from-cargo-registry.py
+69 -8
@@ -3,7 +3,8 @@
3 # SPDX-License-Identifier: GPL-2.0-or-later
4
5 """
6 -get-wraps-from-cargo-registry.py - Update Meson subprojects from a global registry
6 +get-wraps-from-cargo-registry.py - Update Meson subprojects from a Cargo
7 +registry or from the versions pinned in Cargo.lock.
8 """
9
10 # Copyright (C) 2025 Red Hat, Inc.
@@ -18,6 +19,7 @@ import os
19 import shutil
20 import subprocess
21 import sys
22 +import tomllib
23
24
25 def get_name_and_semver(namever: str) -> tuple[str, str]:
@@ -75,6 +77,49 @@ class CargoRegistry(CrateSource):
77 return True
78
79
80 +class CargoLock(CrateSource):
81 + """Locate crates by the versions pinned in a Cargo.lock file."""
82 +
83 + origin = "crates.io"
84 +
85 + def __init__(self, path: str):
86 + with open(path, "rb") as f:
87 + data = tomllib.load(f)
88 +
89 + self.versions: dict[str, list[str]] = {}
90 + self.checksums: dict[str, str] = {}
91 + for pkg in data.get("package", []):
92 + # workspace members have neither a checksum nor a crates.io tarball
93 + if "checksum" not in pkg:
94 + continue
95 + name, version = pkg["name"], pkg["version"]
96 + self.versions.setdefault(name, []).append(version)
97 + self.checksums[f"{name}-{version}"] = pkg["checksum"]
98 +
99 + def find(self, namever: str) -> str | None:
100 + """Find pinned crate matching name and semver prefix"""
101 + name, semver = get_name_and_semver(namever)
102 + versions = sorted(self.versions.get(name, []))
103 +
104 + # exact version match
105 + if semver in versions:
106 + return f"{name}-{semver}"
107 +
108 + # semver match
109 + matches = [v for v in versions if v.startswith(f"{semver}.")]
110 + return f"{name}-{matches[0]}" if matches else None
111 +
112 + def rewrite_source(self, section: configparser.SectionProxy, orig_namever: str, new_namever: str) -> bool:
113 + # rewrite the download keys to fetch the pinned version from crates.io.
114 + if orig_namever == new_namever:
115 + return False
116 + name, version = get_name_and_semver(new_namever)
117 + section["source_url"] = f"https://crates.io/api/v1/crates/{name}/{version}/download"
118 + section["source_filename"] = f"{new_namever}.tar.gz"
119 + section["source_hash"] = self.checksums[new_namever]
120 + return True
121 +
122 +
123 class UpdateSubprojects:
124 cargo_registry: str
125 source: CrateSource
@@ -104,12 +149,13 @@ class UpdateSubprojects:
149 print(" This may affect the build process - please review the differences.")
150
151 def update_subproject(self, wrap_file: str, source_namever: str) -> None:
107 - """Modify [wrap-file] section to point to self.cargo_registry."""
152 + """Modify [wrap-file] section to use the crate resolved as `source_namever`."""
153 assert wrap_file.endswith("-rs.wrap")
154 wrap_name = wrap_file[:-5]
155
156 env = os.environ.copy()
112 - env["MESON_PACKAGE_CACHE_DIR"] = self.cargo_registry
157 + if self.cargo_registry:
158 + env["MESON_PACKAGE_CACHE_DIR"] = self.cargo_registry
159
160 config = configparser.ConfigParser()
161 config.read(wrap_file)
@@ -157,9 +203,15 @@ class UpdateSubprojects:
203 parser = argparse.ArgumentParser(
204 description="Replace Meson subprojects with packages in a Cargo registry"
205 )
206 + parser.add_argument(
207 + "--cargo-lock",
208 + action='store_true',
209 + default=False,
210 + help="Update wraps from Cargo.lock",
211 + )
212 parser.add_argument(
213 "--cargo-registry",
162 - default=os.environ.get("CARGO_REGISTRY"),
214 + default=None,
215 help="Path to Cargo registry (default: CARGO_REGISTRY env var)",
216 )
217 parser.add_argument(
@@ -170,9 +222,15 @@ class UpdateSubprojects:
222 )
223
224 args = parser.parse_args()
173 - if not args.cargo_registry:
174 - print("error: CARGO_REGISTRY environment variable not set and --cargo-registry not provided")
225 + if args.cargo_registry and args.cargo_lock:
226 + print("error: --cargo-registry and --cargo-lock are incompatible")
227 sys.exit(1)
228 + if not args.cargo_registry and not args.cargo_lock:
229 + args.cargo_registry = os.environ.get("CARGO_REGISTRY")
230 + if not args.cargo_registry:
231 + print("error: CARGO_REGISTRY environment variable not set and " +
232 + "--cargo-registry or --cargo-lock not provided")
233 + sys.exit(1)
234
235 return args
236
@@ -180,7 +238,10 @@ class UpdateSubprojects:
238 self.cargo_registry = args.cargo_registry
239 self.dry_run = args.dry_run
240 self.top_srcdir = os.getcwd()
183 - self.source = CargoRegistry(args.cargo_registry)
241 + if args.cargo_lock:
242 + self.source = CargoLock(os.path.join(self.top_srcdir, "Cargo.lock"))
243 + else:
244 + self.source = CargoRegistry(args.cargo_registry)
245
246 def main(self) -> None:
247 if not os.path.exists("subprojects"):
@@ -193,7 +254,7 @@ class UpdateSubprojects:
254
255 source_namever = self.source.find(namever)
256 if not source_namever:
196 - print(f"No installed crate found for {wrap_file}")
257 + print(f"No crate found for {wrap_file}")
258 continue
259
260 self.update_subproject(wrap_file, source_namever)