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

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.

Crate(root: pathlib.Path, path: pathlib.Path)
 54    def __init__(self, root: Path, path: Path):
 55        self.root = root
 56        self._inputs_cache = None
 57        with open(path / "Cargo.toml") as f:
 58            config = toml.load(f)
 59        self.name = config["package"]["name"]
 60        self.version_string = config["package"]["version"]
 61        self.features = config.get("features", {})
 62        self.path = path
 63        self.path_build_dependencies: set[str] = set()
 64        self.path_dev_dependencies: set[str] = set()
 65        self.path_dependencies: set[str] = set()
 66        for dep_type, field in [
 67            ("build-dependencies", self.path_build_dependencies),
 68            ("dev-dependencies", self.path_dev_dependencies),
 69            ("dependencies", self.path_dependencies),
 70        ]:
 71            if dep_type in config:
 72                field.update(
 73                    c.get("package", name)
 74                    for name, c in config[dep_type].items()
 75                    if "path" in c
 76                )
 77        self.rust_version: str | None = None
 78        try:
 79            self.rust_version = str(config["package"]["rust-version"])
 80        except KeyError:
 81            pass
 82        self.bins = []
 83        if "bin" in config:
 84            for bin in config["bin"]:
 85                self.bins.append(bin["name"])
 86        if config["package"].get("autobins", True):
 87            if (path / "src" / "main.rs").exists():
 88                self.bins.append(self.name)
 89            for p in (path / "src" / "bin").glob("*.rs"):
 90                self.bins.append(p.stem)
 91            for p in (path / "src" / "bin").glob("*/main.rs"):
 92                self.bins.append(p.parent.stem)
 93        self.examples = []
 94        if "example" in config:
 95            for example in config["example"]:
 96                self.examples.append(example["name"])
 97        if config["package"].get("autoexamples", True):
 98            for p in (path / "examples").glob("*.rs"):
 99                self.examples.append(p.stem)
100            for p in (path / "examples").glob("*/main.rs"):
101                self.examples.append(p.parent.stem)
root
name
version_string
features
path
path_build_dependencies: set[str]
path_dev_dependencies: set[str]
path_dependencies: set[str]
rust_version: str | None
bins
examples
def inputs(self) -> set[str]:
103    def inputs(self) -> set[str]:
104        """Compute the files that can impact the compilation of this crate.
105
106        Note that the returned list may have false positives (i.e., include
107        files that do not in fact impact the compilation of this crate), but it
108        is not believed to have false negatives.
109
110        Returns:
111            inputs: A list of input files, relative to the root of the
112                Cargo workspace.
113        """
114        # NOTE(benesch): it would be nice to have fine-grained tracking of only
115        # exactly the files that go into a Rust crate, but doing this properly
116        # requires parsing Rust code, and we don't want to force a dependency on
117        # a Rust toolchain for users running demos. Instead, we assume that all†
118        # files in a crate's directory are inputs to that crate.
119        #
120        # † As a development convenience, we omit mzcompose configuration files
121        # within a crate. This is technically incorrect if someone writes
122        # `include!("mzcompose.py")`, but that seems like a crazy thing to do.
123        if self._inputs_cache is not None:
124            return self._inputs_cache
125        return git.expand_globs(
126            self.root,
127            f"{self.path}/**",
128            f":(exclude){self.path}/mzcompose",
129            f":(exclude){self.path}/mzcompose.py",
130        )

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.

