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

Crate(root: pathlib._local.Path, path: pathlib._local.Path)
 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)
root
name
version_string
features
path
path_build_dependencies: set[str]
path_dev_dependencies: set[str]
path_dependencies: set[str]
non_workspace_deps: dict[str, list[str]]
rust_version: str | None
bins
examples
def inputs(self) -> set[str]:
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.

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

Workspace(root: pathlib._local.Path)
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
root
crates: dict[str, Crate]
exclude: dict[str, Crate]
all_crates
default_members: list[str]
workspace_dependencies: dict[str, object]
rust_version: str | None
def crate_for_bin(self, bin: str) -> Crate:
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.

def crate_for_example(self, example: str) -> Crate:
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.

def transitive_path_dependencies( self, crate: Crate, dev: bool = False) -> set[Crate]:
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.

def precompute_crate_inputs(self) -> None:
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.