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

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

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