1use std::collections::BTreeMap;
4
5use crate::{
6 PackageInfo, Result, Version, WorkspaceInfo, WorkspaceKind, WorkspaceSearch, WorkspaceStructure,
7};
8use axoasset::SourceFile;
9use camino::{Utf8Path, Utf8PathBuf};
10use guppy::{
11 graph::{BuildTargetId, BuildTargetKind, DependencyDirection, PackageGraph, PackageMetadata},
12 MetadataCommand,
13};
14use itertools::{concat, Itertools};
15
16pub use axoasset::toml_edit::DocumentMut;
17
18pub type CargoProfiles = BTreeMap<String, CargoProfile>;
20
21pub fn get_workspace(start_dir: &Utf8Path, clamp_to_dir: Option<&Utf8Path>) -> WorkspaceSearch {
30 let manifest_path = match workspace_manifest(start_dir, clamp_to_dir) {
34 Ok(path) => path,
35 Err(e) => {
36 return WorkspaceSearch::Missing(e);
37 }
38 };
39
40 let graph = match package_graph(start_dir) {
41 Ok(graph) => graph,
42 Err(e) => {
43 let error = match e {
44 crate::AxoprojectError::CargoMetadata(e) => {
47 if cargo_version_works() {
48 crate::AxoprojectError::CargoMetadata(e)
52 } else {
53 crate::AxoprojectError::CargoMissing {}
56 }
57 }
58 _ => e,
61 };
62 return WorkspaceSearch::Missing(error);
63 }
64 };
65
66 let workspace = workspace_info(&graph);
68 match workspace {
69 Ok(workspace) => WorkspaceSearch::Found(workspace),
70 Err(e) => WorkspaceSearch::Broken {
71 manifest_path,
72 cause: e,
73 },
74 }
75}
76
77fn cargo_version_works() -> bool {
79 std::process::Command::new("cargo")
80 .arg("--version")
81 .status()
82 .map(|s| s.success())
83 .unwrap_or(false)
84}
85
86fn package_graph(start_dir: &Utf8Path) -> Result<PackageGraph> {
88 let mut metadata_cmd = MetadataCommand::new();
89
90 metadata_cmd.current_dir(start_dir);
91
92 let pkg_graph = metadata_cmd.build_graph()?;
93
94 Ok(pkg_graph)
95}
96
97fn workspace_info(pkg_graph: &PackageGraph) -> Result<WorkspaceStructure> {
99 let workspace = pkg_graph.workspace();
100 let members = pkg_graph.resolve_workspace();
101
102 let manifest_path = workspace.root().join("Cargo.toml");
103 assert!(
106 manifest_path.exists(),
107 "cargo metadata returned a workspace without a Cargo.toml!?"
108 );
109
110 let cargo_profiles = get_profiles(&manifest_path)?;
111
112 let cargo_metadata_table = Some(workspace.metadata_table().clone());
113 let workspace_root = workspace.root();
114 let root_auto_includes = crate::find_auto_includes(workspace_root)?;
115 let mut all_package_info = vec![];
116 for package in members.packages(DependencyDirection::Forward) {
117 let mut info = package_info(workspace_root, &package, pkg_graph)?;
118 crate::merge_auto_includes(&mut info, &root_auto_includes);
119 all_package_info.push(info);
120 }
121
122 let target_dir = workspace.target_directory().to_owned();
123 let workspace_dir = workspace.root().to_owned();
124
125 Ok(WorkspaceStructure {
126 sub_workspaces: vec![],
127 packages: all_package_info,
128 workspace: WorkspaceInfo {
129 kind: WorkspaceKind::Rust,
130 target_dir,
131 workspace_dir,
132
133 manifest_path,
134 dist_manifest_path: None,
135 root_auto_includes,
136 cargo_metadata_table,
137 cargo_profiles,
138 },
139 })
140}
141
142fn package_info(
143 _workspace_root: &Utf8Path,
144 package: &PackageMetadata,
145 pkg_graph: &PackageGraph,
146) -> Result<PackageInfo> {
147 let manifest_path = package.manifest_path().to_owned();
148 let package_root = manifest_path
149 .parent()
150 .expect("package manifest had no parent!?")
151 .to_owned();
152 let cargo_package_id = Some(package.id().clone());
153 let cargo_metadata_table = Some(package.metadata_table().clone());
154
155 let mut binaries = vec![];
156 let mut cdylibs = vec![];
157 let mut cstaticlibs = vec![];
158 for target in package.build_targets() {
159 let build_id = target.id();
160 match build_id {
161 BuildTargetId::Binary(name) => {
162 binaries.push(name.to_owned());
164 }
165 BuildTargetId::Library => {
166 if let BuildTargetKind::LibraryOrExample(crate_types) = target.kind() {
205 for crate_type in crate_types {
206 match &**crate_type {
207 "cdylib" => {
208 cdylibs.push(target.name().to_owned());
209 }
210 "staticlib" => {
211 cstaticlibs.push(target.name().to_owned());
212 }
213 _ => {
214 }
216 }
217 }
218 }
219 }
220 _ => {
221 }
223 }
224 }
225
226 let keywords_and_categories: Option<Vec<String>> =
227 if package.keywords().is_empty() && package.categories().is_empty() {
228 None
229 } else {
230 let categories = package.categories().to_vec();
231 let keywords = package.keywords().to_vec();
232 Some(
233 concat(vec![categories, keywords])
234 .into_iter()
235 .unique()
236 .collect::<Vec<String>>(),
237 )
238 };
239
240 let query = pkg_graph.query_forward(std::iter::once(package.id()))?;
241 let package_set = query.resolve();
242 let mut axoupdater_versions = vec![];
243 for p in package_set.packages(DependencyDirection::Reverse) {
244 for subpackage in p.direct_links() {
245 if subpackage.dep_name() == "axoupdater" {
246 axoupdater_versions.push((
247 p.name().to_owned(),
248 Version::Cargo(subpackage.to().version().to_owned()),
249 ))
250 }
251 }
252 }
253
254 let version = Some(Version::Cargo(package.version().clone()));
255 let mut info = PackageInfo {
256 true_name: package.name().to_owned(),
257 true_version: version.clone(),
258 name: package.name().to_owned(),
259 version,
260 manifest_path,
261 dist_manifest_path: None,
262 package_root: package_root.clone(),
263 description: package.description().map(ToOwned::to_owned),
264 authors: package.authors().to_vec(),
265 keywords: keywords_and_categories,
266 license: package.license().map(ToOwned::to_owned),
267 publish: !package.publish().is_never(),
268 repository_url: package.repository().map(ToOwned::to_owned),
269 homepage_url: package.homepage().map(ToOwned::to_owned),
270 documentation_url: package.documentation().map(ToOwned::to_owned),
271 readme_file: package.readme().map(|readme| package_root.join(readme)),
272 license_files: package
273 .license_file()
274 .map(ToOwned::to_owned)
275 .into_iter()
276 .collect(),
277 changelog_file: None,
278 binaries,
279 cdylibs,
280 cstaticlibs,
281 cargo_metadata_table,
282 cargo_package_id,
283 npm_scope: None,
284 build_command: None,
285 axoupdater_versions,
286 dist: None,
287 };
288
289 let auto_includes = crate::find_auto_includes(&package_root)?;
294 crate::merge_auto_includes(&mut info, &auto_includes);
295
296 if info.documentation_url.is_none() {
298 info.documentation_url = Some(format!(
299 "https://docs.rs/{}/{}",
300 info.name,
301 info.version.as_ref().unwrap()
302 ));
303 }
304
305 Ok(info)
306}
307
308fn workspace_manifest(
311 start_dir: &Utf8Path,
312 clamp_to_dir: Option<&Utf8Path>,
313) -> Result<Utf8PathBuf> {
314 crate::find_file("Cargo.toml", start_dir, clamp_to_dir)
315}
316
317pub fn load_root_cargo_toml(manifest_path: &Utf8Path) -> Result<DocumentMut> {
319 let manifest_src = SourceFile::load_local(manifest_path)?;
320 let manifest = manifest_src.deserialize_toml_edit()?;
321 Ok(manifest)
322}
323
324fn get_profiles(manifest_path: &Utf8Path) -> Result<BTreeMap<String, CargoProfile>> {
325 let mut profiles = CargoProfiles::new();
326 let workspace_toml = load_root_cargo_toml(manifest_path)?;
327 let Some(profiles_table) = &workspace_toml.get("profile").and_then(|t| t.as_table()) else {
328 return Ok(profiles);
330 };
331
332 for (profile_name, profile) in profiles_table.iter() {
333 let debug = profile.get("debug");
335 let split_debuginfo = profile.get("split-debuginfo");
336 let inherits = profile.get("inherits");
337
338 let debug = debug.and_then(|debug| {
340 debug
341 .as_bool()
342 .map(|val| if val { 2 } else { 0 })
343 .or_else(|| debug.as_integer())
344 });
345
346 let split_debuginfo = split_debuginfo
348 .and_then(|v| v.as_str())
349 .map(ToOwned::to_owned);
350 let inherits = inherits.and_then(|v| v.as_str()).map(ToOwned::to_owned);
351
352 let entry = CargoProfile {
353 inherits,
354 debug,
355 split_debuginfo,
356 };
357 profiles.insert(profile_name.to_owned(), entry);
358 }
359
360 Ok(profiles)
361}
362
363#[derive(Debug, Clone)]
365pub struct CargoProfile {
366 pub inherits: Option<String>,
368 pub debug: Option<i64>,
372 pub split_debuginfo: Option<String>,
378}