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 pathlib import Path 19 20import toml 21 22from materialize import git 23 24 25class Crate: 26 """A Cargo crate. 27 28 A crate directory must contain a `Cargo.toml` file with `package.name` and 29 `package.version` keys. 30 31 Args: 32 root: The path to the root of the workspace. 33 path: The path to the crate directory. 34 35 Attributes: 36 name: The name of the crate. 37 version: The version of the crate. 38 features: The features of the crate. 39 path: The path to the crate. 40 path_build_dependencies: The build dependencies which are declared 41 using paths. 42 path_dev_dependencies: The dev dependencies which are declared using 43 paths. 44 path_dependencies: The dependencies which are declared using paths. 45 rust_version: The minimum Rust version declared in the crate, if any. 46 bins: The names of all binaries in the crate. 47 examples: The names of all examples in the crate. 48 """ 49 50 def __init__(self, root: Path, path: Path): 51 self.root = root 52 with open(path / "Cargo.toml") as f: 53 config = toml.load(f) 54 self.name = config["package"]["name"] 55 self.version_string = config["package"]["version"] 56 self.features = config.get("features", {}) 57 self.path = path 58 self.path_build_dependencies: set[str] = set() 59 self.path_dev_dependencies: set[str] = set() 60 self.path_dependencies: set[str] = set() 61 for dep_type, field in [ 62 ("build-dependencies", self.path_build_dependencies), 63 ("dev-dependencies", self.path_dev_dependencies), 64 ("dependencies", self.path_dependencies), 65 ]: 66 if dep_type in config: 67 field.update( 68 c.get("package", name) 69 for name, c in config[dep_type].items() 70 if "path" in c 71 ) 72 self.rust_version: str | None = None 73 try: 74 self.rust_version = str(config["package"]["rust-version"]) 75 except KeyError: 76 pass 77 self.bins = [] 78 if "bin" in config: 79 for bin in config["bin"]: 80 self.bins.append(bin["name"]) 81 if config["package"].get("autobins", True): 82 if (path / "src" / "main.rs").exists(): 83 self.bins.append(self.name) 84 for p in (path / "src" / "bin").glob("*.rs"): 85 self.bins.append(p.stem) 86 for p in (path / "src" / "bin").glob("*/main.rs"): 87 self.bins.append(p.parent.stem) 88 self.examples = [] 89 if "example" in config: 90 for example in config["example"]: 91 self.examples.append(example["name"]) 92 if config["package"].get("autoexamples", True): 93 for p in (path / "examples").glob("*.rs"): 94 self.examples.append(p.stem) 95 for p in (path / "examples").glob("*/main.rs"): 96 self.examples.append(p.parent.stem) 97 98 def inputs(self) -> set[str]: 99 """Compute the files that can impact the compilation of this crate. 100 101 Note that the returned list may have false positives (i.e., include 102 files that do not in fact impact the compilation of this crate), but it 103 is not believed to have false negatives. 104 105 Returns: 106 inputs: A list of input files, relative to the root of the 107 Cargo workspace. 108 """ 109 # NOTE(benesch): it would be nice to have fine-grained tracking of only 110 # exactly the files that go into a Rust crate, but doing this properly 111 # requires parsing Rust code, and we don't want to force a dependency on 112 # a Rust toolchain for users running demos. Instead, we assume that all† 113 # files in a crate's directory are inputs to that crate. 114 # 115 # † As a development convenience, we omit mzcompose configuration files 116 # within a crate. This is technically incorrect if someone writes 117 # `include!("mzcompose.py")`, but that seems like a crazy thing to do. 118 return git.expand_globs( 119 self.root, 120 f"{self.path}/**", 121 f":(exclude){self.path}/mzcompose", 122 f":(exclude){self.path}/mzcompose.py", 123 ) 124 125 126class Workspace: 127 """A Cargo workspace. 128 129 A workspace directory must contain a `Cargo.toml` file with a 130 `workspace.members` key. 131 132 Args: 133 root: The path to the root of the workspace. 134 135 Attributes: 136 crates: A mapping from name to crate definition. 137 """ 138 139 def __init__(self, root: Path): 140 with open(root / "Cargo.toml") as f: 141 config = toml.load(f) 142 143 workspace_config = config["workspace"] 144 145 self.crates: dict[str, Crate] = {} 146 for path in workspace_config["members"]: 147 crate = Crate(root, root / path) 148 self.crates[crate.name] = crate 149 self.exclude: dict[str, Crate] = {} 150 for path in workspace_config.get("exclude", []): 151 if path.endswith("*") and (root / path.rstrip("*")).exists(): 152 for item in (root / path.rstrip("*")).iterdir(): 153 if item.is_dir() and (item / "Cargo.toml").exists(): 154 crate = Crate(root, root / item) 155 self.exclude[crate.name] = crate 156 self.all_crates = self.crates | self.exclude 157 158 self.default_members: list[str] = workspace_config.get("default-members", []) 159 160 self.rust_version: str | None = None 161 try: 162 self.rust_version = workspace_config["package"].get("rust-version") 163 except KeyError: 164 pass 165 166 def crate_for_bin(self, bin: str) -> Crate: 167 """Find the crate containing the named binary. 168 169 Args: 170 bin: The name of the binary to find. 171 172 Raises: 173 ValueError: The named binary did not exist in exactly one crate in 174 the Cargo workspace. 175 """ 176 out = None 177 for crate in self.crates.values(): 178 for b in crate.bins: 179 if b == bin: 180 if out is not None: 181 raise ValueError( 182 f"bin {bin} appears more than once in cargo workspace" 183 ) 184 out = crate 185 if out is None: 186 raise ValueError(f"bin {bin} does not exist in cargo workspace") 187 return out 188 189 def crate_for_example(self, example: str) -> Crate: 190 """Find the crate containing the named example. 191 192 Args: 193 example: The name of the example to find. 194 195 Raises: 196 ValueError: The named example did not exist in exactly one crate in 197 the Cargo workspace. 198 """ 199 out = None 200 for crate in self.crates.values(): 201 for e in crate.examples: 202 if e == example: 203 if out is not None: 204 raise ValueError( 205 f"example {example} appears more than once in cargo workspace" 206 ) 207 out = crate 208 if out is None: 209 raise ValueError(f"example {example} does not exist in cargo workspace") 210 return out 211 212 def transitive_path_dependencies( 213 self, crate: Crate, dev: bool = False 214 ) -> set[Crate]: 215 """Collects the transitive path dependencies of the requested crate. 216 217 Note that only _path_ dependencies are collected. Other types of 218 dependencies, like registry or Git dependencies, are not collected. 219 220 Args: 221 crate: The crate object from which to start the dependency crawl. 222 dev: Whether to consider dev dependencies in the root crate. 223 224 Returns: 225 crate_set: A set of all of the crates in this Cargo workspace upon 226 which the input crate depended upon, whether directly or 227 transitively. 228 229 Raises: 230 IndexError: The input crate did not exist. 231 """ 232 deps = set() 233 234 def visit(c: Crate) -> None: 235 deps.add(c) 236 for d in c.path_dependencies: 237 visit(self.crates[d]) 238 for d in c.path_build_dependencies: 239 visit(self.crates[d]) 240 241 visit(crate) 242 if dev: 243 for d in crate.path_dev_dependencies: 244 visit(self.crates[d]) 245 return deps
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 rust_version: The minimum Rust version declared in the crate, if any. 47 bins: The names of all binaries in the crate. 48 examples: The names of all examples in the crate. 49 """ 50 51 def __init__(self, root: Path, path: Path): 52 self.root = root 53 with open(path / "Cargo.toml") as f: 54 config = toml.load(f) 55 self.name = config["package"]["name"] 56 self.version_string = config["package"]["version"] 57 self.features = config.get("features", {}) 58 self.path = path 59 self.path_build_dependencies: set[str] = set() 60 self.path_dev_dependencies: set[str] = set() 61 self.path_dependencies: set[str] = set() 62 for dep_type, field in [ 63 ("build-dependencies", self.path_build_dependencies), 64 ("dev-dependencies", self.path_dev_dependencies), 65 ("dependencies", self.path_dependencies), 66 ]: 67 if dep_type in config: 68 field.update( 69 c.get("package", name) 70 for name, c in config[dep_type].items() 71 if "path" in c 72 ) 73 self.rust_version: str | None = None 74 try: 75 self.rust_version = str(config["package"]["rust-version"]) 76 except KeyError: 77 pass 78 self.bins = [] 79 if "bin" in config: 80 for bin in config["bin"]: 81 self.bins.append(bin["name"]) 82 if config["package"].get("autobins", True): 83 if (path / "src" / "main.rs").exists(): 84 self.bins.append(self.name) 85 for p in (path / "src" / "bin").glob("*.rs"): 86 self.bins.append(p.stem) 87 for p in (path / "src" / "bin").glob("*/main.rs"): 88 self.bins.append(p.parent.stem) 89 self.examples = [] 90 if "example" in config: 91 for example in config["example"]: 92 self.examples.append(example["name"]) 93 if config["package"].get("autoexamples", True): 94 for p in (path / "examples").glob("*.rs"): 95 self.examples.append(p.stem) 96 for p in (path / "examples").glob("*/main.rs"): 97 self.examples.append(p.parent.stem) 98 99 def inputs(self) -> set[str]: 100 """Compute the files that can impact the compilation of this crate. 101 102 Note that the returned list may have false positives (i.e., include 103 files that do not in fact impact the compilation of this crate), but it 104 is not believed to have false negatives. 105 106 Returns: 107 inputs: A list of input files, relative to the root of the 108 Cargo workspace. 109 """ 110 # NOTE(benesch): it would be nice to have fine-grained tracking of only 111 # exactly the files that go into a Rust crate, but doing this properly 112 # requires parsing Rust code, and we don't want to force a dependency on 113 # a Rust toolchain for users running demos. Instead, we assume that all† 114 # files in a crate's directory are inputs to that crate. 115 # 116 # † As a development convenience, we omit mzcompose configuration files 117 # within a crate. This is technically incorrect if someone writes 118 # `include!("mzcompose.py")`, but that seems like a crazy thing to do. 119 return git.expand_globs( 120 self.root, 121 f"{self.path}/**", 122 f":(exclude){self.path}/mzcompose", 123 f":(exclude){self.path}/mzcompose.py", 124 )
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. 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.
51 def __init__(self, root: Path, path: Path): 52 self.root = root 53 with open(path / "Cargo.toml") as f: 54 config = toml.load(f) 55 self.name = config["package"]["name"] 56 self.version_string = config["package"]["version"] 57 self.features = config.get("features", {}) 58 self.path = path 59 self.path_build_dependencies: set[str] = set() 60 self.path_dev_dependencies: set[str] = set() 61 self.path_dependencies: set[str] = set() 62 for dep_type, field in [ 63 ("build-dependencies", self.path_build_dependencies), 64 ("dev-dependencies", self.path_dev_dependencies), 65 ("dependencies", self.path_dependencies), 66 ]: 67 if dep_type in config: 68 field.update( 69 c.get("package", name) 70 for name, c in config[dep_type].items() 71 if "path" in c 72 ) 73 self.rust_version: str | None = None 74 try: 75 self.rust_version = str(config["package"]["rust-version"]) 76 except KeyError: 77 pass 78 self.bins = [] 79 if "bin" in config: 80 for bin in config["bin"]: 81 self.bins.append(bin["name"]) 82 if config["package"].get("autobins", True): 83 if (path / "src" / "main.rs").exists(): 84 self.bins.append(self.name) 85 for p in (path / "src" / "bin").glob("*.rs"): 86 self.bins.append(p.stem) 87 for p in (path / "src" / "bin").glob("*/main.rs"): 88 self.bins.append(p.parent.stem) 89 self.examples = [] 90 if "example" in config: 91 for example in config["example"]: 92 self.examples.append(example["name"]) 93 if config["package"].get("autoexamples", True): 94 for p in (path / "examples").glob("*.rs"): 95 self.examples.append(p.stem) 96 for p in (path / "examples").glob("*/main.rs"): 97 self.examples.append(p.parent.stem)
99 def inputs(self) -> set[str]: 100 """Compute the files that can impact the compilation of this crate. 101 102 Note that the returned list may have false positives (i.e., include 103 files that do not in fact impact the compilation of this crate), but it 104 is not believed to have false negatives. 105 106 Returns: 107 inputs: A list of input files, relative to the root of the 108 Cargo workspace. 109 """ 110 # NOTE(benesch): it would be nice to have fine-grained tracking of only 111 # exactly the files that go into a Rust crate, but doing this properly 112 # requires parsing Rust code, and we don't want to force a dependency on 113 # a Rust toolchain for users running demos. Instead, we assume that all† 114 # files in a crate's directory are inputs to that crate. 115 # 116 # † As a development convenience, we omit mzcompose configuration files 117 # within a crate. This is technically incorrect if someone writes 118 # `include!("mzcompose.py")`, but that seems like a crazy thing to do. 119 return git.expand_globs( 120 self.root, 121 f"{self.path}/**", 122 f":(exclude){self.path}/mzcompose", 123 f":(exclude){self.path}/mzcompose.py", 124 )
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.
127class Workspace: 128 """A Cargo workspace. 129 130 A workspace directory must contain a `Cargo.toml` file with a 131 `workspace.members` key. 132 133 Args: 134 root: The path to the root of the workspace. 135 136 Attributes: 137 crates: A mapping from name to crate definition. 138 """ 139 140 def __init__(self, root: Path): 141 with open(root / "Cargo.toml") as f: 142 config = toml.load(f) 143 144 workspace_config = config["workspace"] 145 146 self.crates: dict[str, Crate] = {} 147 for path in workspace_config["members"]: 148 crate = Crate(root, root / path) 149 self.crates[crate.name] = crate 150 self.exclude: dict[str, Crate] = {} 151 for path in workspace_config.get("exclude", []): 152 if path.endswith("*") and (root / path.rstrip("*")).exists(): 153 for item in (root / path.rstrip("*")).iterdir(): 154 if item.is_dir() and (item / "Cargo.toml").exists(): 155 crate = Crate(root, root / item) 156 self.exclude[crate.name] = crate 157 self.all_crates = self.crates | self.exclude 158 159 self.default_members: list[str] = workspace_config.get("default-members", []) 160 161 self.rust_version: str | None = None 162 try: 163 self.rust_version = workspace_config["package"].get("rust-version") 164 except KeyError: 165 pass 166 167 def crate_for_bin(self, bin: str) -> Crate: 168 """Find the crate containing the named binary. 169 170 Args: 171 bin: The name of the binary to find. 172 173 Raises: 174 ValueError: The named binary did not exist in exactly one crate in 175 the Cargo workspace. 176 """ 177 out = None 178 for crate in self.crates.values(): 179 for b in crate.bins: 180 if b == bin: 181 if out is not None: 182 raise ValueError( 183 f"bin {bin} appears more than once in cargo workspace" 184 ) 185 out = crate 186 if out is None: 187 raise ValueError(f"bin {bin} does not exist in cargo workspace") 188 return out 189 190 def crate_for_example(self, example: str) -> Crate: 191 """Find the crate containing the named example. 192 193 Args: 194 example: The name of the example to find. 195 196 Raises: 197 ValueError: The named example did not exist in exactly one crate in 198 the Cargo workspace. 199 """ 200 out = None 201 for crate in self.crates.values(): 202 for e in crate.examples: 203 if e == example: 204 if out is not None: 205 raise ValueError( 206 f"example {example} appears more than once in cargo workspace" 207 ) 208 out = crate 209 if out is None: 210 raise ValueError(f"example {example} does not exist in cargo workspace") 211 return out 212 213 def transitive_path_dependencies( 214 self, crate: Crate, dev: bool = False 215 ) -> set[Crate]: 216 """Collects the transitive path dependencies of the requested crate. 217 218 Note that only _path_ dependencies are collected. Other types of 219 dependencies, like registry or Git dependencies, are not collected. 220 221 Args: 222 crate: The crate object from which to start the dependency crawl. 223 dev: Whether to consider dev dependencies in the root crate. 224 225 Returns: 226 crate_set: A set of all of the crates in this Cargo workspace upon 227 which the input crate depended upon, whether directly or 228 transitively. 229 230 Raises: 231 IndexError: The input crate did not exist. 232 """ 233 deps = set() 234 235 def visit(c: Crate) -> None: 236 deps.add(c) 237 for d in c.path_dependencies: 238 visit(self.crates[d]) 239 for d in c.path_build_dependencies: 240 visit(self.crates[d]) 241 242 visit(crate) 243 if dev: 244 for d in crate.path_dev_dependencies: 245 visit(self.crates[d]) 246 return deps
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: crates: A mapping from name to crate definition.
140 def __init__(self, root: Path): 141 with open(root / "Cargo.toml") as f: 142 config = toml.load(f) 143 144 workspace_config = config["workspace"] 145 146 self.crates: dict[str, Crate] = {} 147 for path in workspace_config["members"]: 148 crate = Crate(root, root / path) 149 self.crates[crate.name] = crate 150 self.exclude: dict[str, Crate] = {} 151 for path in workspace_config.get("exclude", []): 152 if path.endswith("*") and (root / path.rstrip("*")).exists(): 153 for item in (root / path.rstrip("*")).iterdir(): 154 if item.is_dir() and (item / "Cargo.toml").exists(): 155 crate = Crate(root, root / item) 156 self.exclude[crate.name] = crate 157 self.all_crates = self.crates | self.exclude 158 159 self.default_members: list[str] = workspace_config.get("default-members", []) 160 161 self.rust_version: str | None = None 162 try: 163 self.rust_version = workspace_config["package"].get("rust-version") 164 except KeyError: 165 pass
167 def crate_for_bin(self, bin: str) -> Crate: 168 """Find the crate containing the named binary. 169 170 Args: 171 bin: The name of the binary to find. 172 173 Raises: 174 ValueError: The named binary did not exist in exactly one crate in 175 the Cargo workspace. 176 """ 177 out = None 178 for crate in self.crates.values(): 179 for b in crate.bins: 180 if b == bin: 181 if out is not None: 182 raise ValueError( 183 f"bin {bin} appears more than once in cargo workspace" 184 ) 185 out = crate 186 if out is None: 187 raise ValueError(f"bin {bin} does not exist in cargo workspace") 188 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.
190 def crate_for_example(self, example: str) -> Crate: 191 """Find the crate containing the named example. 192 193 Args: 194 example: The name of the example to find. 195 196 Raises: 197 ValueError: The named example did not exist in exactly one crate in 198 the Cargo workspace. 199 """ 200 out = None 201 for crate in self.crates.values(): 202 for e in crate.examples: 203 if e == example: 204 if out is not None: 205 raise ValueError( 206 f"example {example} appears more than once in cargo workspace" 207 ) 208 out = crate 209 if out is None: 210 raise ValueError(f"example {example} does not exist in cargo workspace") 211 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.
213 def transitive_path_dependencies( 214 self, crate: Crate, dev: bool = False 215 ) -> set[Crate]: 216 """Collects the transitive path dependencies of the requested crate. 217 218 Note that only _path_ dependencies are collected. Other types of 219 dependencies, like registry or Git dependencies, are not collected. 220 221 Args: 222 crate: The crate object from which to start the dependency crawl. 223 dev: Whether to consider dev dependencies in the root crate. 224 225 Returns: 226 crate_set: A set of all of the crates in this Cargo workspace upon 227 which the input crate depended upon, whether directly or 228 transitively. 229 230 Raises: 231 IndexError: The input crate did not exist. 232 """ 233 deps = set() 234 235 def visit(c: Crate) -> None: 236 deps.add(c) 237 for d in c.path_dependencies: 238 visit(self.crates[d]) 239 for d in c.path_build_dependencies: 240 visit(self.crates[d]) 241 242 visit(crate) 243 if dev: 244 for d in crate.path_dev_dependencies: 245 visit(self.crates[d]) 246 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.