1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
use crate::config::workspace::WorkspaceMember;
use axoproject::{PackageIdx, WorkspaceInfo, WorkspaceSearch};
use camino::{Utf8Path, Utf8PathBuf};
use std::path::PathBuf;
use super::ProjectLayer;
use crate::errors::*;
/// Info gleaned from axoproject
#[derive(Debug)]
pub struct AxoprojectLayer {
/// Generic project info
pub project: Option<ProjectLayer>,
/// Did they have cargo_dist settings?
pub cargo_dist: Option<bool>,
/// Information about workspace packages
pub members: Option<Vec<WorkspaceMember>>,
}
impl AxoprojectLayer {
/// Load package information about a single-package workspace, where the package is equal to the
/// current working dir. This is in opposition to our workspace support, which needs to be
/// explicitly enabled. axoproject is workspace-aware, but we don't use the multi-package
/// workspace functionality it gives us when ran like this.
pub fn load(project_root: Option<PathBuf>) -> Result<Option<AxoprojectLayer>> {
// Start in the project root, or failing that current dir
let start_dir = project_root.unwrap_or_else(|| {
std::env::current_dir().expect("couldn't get current working dir!?")
});
let start_dir = Utf8PathBuf::from_path_buf(start_dir).expect("project path isn't utf8!?");
let workspace = Self::get_best_workspace(&start_dir);
let Some(workspace) = workspace else {
return Ok(None);
};
let project = Self::get_root_package(&start_dir, workspace);
if let Some((workspace, pkg)) = project {
// Cool we found the best possible match, now extract all the values we care about from it
let package = workspace.package(pkg);
// If there's a [workspace.metadata.dist] table, we can auto-enable cargo-dist
// If there's no [workspace.metadata] table at all, inconclusive.
let cargo_dist = workspace
.cargo_metadata_table
.as_ref()
.map(|t| t.get("dist").is_some());
Ok(Some(AxoprojectLayer {
project: Some(ProjectLayer {
name: Some(package.name.clone()),
description: package.description.clone(),
homepage: package.homepage_url.clone(),
repository: package.repository_url.clone(),
version: package.version.as_ref().map(|v| v.to_string()),
license: package.license.clone(),
readme_path: package.readme_file.as_ref().map(|v| v.to_string()),
}),
cargo_dist,
members: None,
}))
} else {
Ok(None)
}
}
/// Load packages from an actual workspace. This is in contract to `load`, which only collects
/// information about one package. Here, we simply collect workspace metadata for every
/// found workspace member.
pub fn load_workspace(project_root: &Utf8Path) -> Result<Option<AxoprojectLayer>> {
// Just ignore the package this function picks out for us. We want all packages instead
let Some(workspace) = Self::get_best_workspace(project_root) else {
return Ok(None);
};
// Gimme all packages!
let mut members = Vec::new();
for (_, package) in workspace.packages() {
let member = WorkspaceMember {
path: package.package_root.clone().into(),
slug: slug::slugify(package.name.clone()),
};
members.push(member);
}
Ok(Some(AxoprojectLayer {
project: None,
cargo_dist: None,
members: Some(members),
}))
}
/// Given context, fetches workspaces and returns whichever "wins". Right now, this means Cargo
/// projects always win over JS projects, but this will change in the future as we introduce more
/// criteria.
pub fn get_best_workspace(start_dir: &Utf8Path) -> Option<WorkspaceInfo> {
// Clamp the search for project files to the the start dir, because oranda
// wants to work in so many different situations that things get muddy very quickly
let clamp_to_dir = start_dir;
// Search for workspaces and process the results
let workspaces = axoproject::get_workspaces(start_dir, Some(clamp_to_dir));
let rust_workspace = Self::handle_search_result(workspaces.rust, "rust");
let js_workspace = Self::handle_search_result(workspaces.javascript, "javascript");
// Now pick the "best" one based on... absolutely nothing right now! Since we clamp to
// one dir, all the parseable projects are on perfectly even footing, so we just
// will always pick the Cargo.toml over the package.json. In the future we'll have
// configs to disambiguate.
let all_workspaces = vec![rust_workspace, js_workspace];
let mut best_workspace: Option<WorkspaceInfo> = None;
let mut rejected_workspaces = vec![];
for workspace in all_workspaces {
let Some(workspace) = workspace else {
continue;
};
// In the future this will be some more complex criteria like "closes package" or
// "has an oranda config", but for now the criteria is "first one wins".
let is_better = best_workspace.is_none();
if is_better {
if let Some(defeated) = best_workspace {
rejected_workspaces.push(defeated);
}
best_workspace = Some(workspace);
} else {
rejected_workspaces.push(workspace);
}
}
// Warn about the existence of perfectly good losers
for reject_ws in rejected_workspaces {
let message = format!(
"Also found a {:?} project at {}, but we're ignoring it",
reject_ws.kind, reject_ws.manifest_path,
);
tracing::warn!("{}", &message);
}
best_workspace
}
/// Handles the `WorkspaceSearch` enum, emitting warnings for a bunch of cases.
fn handle_search_result(search: WorkspaceSearch, name: &str) -> Option<WorkspaceInfo> {
match search {
WorkspaceSearch::Found(workspace) => Some(workspace),
WorkspaceSearch::Broken {
manifest_path,
cause,
} => {
let warning = OrandaError::BrokenProject {
kind: name.to_owned(),
manifest_path,
cause,
};
eprintln!("{:?}", miette::Report::new(warning));
None
}
WorkspaceSearch::Missing(cause) => {
// Just quietly debug log this in case it's useful
tracing::trace!(
"Couldn't find a {name} project: {:?}",
&miette::Report::new(cause)
);
None
}
}
}
/// Given a workspace, tries to find the "root" package that's contained in the same directory
/// as the workspace root.
fn get_root_package(
start_dir: &Utf8Path,
workspace: WorkspaceInfo,
) -> Option<(WorkspaceInfo, PackageIdx)> {
// Now that we found the workspace, find the actual package that appears
// in the dir we're looking at. We need to use canonicalize here because
// something in guppy/cargo is desugarring symlinks in their output, so
// we need to too.
let package = workspace.packages().find_map(|(idx, p)| {
let package_dir = p
.manifest_path
.parent()
.expect("project manifest file wasn't in a dir!?");
if is_same_path(package_dir, start_dir) {
Some(idx)
} else {
None
}
});
package.map(|pkg_idx| (workspace, pkg_idx))
}
}
fn is_same_path(path1: &Utf8Path, path2: &Utf8Path) -> bool {
if let Ok(path1) = std::fs::canonicalize(path1) {
if let Ok(path2) = std::fs::canonicalize(path2) {
return path1 == path2;
}
}
path1 == path2
}