misc.python.materialize.cargo

A pure Python metadata parser for Cargo, Rust's package manager.

See the Cargo documentation for details. Only the features that are presently necessary to support this repository are implemented.

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

A Cargo crate.

A crate directory must contain a Cargo.toml file with package.name and package.version keys.

Args: root: The path to the root of the workspace. path: The path to the crate directory.

Attributes: name: The name of the crate. version: The version of the crate. features: The features of the crate. path: The path to the crate. path_build_dependencies: The build dependencies which are declared using paths. path_dev_dependencies: The dev dependencies which are declared using paths. path_dependencies: The dependencies which are declared using paths. rust_version: The minimum Rust version declared in the crate, if any. bins: The names of all binaries in the crate. examples: The names of all examples in the crate.

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

Compute the files that can impact the compilation of this crate.

Note that the returned list may have false positives (i.e., include files that do not in fact impact the compilation of this crate), but it is not believed to have false negatives.

Returns: inputs: A list of input files, relative to the root of the Cargo workspace.

class Workspace:
127class Workspace:
128    """A Cargo workspace.
129
130    A workspace directory must contain a `Cargo.toml` file with a
131    `workspace.members` key.
132
133    Args:
134        root: The path to the root of the workspace.
135
136    Attributes:
137        crates: A mapping from name to crate definition.
138    """
139
140    def __init__(self, root: Path):
141        with open(root / "Cargo.toml") as f:
142            config = toml.load(f)
143
144        workspace_config = config["workspace"]
145
146        self.crates: dict[str, Crate] = {}
147        for path in workspace_config["members"]:
148            crate = Crate(root, root / path)
149            self.crates[crate.name] = crate
150        self.exclude: dict[str, Crate] = {}
151        for path in workspace_config.get("exclude", []):
152            if path.endswith("*") and (root / path.rstrip("*")).exists():
153                for item in (root / path.rstrip("*")).iterdir():
154                    if item.is_dir() and (item / "Cargo.toml").exists():
155                        crate = Crate(root, root / item)
156                        self.exclude[crate.name] = crate
157        self.all_crates = self.crates | self.exclude
158
159        self.default_members: list[str] = workspace_config.get("default-members", [])
160
161        self.rust_version: str | None = None
162        try:
163            self.rust_version = workspace_config["package"].get("rust-version")
164        except KeyError:
165            pass
166
167    def crate_for_bin(self, bin: str) -> Crate:
168        """Find the crate containing the named binary.
169
170        Args:
171            bin: The name of the binary to find.
172
173        Raises:
174            ValueError: The named binary did not exist in exactly one crate in
175                the Cargo workspace.
176        """
177        out = None
178        for crate in self.crates.values():
179            for b in crate.bins:
180                if b == bin:
181                    if out is not None:
182                        raise ValueError(
183                            f"bin {bin} appears more than once in cargo workspace"
184                        )
185                    out = crate
186        if out is None:
187            raise ValueError(f"bin {bin} does not exist in cargo workspace")
188        return out
189
190    def crate_for_example(self, example: str) -> Crate:
191        """Find the crate containing the named example.
192
193        Args:
194            example: The name of the example to find.
195
196        Raises:
197            ValueError: The named example did not exist in exactly one crate in
198                the Cargo workspace.
199        """
200        out = None
201        for crate in self.crates.values():
202            for e in crate.examples:
203                if e == example:
204                    if out is not None:
205                        raise ValueError(
206                            f"example {example} appears more than once in cargo workspace"
207                        )
208                    out = crate
209        if out is None:
210            raise ValueError(f"example {example} does not exist in cargo workspace")
211        return out
212
213    def transitive_path_dependencies(
214        self, crate: Crate, dev: bool = False
215    ) -> set[Crate]:
216        """Collects the transitive path dependencies of the requested crate.
217
218        Note that only _path_ dependencies are collected. Other types of
219        dependencies, like registry or Git dependencies, are not collected.
220
221        Args:
222            crate: The crate object from which to start the dependency crawl.
223            dev: Whether to consider dev dependencies in the root crate.
224
225        Returns:
226            crate_set: A set of all of the crates in this Cargo workspace upon
227                which the input crate depended upon, whether directly or
228                transitively.
229
230        Raises:
231            IndexError: The input crate did not exist.
232        """
233        deps = set()
234
235        def visit(c: Crate) -> None:
236            deps.add(c)
237            for d in c.path_dependencies:
238                visit(self.crates[d])
239            for d in c.path_build_dependencies:
240                visit(self.crates[d])
241
242        visit(crate)
243        if dev:
244            for d in crate.path_dev_dependencies:
245                visit(self.crates[d])
246        return deps

A Cargo workspace.

A workspace directory must contain a Cargo.toml file with a workspace.members key.

Args: root: The path to the root of the workspace.

Attributes: crates: A mapping from name to crate definition.

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

Find the crate containing the named binary.

Args: bin: The name of the binary to find.

Raises: ValueError: The named binary did not exist in exactly one crate in the Cargo workspace.

def crate_for_example(self, example: str) -> Crate:
190    def crate_for_example(self, example: str) -> Crate:
191        """Find the crate containing the named example.
192
193        Args:
194            example: The name of the example to find.
195
196        Raises:
197            ValueError: The named example did not exist in exactly one crate in
198                the Cargo workspace.
199        """
200        out = None
201        for crate in self.crates.values():
202            for e in crate.examples:
203                if e == example:
204                    if out is not None:
205                        raise ValueError(
206                            f"example {example} appears more than once in cargo workspace"
207                        )
208                    out = crate
209        if out is None:
210            raise ValueError(f"example {example} does not exist in cargo workspace")
211        return out

Find the crate containing the named example.

Args: example: The name of the example to find.

Raises: ValueError: The named example did not exist in exactly one crate in the Cargo workspace.

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

Collects the transitive path dependencies of the requested crate.

Note that only _path_ dependencies are collected. Other types of dependencies, like registry or Git dependencies, are not collected.

Args: crate: The crate object from which to start the dependency crawl. dev: Whether to consider dev dependencies in the root crate.

Returns: crate_set: A set of all of the crates in this Cargo workspace upon which the input crate depended upon, whether directly or transitively.

Raises: IndexError: The input crate did not exist.