class Workspace:
133class Workspace:
134    """A Cargo workspace.
135
136    A workspace directory must contain a `Cargo.toml` file with a
137    `workspace.members` key.
138
139    Args:
140        root: The path to the root of the workspace.
141
142    Attributes:
143        crates: A mapping from name to crate definition.
144    """
145
146    def __init__(self, root: Path):
147        with open(root / "Cargo.toml") as f:
148            config = toml.load(f)
149
150        workspace_config = config["workspace"]
151
152        self.crates: dict[str, Crate] = {}
153        for path in workspace_config["members"]:
154            crate = Crate(root, root / path)
155            self.crates[crate.name] = crate
156        self.exclude: dict[str, Crate] = {}
157        for path in workspace_config.get("exclude", []):
158            if path.endswith("*") and (root / path.rstrip("*")).exists():
159                for item in (root / path.rstrip("*")).iterdir():
160                    if item.is_dir() and (item / "Cargo.toml").exists():
161                        crate = Crate(root, root / item)
162                        self.exclude[crate.name] = crate
163        self.all_crates = self.crates | self.exclude
164
165        self.default_members: list[str] = workspace_config.get("default-members", [])
166
167        self.rust_version: str | None = None
168        try:
169            self.rust_version = workspace_config["package"].get("rust-version")
170        except KeyError:
171            pass
172
173    def crate_for_bin(self, bin: str) -> Crate:
174        """Find the crate containing the named binary.
175
176        Args:
177            bin: The name of the binary to find.
178
179        Raises:
180            ValueError: The named binary did not exist in exactly one crate in
181                the Cargo workspace.
182        """
183        out = None
184        for crate in self.crates.values():
185            for b in crate.bins:
186                if b == bin:
187                    if out is not None:
188                        raise ValueError(
189                            f"bin {bin} appears more than once in cargo workspace"
190                        )
191                    out = crate
192        if out is None:
193            raise ValueError(f"bin {bin} does not exist in cargo workspace")
194        return out
195
196    def crate_for_example(self, example: str) -> Crate:
197        """Find the crate containing the named example.
198
199        Args:
200            example: The name of the example to find.
201
202        Raises:
203            ValueError: The named example did not exist in exactly one crate in
204                the Cargo workspace.
205        """
206        out = None
207        for crate in self.crates.values():
208            for e in crate.examples:
209                if e == example:
210                    if out is not None:
211                        raise ValueError(
212                            f"example {example} appears more than once in cargo workspace"
213                        )
214                    out = crate
215        if out is None:
216            raise ValueError(f"example {example} does not exist in cargo workspace")
217        return out
218
219    def transitive_path_dependencies(
220        self, crate: Crate, dev: bool = False
221    ) -> set[Crate]:
222        """Collects the transitive path dependencies of the requested crate.
223
224        Note that only _path_ dependencies are collected. Other types of
225        dependencies, like registry or Git dependencies, are not collected.
226
227        Args:
228            crate: The crate object from which to start the dependency crawl.
229            dev: Whether to consider dev dependencies in the root crate.
230
231        Returns:
232            crate_set: A set of all of the crates in this Cargo workspace upon
233                which the input crate depended upon, whether directly or
234                transitively.
235
236        Raises:
237            IndexError: The input crate did not exist.
238        """
239        deps = set()
240
241        @cache
242        def visit(c: Crate) -> None:
243            deps.add(c)
244            for d in c.path_dependencies:
245                visit(self.crates[d])
246            for d in c.path_build_dependencies:
247                visit(self.crates[d])
248
249        visit(crate)
250        if dev:
251            for d in crate.path_dev_dependencies:
252                visit(self.crates[d])
253        return deps
254
255    def precompute_crate_inputs(self) -> None:
256        """Pre-fetch all crate input files in a single batched git call.
257
258        This replaces ~118 individual pairs of git subprocess calls with
259        a single pair, then partitions the results by crate path in Python.
260        """
261        from materialize import spawn
262
263        root = next(iter(self.all_crates.values())).root
264        # Use paths relative to root for git specs and partitioning, since
265        # git --relative outputs paths relative to cwd (root). Crate paths
266        # may be absolute when MZ_ROOT is an absolute path.
267        crate_rel_paths = sorted(
268            set(str(c.path.relative_to(root)) for c in self.all_crates.values())
269        )
270
271        specs = []
272        for p in crate_rel_paths:
273            specs.append(f"{p}/**")
274            specs.append(f":(exclude){p}/mzcompose")
275            specs.append(f":(exclude){p}/mzcompose.py")
276
277        empty_tree = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
278        diff_files = spawn.capture(
279            ["git", "diff", "--name-only", "-z", "--relative", empty_tree, "--"]
280            + specs,
281            cwd=root,
282        )
283        ls_files = spawn.capture(
284            ["git", "ls-files", "--others", "--exclude-standard", "-z", "--"] + specs,
285            cwd=root,
286        )
287        all_files = set(
288            f for f in (diff_files + ls_files).split("\0") if f.strip() != ""
289        )
290
291        # Partition files by crate path (longest match first for nested crates)
292        crate_file_map: dict[str, set[str]] = {p: set() for p in crate_rel_paths}
293        sorted_paths = sorted(crate_rel_paths, key=len, reverse=True)
294        for f in all_files:
295            for cp in sorted_paths:
296                if f.startswith(cp + "/"):
297                    crate_file_map[cp].add(f)
298                    break
299
300        # Inject cached results into each Crate object
301        for crate in self.all_crates.values():
302            rel = str(crate.path.relative_to(root))
303            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: crates: A mapping from name to crate definition.

