cargo.py 8.9 KB

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