1use std::{
2 collections::{btree_map, BTreeMap, BTreeSet},
3 ffi::OsStr,
4 path::{Path, PathBuf},
5 process::Command,
6 str::FromStr,
7};
8
9use anyhow::Context;
10use cargo_metadata::{
11 Metadata as CargoMetadata, Package as MetadataPackage, PackageId as MetadataId,
12 PackageName as MetadataPackageName,
13};
14
15#[derive(Debug)]
20pub struct Package {
21 name: MetadataPackageName,
22 src_paths: Vec<PathBuf>,
23}
24
25impl Package {
26 pub fn name(&self) -> &str {
27 self.name.as_str()
28 }
29
30 pub fn src_paths(&self) -> &[PathBuf] {
31 &self.src_paths
32 }
33}
34
35impl From<&MetadataPackage> for Package {
36 fn from(package: &MetadataPackage) -> Self {
37 let name = package.name.clone();
38 let src_paths = package
39 .targets
40 .iter()
41 .map(|target| target.src_path.clone().into_std_path_buf())
42 .collect();
43
44 Self { name, src_paths }
45 }
46}
47
48pub struct Metadata {
50 packages: BTreeMap<MetadataId, Package>,
54 workspace_members: Vec<MetadataId>,
56 workspace_root: PathBuf,
58 reverse_deps: BTreeMap<MetadataId, BTreeSet<MetadataId>>,
60 target_directory: PathBuf,
64 current_package: Option<Package>,
67}
68
69impl Metadata {
70 pub fn from_current_directory(cargo: &OsStr) -> anyhow::Result<Self> {
72 let output = Command::new(cargo)
73 .args(["metadata", "--format-version=1"])
74 .output()
75 .context("Could not fetch metadata")?;
76
77 std::str::from_utf8(&output.stdout)
78 .context("Invalid `cargo metadata` output")?
79 .parse()
80 .context("Issue parsing `cargo metadata` output - consider manually running it to check for issues")
81 }
82
83 pub fn package(&self, id: &MetadataId) -> Option<&Package> {
84 self.packages.get(id)
85 }
86
87 pub fn entries(&self) -> btree_map::Iter<'_, MetadataId, Package> {
88 self.packages.iter()
89 }
90
91 pub fn workspace_members(&self) -> &[MetadataId] {
92 &self.workspace_members
93 }
94
95 pub fn workspace_root(&self) -> &Path {
96 &self.workspace_root
97 }
98
99 pub fn target_directory(&self) -> &Path {
100 &self.target_directory
101 }
102
103 pub fn current_package(&self) -> Option<&Package> {
104 self.current_package.as_ref()
105 }
106
107 pub fn all_dependents_of(&self, id: &MetadataId) -> BTreeSet<&MetadataId> {
109 let mut dependents = BTreeSet::new();
110 self.all_dependents_of_helper(id, &mut dependents);
111 dependents
112 }
113
114 fn all_dependents_of_helper<'this>(
115 &'this self,
116 id: &MetadataId,
117 dependents: &mut BTreeSet<&'this MetadataId>,
118 ) {
119 if let Some(immediate_dependents) = self.reverse_deps.get(id) {
120 for immediate_dependent in immediate_dependents {
121 if dependents.insert(immediate_dependent) {
122 self.all_dependents_of_helper(immediate_dependent, dependents);
123 }
124 }
125 }
126 }
127}
128
129impl FromStr for Metadata {
130 type Err = anyhow::Error;
131
132 fn from_str(s: &str) -> Result<Self, Self::Err> {
133 let cargo_metadata: CargoMetadata = serde_json::from_str(s)?;
134
135 let current_package: Option<Package> = cargo_metadata.root_package().map(Package::from);
138
139 let CargoMetadata {
140 packages: metadata_packages,
141 workspace_members,
142 workspace_root,
143 resolve,
144 target_directory,
145 ..
146 } = cargo_metadata;
147
148 let mut packages = BTreeMap::new();
149 for metadata_package in metadata_packages {
150 let package = Package::from(&metadata_package);
151 packages.insert(metadata_package.id, package);
152 }
153
154 let mut reverse_deps: BTreeMap<_, BTreeSet<_>> = BTreeMap::new();
155 let resolve =
156 resolve.context("Resolving the dependency graph failed (old version of cargo)")?;
157 for node in resolve.nodes {
158 for dep in node.deps {
159 let dependent = node.id.clone();
160 let dependency = dep.pkg;
161 reverse_deps
162 .entry(dependency)
163 .or_default()
164 .insert(dependent);
165 }
166 }
167
168 let workspace_root = workspace_root.into_std_path_buf();
169 let target_directory = target_directory.into_std_path_buf();
170
171 Ok(Self {
172 packages,
173 workspace_members,
174 workspace_root,
175 reverse_deps,
176 target_directory,
177 current_package,
178 })
179 }
180}
181
182pub(crate) fn manifest_dir(cargo: &OsStr) -> anyhow::Result<PathBuf> {
185 let stdout = Command::new(cargo)
186 .args(["locate-project", "--message-format=plain"])
187 .output()
188 .context("could not locate manifest directory")?
189 .stdout;
190
191 let mut manifest_path: PathBuf = std::str::from_utf8(&stdout)
192 .context("output of `cargo locate-project` was not valid UTF-8")?
193 .trim()
195 .into();
196
197 manifest_path.pop();
198 Ok(manifest_path)
199}