Workspace(root: pathlib.Path)
146    def __init__(self, root: Path):
147        with open(root / "Cargo.toml") as f:
148            config = toml.load(f)
149
150        workspace_config = config["workspace"]
151
152        self.crates: dict[str, Crate] = {}
153        for path in workspace_config["members"]:
154            crate = Crate(root, root / path)
155            self.crates[crate.name] = crate
156        self.exclude: dict[str, Crate] = {}
157        for path in workspace_config.get("exclude", []):
158            if path.endswith("*") and (root / path.rstrip("*")).exists():
159                for item in (root / path.rstrip("*")).iterdir():
160                    if item.is_dir() and (item / "Cargo.toml").exists():
161                        crate = Crate(root, root / item)
162                        self.exclude[crate.name] = crate
163        self.all_crates = self.crates | self.exclude
164
165        self.default_members: list[str] = workspace_config.get("default-members", [])
166
167        self.rust_version: str | None = None
168        try:
169            self.rust_version = workspace_config["package"].get("rust-version")
170        except KeyError:
171            pass
crates: dict[str, Crate]
exclude: dict[str, Crate]
all_crates
default_members: list[str]
rust_version: str | None
def crate_for_bin(self, bin: str) -> Crate:
173    def crate_for_bin(self, bin: str) -> Crate:
174        """Find the crate containing the named binary.
175
176        Args:
177            bin: The name of the binary to find.
178
179        Raises:
180            ValueError: The named binary did not exist in exactly one crate in
181                the Cargo workspace.
182        """
183        out = None
184        for crate in self.crates.values():
185            for b in crate.bins:
186                if b == bin:
187                    if out is not None:
188                        raise ValueError(
189                            f"bin {bin} appears more than once in cargo workspace"
190                        )
191                    out = crate
192        if out is None:
193            raise ValueError(f"bin {bin} does not exist in cargo workspace")
194        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.

def crate_for_example(self, example: str) -> Crate:
196    def crate_for_example(self, example: str) -> Crate:
197        """Find the crate containing the named example.
198
199        Args:
200            example: The name of the example to find.
201
202        Raises:
203            ValueError: The named example did not exist in exactly one crate in
204                the Cargo workspace.
205        """
206        out = None
207        for crate in self.crates.values():
208            for e in crate.examples:
209                if e == example:
210                    if out is not None:
211                        raise ValueError(
212                            f"example {example} appears more than once in cargo workspace"
213                        )
214                    out = crate
215        if out is None:
216            raise ValueError(f"example {example} does not exist in cargo workspace")
217        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.

def transitive_path_dependencies( self, crate: Crate, dev: bool = False) -> set[Crate]:
219    def transitive_path_dependencies(
220        self, crate: Crate, dev: bool = False
221    ) -> set[Crate]:
222        """Collects the transitive path dependencies of the requested crate.
223
224        Note that only _path_ dependencies are collected. Other types of
225        dependencies, like registry or Git dependencies, are not collected.
226
227        Args:
228            crate: The crate object from which to start the dependency crawl.
229            dev: Whether to consider dev dependencies in the root crate.
230
231        Returns:
232            crate_set: A set of all of the crates in this Cargo workspace upon
233                which the input crate depended upon, whether directly or
234                transitively.
235
236        Raises:
237            IndexError: The input crate did not exist.
238        """
239        deps = set()
240
241        @cache
242        def visit(c: Crate) -> None:
243            deps.add(c)
244            for d in c.path_dependencies:
245                visit(self.crates[d])
246            for d in c.path_build_dependencies:
247                visit(self.crates[d])
248
249        visit(crate)
250        if dev:
251            for d in crate.path_dev_dependencies:
252                visit(self.crates[d])
253        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.

def precompute_crate_inputs(self) -> None:
255    def precompute_crate_inputs(self) -> None:
256        """Pre-fetch all crate input files in a single batched git call.
257
258        This replaces ~118 individual pairs of git subprocess calls with
259        a single pair, then partitions the results by crate path in Python.
260        """
261        from materialize import spawn
262
263        root = next(iter(self.all_crates.values())).root
264        # Use paths relative to root for git specs and partitioning, since
265        # git --relative outputs paths relative to cwd (root). Crate paths
266        # may be absolute when MZ_ROOT is an absolute path.
267        crate_rel_paths = sorted(
268            set(str(c.path.relative_to(root)) for c in self.all_crates.values())
269        )
270
271        specs = []
272        for p in crate_rel_paths:
273            specs.append(f"{p}/**")
274            specs.append(f":(exclude){p}/mzcompose")
275            specs.append(f":(exclude){p}/mzcompose.py")
276
277        empty_tree = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
278        diff_files = spawn.capture(
279            ["git", "diff", "--name-only", "-z", "--relative", empty_tree, "--"]
280            + specs,
281            cwd=root,
282        )
283        ls_files = spawn.capture(
284            ["git", "ls-files", "--others", "--exclude-standard", "-z", "--"] + specs,
285            cwd=root,
286        )
287        all_files = set(
288            f for f in (diff_files + ls_files).split("\0") if f.strip() != ""
289        )
290
291        # Partition files by crate path (longest match first for nested crates)
292        crate_file_map: dict[str, set[str]] = {p: set() for p in crate_rel_paths}
293        sorted_paths = sorted(crate_rel_paths, key=len, reverse=True)
294        for f in all_files:
295            for cp in sorted_paths:
296                if f.startswith(cp + "/"):
297                    crate_file_map[cp].add(f)
298                    break
299
300        # Inject cached results into each Crate object
301        for crate in self.all_crates.values():
302            rel = str(crate.path.relative_to(root))
303            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.