misc.python.materialize.cargo
A pure Python metadata parser for Cargo, Rust's package manager.
See the Cargo documentation for details. Only the features that are presently necessary to support this repository are implemented.
1# Copyright Materialize, Inc. and contributors. All rights reserved. 2# 3# Use of this software is governed by the Business Source License 4# included in the LICENSE file at the root of this repository. 5# 6# As of the Change Date specified in that file, in accordance with 7# the Business Source License, use of this software will be governed 8# by the Apache License, Version 2.0. 9 10"""A pure Python metadata parser for Cargo, Rust's package manager. 11 12See the [Cargo][] documentation for details. Only the features that are presently 13necessary to support this repository are implemented. 14 15[Cargo]: https://doc.rust-lang.org/cargo/ 16""" 17 18from functools import cache 19from pathlib import Path 20 21import toml 22 23from materialize import git 24 25 26class Crate: 27 """A Cargo crate. 28 29 A crate directory must contain a `Cargo.toml` file with `package.name` and 30 `package.version` keys. 31 32 Args: 33 root: The path to the root of the workspace. 34 path: The path to the crate directory. 35 36 Attributes: 37 name: The name of the crate. 38 version: The version of the crate. 39 features: The features of the crate. 40 path: The path to the crate. 41 path_build_dependencies: The build dependencies which are declared 42 using paths. 43 path_dev_dependencies: The dev dependencies which are declared using 44 paths. 45 path_dependencies: The dependencies which are declared using paths. 46 non_workspace_deps: The dependencies which are declared neither using 47 paths nor with `workspace = true`, including target-specific ones, 48 mapped to the sections that declare them. 49 rust_version: The minimum Rust version declared in the crate, if any. 50 bins: The names of all binaries in the crate. 51 examples: The names of all examples in the crate. 52 """ 53 54 _inputs_cache: set[str] | None 55 56 def __init__(self, root: Path, path: Path): 57 self.root = root 58 self._inputs_cache = None 59 with open(path / "Cargo.toml") as f: 60 config = toml.load(f) 61 self.name = config["package"]["name"] 62 self.version_string = config["package"]["version"] 63 self.features = config.get("features", {}) 64 self.path = path 65 self.path_build_dependencies: set[str] = set() 66 self.path_dev_dependencies: set[str] = set() 67 self.path_dependencies: set[str] = set() 68 self.non_workspace_deps: dict[str, list[str]] = {} 69 for dep_type, field in [ 70 ("build-dependencies", self.path_build_dependencies), 71 ("dev-dependencies", self.path_dev_dependencies), 72 ("dependencies", self.path_dependencies), 73 ]: 74 if dep_type in config: 75 for name, c in config[dep_type].items(): 76 if isinstance(c, dict) and "path" in c: 77 field.add(c.get("package", name)) 78 elif isinstance(c, dict) and c.get("workspace"): 79 pass 80 else: 81 self.non_workspace_deps.setdefault(name, []).append(dep_type) 82 for target, target_config in config.get("target", {}).items(): 83 for dep_type in ("build-dependencies", "dev-dependencies", "dependencies"): 84 for name, c in target_config.get(dep_type, {}).items(): 85 if isinstance(c, dict) and ("path" in c or c.get("workspace")): 86 continue 87 self.non_workspace_deps.setdefault(name, []).append( 88 f"target.'{target}'.{dep_type}" 89 ) 90 self.rust_version: str | None = None 91 try: 92 self.rust_version = str(config["package"]["rust-version"]) 93 except KeyError: 94 pass 95 self.bins = [] 96 if "bin" in config: 97 for bin in config["bin"]: 98 self.bins.append(bin["name"]) 99 if config["package"].get("autobins", True): 100 if (path / "src" / "main.rs").exists(): 101 self.bins.append(self.name) 102 for p in (path / "src" / "bin").glob("*.rs"): 103 self.bins.append(p.stem) 104 for p in (path / "src" / "bin").glob("*/main.rs"): 105 self.bins.append(p.parent.stem) 106 self.examples = [] 107 if "example" in config: 108 for example in config["example"]: 109 self.examples.append(example["name"]) 110 if config["package"].get("autoexamples", True): 111 for p in (path / "examples").glob("*.rs"): 112 self.examples.append(p.stem) 113 for p in (path / "examples").glob("*/main.rs"): 114 self.examples.append(p.parent.stem) 115 116 def inputs(self) -> set[str]: 117 """Compute the files that can impact the compilation of this crate. 118 119 Note that the returned list may have false positives (i.e., include 120 files that do not in fact impact the compilation of this crate), but it 121 is not believed to have false negatives. 122 123 Returns: 124 inputs: A list of input files, relative to the root of the 125 Cargo workspace. 126 """ 127 # NOTE(benesch): it would be nice to have fine-grained tracking of only 128 # exactly the files that go into a Rust crate, but doing this properly 129 # requires parsing Rust code, and we don't want to force a dependency on 130 # a Rust toolchain for users running demos. Instead, we assume that all†131 # files in a crate's directory are inputs to that crate. 132 # 133 # †As a development convenience, we omit mzcompose configuration files 134 # within a crate. This is technically incorrect if someone writes 135 # `include!("mzcompose.py")`, but that seems like a crazy thing to do. 136 if self._inputs_cache is not None: 137 return self._inputs_cache 138 return git.expand_globs( 139 self.root, 140 f"{self.path}/**", 141 f":(exclude){self.path}/mzcompose", 142 f":(exclude){self.path}/mzcompose.py", 143 ) 144 145 146class Workspace: 147 """A Cargo workspace. 148 149 A workspace directory must contain a `Cargo.toml` file with a 150 `workspace.members` key. 151 152 Args: 153 root: The path to the root of the workspace. 154 155 Attributes: 156 root: The path to the root of the workspace. 157 crates: A mapping from name to crate definition. 158 """ 159 160 def __init__(self, root: Path): 161 self.root = root 162 with open(root / "Cargo.toml") as f: 163 config = toml.load(f) 164 165 workspace_config = config["workspace"] 166 167 self.crates: dict[str, Crate] = {} 168 for path in workspace_config["members"]: 169 crate = Crate(root, root / path) 170 self.crates[crate.name] = crate 171 self.exclude: dict[str, Crate] = {} 172 for path in workspace_config.get("exclude", []): 173 if path.endswith("*") and (root / path.rstrip("*")).exists(): 174 for item in (root / path.rstrip("*")).iterdir(): 175 if item.is_dir() and (item / "Cargo.toml").exists(): 176 crate = Crate(root, root / item) 177 self.exclude[crate.name] = crate 178 self.all_crates = self.crates | self.exclude 179 180 self.default_members: list[str] = workspace_config.get("default-members", []) 181 self.workspace_dependencies: dict[str, object] = workspace_config.get( 182 "dependencies", {} 183 ) 184 185 self.rust_version: str | None = None 186 try: 187 self.rust_version = workspace_config["package"].get("rust-version") 188 except KeyError: 189 pass 190 191 def crate_for_bin(self, bin: str) -> Crate: 192 """Find the crate containing the named binary. 193 194 Args: 195 bin: The name of the binary to find. 196 197 Raises: 198 ValueError: The named binary did not exist in exactly one crate in 199 the Cargo workspace. 200 """ 201 out = None 202 for crate in self.crates.values(): 203 for b in crate.bins: 204 if b == bin: 205 if out is not None: 206 raise ValueError( 207 f"bin {bin} appears more than once in cargo workspace" 208 ) 209 out = crate 210 if out is None: 211 raise ValueError(f"bin {bin} does not exist in cargo workspace") 212 return out 213 214 def crate_for_example(self, example: str) -> Crate: 215 """Find the crate containing the named example. 216 217 Args: 218 example: The name of the example to find. 219 220 Raises: 221 ValueError: The named example did not exist in exactly one crate in 222 the Cargo workspace. 223 """ 224 out = None 225 for crate in self.crates.values(): 226 for e in crate.examples: 227 if e == example: 228 if out is not None: 229 raise ValueError( 230 f"example {example} appears more than once in cargo workspace" 231 ) 232 out = crate 233 if out is None: 234 raise ValueError(f"example {example} does not exist in cargo workspace") 235 return out 236 237 def transitive_path_dependencies( 238 self, crate: Crate, dev: bool = False 239 ) -> set[Crate]: 240 """Collects the transitive path dependencies of the requested crate. 241 242 Note that only _path_ dependencies are collected. Other types of 243 dependencies, like registry or Git dependencies, are not collected. 244 245 Args: 246 crate: The crate object from which to start the dependency crawl. 247 dev: Whether to consider dev dependencies in the root crate. 248 249 Returns: 250 crate_set: A set of all of the crates in this Cargo workspace upon 251 which the input crate depended upon, whether directly or 252 transitively. 253 254 Raises: 255 IndexError: The input crate did not exist. 256 """ 257 deps = set() 258 259 @cache 260 def visit(c: Crate) -> None: 261 deps.add(c) 262 for d in c.path_dependencies: 263 visit(self.crates[d]) 264 for d in c.path_build_dependencies: 265 visit(self.crates[d]) 266 267 visit(crate) 268 if dev: 269 for d in crate.path_dev_dependencies: 270 visit(self.crates[d]) 271 return deps 272 273 def precompute_crate_inputs(self) -> None: 274 """Pre-fetch all crate input files in a single batched git call. 275 276 This replaces ~118 individual pairs of git subprocess calls with 277 a single pair, then partitions the results by crate path in Python. 278 """ 279 from materialize import spawn 280 281 root = next(iter(self.all_crates.values())).root 282 # Use paths relative to root for git specs and partitioning, since 283 # git --relative outputs paths relative to cwd (root). Crate paths 284 # may be absolute when MZ_ROOT is an absolute path. 285 crate_rel_paths = sorted( 286 set(str(c.path.relative_to(root)) for c in self.all_crates.values()) 287 ) 288 289 specs = [] 290 for p in crate_rel_paths: 291 specs.append(f"{p}/**") 292 specs.append(f":(exclude){p}/mzcompose") 293 specs.append(f":(exclude){p}/mzcompose.py") 294 295 empty_tree = "4b825dc642cb6eb9a060e54bf8d69288fbee4904" 296 diff_files = spawn.capture( 297 ["git", "diff", "--name-only", "-z", "--relative", empty_tree, "--"] 298 + specs, 299 cwd=root, 300 ) 301 ls_files = spawn.capture( 302 ["git", "ls-files", "--others", "--exclude-standard", "-z", "--"] + specs, 303 cwd=root, 304 ) 305 all_files = set( 306 f for f in (diff_files + ls_files).split("\0") if f.strip() != "" 307 ) 308 309 # Partition files by crate path (longest match first for nested crates) 310 crate_file_map: dict[str, set[str]] = {p: set() for p in crate_rel_paths} 311 sorted_paths = sorted(crate_rel_paths, key=len, reverse=True) 312 for f in all_files: 313 for cp in sorted_paths: 314 if f.startswith(cp + "/"): 315 crate_file_map[cp].add(f) 316 break 317 318 # Inject cached results into each Crate object 319 for crate in self.all_crates.values(): 320 rel = str(crate.path.relative_to(root)) 321 crate._inputs_cache = crate_file_map.get(rel, set())
27class Crate: 28 """A Cargo crate. 29 30 A crate directory must contain a `Cargo.toml` file with `package.name` and 31 `package.version` keys. 32 33 Args: 34 root: The path to the root of the workspace. 35 path: The path to the crate directory. 36 37 Attributes: 38 name: The name of the crate. 39 version: The version of the crate. 40 features: The features of the crate. 41 path: The path to the crate. 42 path_build_dependencies: The build dependencies which are declared 43 using paths. 44 path_dev_dependencies: The dev dependencies which are declared using 45 paths. 46 path_dependencies: The dependencies which are declared using paths. 47 non_workspace_deps: The dependencies which are declared neither using 48 paths nor with `workspace = true`, including target-specific ones, 49 mapped to the sections that declare them. 50 rust_version: The minimum Rust version declared in the crate, if any. 51 bins: The names of all binaries in the crate. 52 examples: The names of all examples in the crate. 53 """ 54 55 _inputs_cache: set[str] | None 56 57 def __init__(self, root: Path, path: Path): 58 self.root = root 59 self._inputs_cache = None 60 with open(path / "Cargo.toml") as f: 61 config = toml.load(f) 62 self.name = config["package"]["name"] 63 self.version_string = config["package"]["version"] 64 self.features = config.get("features", {}) 65 self.path = path 66 self.path_build_dependencies: set[str] = set() 67 self.path_dev_dependencies: set[str] = set() 68 self.path_dependencies: set[str] = set() 69 self.non_workspace_deps: dict[str, list[str]] = {} 70 for dep_type, field in [ 71 ("build-dependencies", self.path_build_dependencies), 72 ("dev-dependencies", self.path_dev_dependencies), 73 ("dependencies", self.path_dependencies), 74 ]: 75 if dep_type in config: 76 for name, c in config[dep_type].items(): 77 if isinstance(c, dict) and "path" in c: 78 field.add(c.get("package", name)) 79 elif isinstance(c, dict) and c.get("workspace"): 80 pass 81 else: 82 self.non_workspace_deps.setdefault(name, []).append(dep_type) 83 for target, target_config in config.get("target", {}).items(): 84 for dep_type in ("build-dependencies", "dev-dependencies", "dependencies"): 85 for name, c in target_config.get(dep_type, {}).items(): 86 if isinstance(c, dict) and ("path" in c or c.get("workspace")): 87 continue 88 self.non_workspace_deps.setdefault(name, []).append( 89 f"target.'{target}'.{dep_type}" 90 ) 91 self.rust_version: str | None = None 92 try: 93 self.rust_version = str(config["package"]["rust-version"]) 94 except KeyError: 95 pass 96 self.bins = [] 97 if "bin" in config: 98 for bin in config["bin"]: 99 self.bins.append(bin["name"]) 100 if config["package"].get("autobins", True): 101 if (path / "src" / "main.rs").exists(): 102 self.bins.append(self.name) 103 for p in (path / "src" / "bin").glob("*.rs"): 104 self.bins.append(p.stem) 105 for p in (path / "src" / "bin").glob("*/main.rs"): 106 self.bins.append(p.parent.stem) 107 self.examples = [] 108 if "example" in config: 109 for example in config["example"]: 110 self.examples.append(example["name"]) 111 if config["package"].get("autoexamples", True): 112 for p in (path / "examples").glob("*.rs"): 113 self.examples.append(p.stem) 114 for p in (path / "examples").glob("*/main.rs"): 115 self.examples.append(p.parent.stem) 116 117 def inputs(self) -> set[str]: 118 """Compute the files that can impact the compilation of this crate. 119 120 Note that the returned list may have false positives (i.e., include 121 files that do not in fact impact the compilation of this crate), but it 122 is not believed to have false negatives. 123 124 Returns: 125 inputs: A list of input files, relative to the root of the 126 Cargo workspace. 127 """ 128 # NOTE(benesch): it would be nice to have fine-grained tracking of only 129 # exactly the files that go into a Rust crate, but doing this properly 130 # requires parsing Rust code, and we don't want to force a dependency on 131 # a Rust toolchain for users running demos. Instead, we assume that all†132 # files in a crate's directory are inputs to that crate. 133 # 134 # †As a development convenience, we omit mzcompose configuration files 135 # within a crate. This is technically incorrect if someone writes 136 # `include!("mzcompose.py")`, but that seems like a crazy thing to do. 137 if self._inputs_cache is not None: 138 return self._inputs_cache 139 return git.expand_globs( 140 self.root, 141 f"{self.path}/**", 142 f":(exclude){self.path}/mzcompose", 143 f":(exclude){self.path}/mzcompose.py", 144 )
A Cargo crate.
A crate directory must contain a Cargo.toml file with package.name and
package.version keys.
Args: root: The path to the root of the workspace. path: The path to the crate directory.
Attributes:
name: The name of the crate.
version: The version of the crate.
features: The features of the crate.
path: The path to the crate.
path_build_dependencies: The build dependencies which are declared
using paths.
path_dev_dependencies: The dev dependencies which are declared using
paths.
path_dependencies: The dependencies which are declared using paths.
non_workspace_deps: The dependencies which are declared neither using
paths nor with workspace = true, including target-specific ones,
mapped to the sections that declare them.
rust_version: The minimum Rust version declared in the crate, if any.
bins: The names of all binaries in the crate.
examples: The names of all examples in the crate.
57 def __init__(self, root: Path, path: Path): 58 self.root = root 59 self._inputs_cache = None 60 with open(path / "Cargo.toml") as f: 61 config = toml.load(f) 62 self.name = config["package"]["name"] 63 self.version_string = config["package"]["version"] 64 self.features = config.get("features", {}) 65 self.path = path 66 self.path_build_dependencies: set[str] = set() 67 self.path_dev_dependencies: set[str] = set() 68 self.path_dependencies: set[str] = set() 69 self.non_workspace_deps: dict[str, list[str]] = {} 70 for dep_type, field in [ 71 ("build-dependencies", self.path_build_dependencies), 72 ("dev-dependencies", self.path_dev_dependencies), 73 ("dependencies", self.path_dependencies), 74 ]: 75 if dep_type in config: 76 for name, c in config[dep_type].items(): 77 if isinstance(c, dict) and "path" in c: 78 field.add(c.get("package", name)) 79 elif isinstance(c, dict) and c.get("workspace"): 80 pass 81 else: 82 self.non_workspace_deps.setdefault(name, []).append(dep_type) 83 for target, target_config in config.get("target", {}).items(): 84 for dep_type in ("build-dependencies", "dev-dependencies", "dependencies"): 85 for name, c in target_config.get(dep_type, {}).items(): 86 if isinstance(c, dict) and ("path" in c or c.get("workspace")): 87 continue 88 self.non_workspace_deps.setdefault(name, []).append( 89 f"target.'{target}'.{dep_type}" 90 ) 91 self.rust_version: str | None = None 92 try: 93 self.rust_version = str(config["package"]["rust-version"]) 94 except KeyError: 95 pass 96 self.bins = [] 97 if "bin" in config: 98 for bin in config["bin"]: 99 self.bins.append(bin["name"]) 100 if config["package"].get("autobins", True): 101 if (path / "src" / "main.rs").exists(): 102 self.bins.append(self.name) 103 for p in (path / "src" / "bin").glob("*.rs"): 104 self.bins.append(p.stem) 105 for p in (path / "src" / "bin").glob("*/main.rs"): 106 self.bins.append(p.parent.stem) 107 self.examples = [] 108 if "example" in config: 109 for example in config["example"]: 110 self.examples.append(example["name"]) 111 if config["package"].get("autoexamples", True): 112 for p in (path / "examples").glob("*.rs"): 113 self.examples.append(p.stem) 114 for p in (path / "examples").glob("*/main.rs"): 115 self.examples.append(p.parent.stem)
117 def inputs(self) -> set[str]: 118 """Compute the files that can impact the compilation of this crate. 119 120 Note that the returned list may have false positives (i.e., include 121 files that do not in fact impact the compilation of this crate), but it 122 is not believed to have false negatives. 123 124 Returns: 125 inputs: A list of input files, relative to the root of the 126 Cargo workspace. 127 """ 128 # NOTE(benesch): it would be nice to have fine-grained tracking of only 129 # exactly the files that go into a Rust crate, but doing this properly 130 # requires parsing Rust code, and we don't want to force a dependency on 131 # a Rust toolchain for users running demos. Instead, we assume that all†132 # files in a crate's directory are inputs to that crate. 133 # 134 # †As a development convenience, we omit mzcompose configuration files 135 # within a crate. This is technically incorrect if someone writes 136 # `include!("mzcompose.py")`, but that seems like a crazy thing to do. 137 if self._inputs_cache is not None: 138 return self._inputs_cache 139 return git.expand_globs( 140 self.root, 141 f"{self.path}/**", 142 f":(exclude){self.path}/mzcompose", 143 f":(exclude){self.path}/mzcompose.py", 144 )
Compute the files that can impact the compilation of this crate.
Note that the returned list may have false positives (i.e., include files that do not in fact impact the compilation of this crate), but it is not believed to have false negatives.
Returns: inputs: A list of input files, relative to the root of the Cargo workspace.
147class Workspace: 148 """A Cargo workspace. 149 150 A workspace directory must contain a `Cargo.toml` file with a 151 `workspace.members` key. 152 153 Args: 154 root: The path to the root of the workspace. 155 156 Attributes: 157 root: The path to the root of the workspace. 158 crates: A mapping from name to crate definition. 159 """ 160 161 def __init__(self, root: Path): 162 self.root = root 163 with open(root / "Cargo.toml") as f: 164 config = toml.load(f) 165 166 workspace_config = config["workspace"] 167 168 self.crates: dict[str, Crate] = {} 169 for path in workspace_config["members"]: 170 crate = Crate(root, root / path) 171 self.crates[crate.name] = crate 172 self.exclude: dict[str, Crate] = {} 173 for path in workspace_config.get("exclude", []): 174 if path.endswith("*") and (root / path.rstrip("*")).exists(): 175 for item in (root / path.rstrip("*")).iterdir(): 176 if item.is_dir() and (item / "Cargo.toml").exists(): 177 crate = Crate(root, root / item) 178 self.exclude[crate.name] = crate 179 self.all_crates = self.crates | self.exclude 180 181 self.default_members: list[str] = workspace_config.get("default-members", []) 182 self.workspace_dependencies: dict[str, object] = workspace_config.get( 183 "dependencies", {} 184 ) 185 186 self.rust_version: str | None = None 187 try: 188 self.rust_version = workspace_config["package"].get("rust-version") 189 except KeyError: 190 pass 191 192 def crate_for_bin(self, bin: str) -> Crate: 193 """Find the crate containing the named binary. 194 195 Args: 196 bin: The name of the binary to find. 197 198 Raises: 199 ValueError: The named binary did not exist in exactly one crate in 200 the Cargo workspace. 201 """ 202 out = None 203 for crate in self.crates.values(): 204 for b in crate.bins: 205 if b == bin: 206 if out is not None: 207 raise ValueError( 208 f"bin {bin} appears more than once in cargo workspace" 209 ) 210 out = crate 211 if out is None: 212 raise ValueError(f"bin {bin} does not exist in cargo workspace") 213 return out 214 215 def crate_for_example(self, example: str) -> Crate: 216 """Find the crate containing the named example. 217 218 Args: 219 example: The name of the example to find. 220 221 Raises: 222 ValueError: The named example did not exist in exactly one crate in 223 the Cargo workspace. 224 """ 225 out = None 226 for crate in self.crates.values(): 227 for e in crate.examples: 228 if e == example: 229 if out is not None: 230 raise ValueError( 231 f"example {example} appears more than once in cargo workspace" 232 ) 233 out = crate 234 if out is None: 235 raise ValueError(f"example {example} does not exist in cargo workspace") 236 return out 237 238 def transitive_path_dependencies( 239 self, crate: Crate, dev: bool = False 240 ) -> set[Crate]: 241 """Collects the transitive path dependencies of the requested crate. 242 243 Note that only _path_ dependencies are collected. Other types of 244 dependencies, like registry or Git dependencies, are not collected. 245 246 Args: 247 crate: The crate object from which to start the dependency crawl. 248 dev: Whether to consider dev dependencies in the root crate. 249 250 Returns: 251 crate_set: A set of all of the crates in this Cargo workspace upon 252 which the input crate depended upon, whether directly or 253 transitively. 254 255 Raises: 256 IndexError: The input crate did not exist. 257 """ 258 deps = set() 259 260 @cache 261 def visit(c: Crate) -> None: 262 deps.add(c) 263 for d in c.path_dependencies: 264 visit(self.crates[d]) 265 for d in c.path_build_dependencies: 266 visit(self.crates[d]) 267 268 visit(crate) 269 if dev: 270 for d in crate.path_dev_dependencies: 271 visit(self.crates[d]) 272 return deps 273 274 def precompute_crate_inputs(self) -> None: 275 """Pre-fetch all crate input files in a single batched git call. 276 277 This replaces ~118 individual pairs of git subprocess calls with 278 a single pair, then partitions the results by crate path in Python. 279 """ 280 from materialize import spawn 281 282 root = next(iter(self.all_crates.values())).root 283 # Use paths relative to root for git specs and partitioning, since 284 # git --relative outputs paths relative to cwd (root). Crate paths 285 # may be absolute when MZ_ROOT is an absolute path. 286 crate_rel_paths = sorted( 287 set(str(c.path.relative_to(root)) for c in self.all_crates.values()) 288 ) 289 290 specs = [] 291 for p in crate_rel_paths: 292 specs.append(f"{p}/**") 293 specs.append(f":(exclude){p}/mzcompose") 294 specs.append(f":(exclude){p}/mzcompose.py") 295 296 empty_tree = "4b825dc642cb6eb9a060e54bf8d69288fbee4904" 297 diff_files = spawn.capture( 298 ["git", "diff", "--name-only", "-z", "--relative", empty_tree, "--"] 299 + specs, 300 cwd=root, 301 ) 302 ls_files = spawn.capture( 303 ["git", "ls-files", "--others", "--exclude-standard", "-z", "--"] + specs, 304 cwd=root, 305 ) 306 all_files = set( 307 f for f in (diff_files + ls_files).split("\0") if f.strip() != "" 308 ) 309 310 # Partition files by crate path (longest match first for nested crates) 311 crate_file_map: dict[str, set[str]] = {p: set() for p in crate_rel_paths} 312 sorted_paths = sorted(crate_rel_paths, key=len, reverse=True) 313 for f in all_files: 314 for cp in sorted_paths: 315 if f.startswith(cp + "/"): 316 crate_file_map[cp].add(f) 317 break 318 319 # Inject cached results into each Crate object 320 for crate in self.all_crates.values(): 321 rel = str(crate.path.relative_to(root)) 322 crate._inputs_cache = crate_file_map.get(rel, set())
A Cargo workspace.
A workspace directory must contain a Cargo.toml file with a
workspace.members key.
Args: root: The path to the root of the workspace.
Attributes: root: The path to the root of the workspace. crates: A mapping from name to crate definition.
161 def __init__(self, root: Path): 162 self.root = root 163 with open(root / "Cargo.toml") as f: 164 config = toml.load(f) 165 166 workspace_config = config["workspace"] 167 168 self.crates: dict[str, Crate] = {} 169 for path in workspace_config["members"]: 170 crate = Crate(root, root / path) 171 self.crates[crate.name] = crate 172 self.exclude: dict[str, Crate] = {} 173 for path in workspace_config.get("exclude", []): 174 if path.endswith("*") and (root / path.rstrip("*")).exists(): 175 for item in (root / path.rstrip("*")).iterdir(): 176 if item.is_dir() and (item / "Cargo.toml").exists(): 177 crate = Crate(root, root / item) 178 self.exclude[crate.name] = crate 179 self.all_crates = self.crates | self.exclude 180 181 self.default_members: list[str] = workspace_config.get("default-members", []) 182 self.workspace_dependencies: dict[str, object] = workspace_config.get( 183 "dependencies", {} 184 ) 185 186 self.rust_version: str | None = None 187 try: 188 self.rust_version = workspace_config["package"].get("rust-version") 189 except KeyError: 190 pass
192 def crate_for_bin(self, bin: str) -> Crate: 193 """Find the crate containing the named binary. 194 195 Args: 196 bin: The name of the binary to find. 197 198 Raises: 199 ValueError: The named binary did not exist in exactly one crate in 200 the Cargo workspace. 201 """ 202 out = None 203 for crate in self.crates.values(): 204 for b in crate.bins: 205 if b == bin: 206 if out is not None: 207 raise ValueError( 208 f"bin {bin} appears more than once in cargo workspace" 209 ) 210 out = crate 211 if out is None: 212 raise ValueError(f"bin {bin} does not exist in cargo workspace") 213 return out
Find the crate containing the named binary.
Args: bin: The name of the binary to find.
Raises: ValueError: The named binary did not exist in exactly one crate in the Cargo workspace.
215 def crate_for_example(self, example: str) -> Crate: 216 """Find the crate containing the named example. 217 218 Args: 219 example: The name of the example to find. 220 221 Raises: 222 ValueError: The named example did not exist in exactly one crate in 223 the Cargo workspace. 224 """ 225 out = None 226 for crate in self.crates.values(): 227 for e in crate.examples: 228 if e == example: 229 if out is not None: 230 raise ValueError( 231 f"example {example} appears more than once in cargo workspace" 232 ) 233 out = crate 234 if out is None: 235 raise ValueError(f"example {example} does not exist in cargo workspace") 236 return out
Find the crate containing the named example.
Args: example: The name of the example to find.
Raises: ValueError: The named example did not exist in exactly one crate in the Cargo workspace.
238 def transitive_path_dependencies( 239 self, crate: Crate, dev: bool = False 240 ) -> set[Crate]: 241 """Collects the transitive path dependencies of the requested crate. 242 243 Note that only _path_ dependencies are collected. Other types of 244 dependencies, like registry or Git dependencies, are not collected. 245 246 Args: 247 crate: The crate object from which to start the dependency crawl. 248 dev: Whether to consider dev dependencies in the root crate. 249 250 Returns: 251 crate_set: A set of all of the crates in this Cargo workspace upon 252 which the input crate depended upon, whether directly or 253 transitively. 254 255 Raises: 256 IndexError: The input crate did not exist. 257 """ 258 deps = set() 259 260 @cache 261 def visit(c: Crate) -> None: 262 deps.add(c) 263 for d in c.path_dependencies: 264 visit(self.crates[d]) 265 for d in c.path_build_dependencies: 266 visit(self.crates[d]) 267 268 visit(crate) 269 if dev: 270 for d in crate.path_dev_dependencies: 271 visit(self.crates[d]) 272 return deps
Collects the transitive path dependencies of the requested crate.
Note that only _path_ dependencies are collected. Other types of dependencies, like registry or Git dependencies, are not collected.
Args: crate: The crate object from which to start the dependency crawl. dev: Whether to consider dev dependencies in the root crate.
Returns: crate_set: A set of all of the crates in this Cargo workspace upon which the input crate depended upon, whether directly or transitively.
Raises: IndexError: The input crate did not exist.
274 def precompute_crate_inputs(self) -> None: 275 """Pre-fetch all crate input files in a single batched git call. 276 277 This replaces ~118 individual pairs of git subprocess calls with 278 a single pair, then partitions the results by crate path in Python. 279 """ 280 from materialize import spawn 281 282 root = next(iter(self.all_crates.values())).root 283 # Use paths relative to root for git specs and partitioning, since 284 # git --relative outputs paths relative to cwd (root). Crate paths 285 # may be absolute when MZ_ROOT is an absolute path. 286 crate_rel_paths = sorted( 287 set(str(c.path.relative_to(root)) for c in self.all_crates.values()) 288 ) 289 290 specs = [] 291 for p in crate_rel_paths: 292 specs.append(f"{p}/**") 293 specs.append(f":(exclude){p}/mzcompose") 294 specs.append(f":(exclude){p}/mzcompose.py") 295 296 empty_tree = "4b825dc642cb6eb9a060e54bf8d69288fbee4904" 297 diff_files = spawn.capture( 298 ["git", "diff", "--name-only", "-z", "--relative", empty_tree, "--"] 299 + specs, 300 cwd=root, 301 ) 302 ls_files = spawn.capture( 303 ["git", "ls-files", "--others", "--exclude-standard", "-z", "--"] + specs, 304 cwd=root, 305 ) 306 all_files = set( 307 f for f in (diff_files + ls_files).split("\0") if f.strip() != "" 308 ) 309 310 # Partition files by crate path (longest match first for nested crates) 311 crate_file_map: dict[str, set[str]] = {p: set() for p in crate_rel_paths} 312 sorted_paths = sorted(crate_rel_paths, key=len, reverse=True) 313 for f in all_files: 314 for cp in sorted_paths: 315 if f.startswith(cp + "/"): 316 crate_file_map[cp].add(f) 317 break 318 319 # Inject cached results into each Crate object 320 for crate in self.all_crates.values(): 321 rel = str(crate.path.relative_to(root)) 322 crate._inputs_cache = crate_file_map.get(rel, set())
Pre-fetch all crate input files in a single batched git call.
This replaces ~118 individual pairs of git subprocess calls with a single pair, then partitions the results by crate path in Python.