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