Skip to content

Commit 70046ed

Browse files
authored
Linearize workspace detection (#23)
When `cargo` scans for a workspace it simply walks up the directories until finding the first `Cargo.toml` with a `[workspace]` declaration. All crates are detected relative to this, and no special skipping behaviour (walking further up the directories) occurs when a package was not found (was previously happening in `find_package()+member()`). Simplify the original code somewhat by making this feat more obvious, rejecting more invalid configurations.
1 parent 72db8e5 commit 70046ed

File tree

3 files changed

+101
-38
lines changed

3 files changed

+101
-38
lines changed

src/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ use toml::de::Error as TomlError;
77
#[derive(Debug)]
88
pub enum Error {
99
InvalidArgs,
10+
ManifestNotAWorkspace,
1011
ManifestNotFound,
1112
RustcNotFound,
1213
ManifestPathNotFound,
1314
MultiplePackagesNotSupported,
1415
GlobPatternError(&'static str),
1516
Glob(GlobError),
17+
UnexpectedWorkspace(PathBuf),
1618
Io(PathBuf, IoError),
1719
Toml(PathBuf, TomlError),
1820
}
@@ -23,6 +25,7 @@ impl Display for Error {
2325
fn fmt(&self, f: &mut Formatter) -> FmtResult {
2426
f.write_str(match self {
2527
Self::InvalidArgs => "Invalid args.",
28+
Self::ManifestNotAWorkspace => "The provided Cargo.toml does not contain a `[workspace]`",
2629
Self::ManifestNotFound => "Didn't find Cargo.toml.",
2730
Self::ManifestPathNotFound => "The manifest-path must be a path to a Cargo.toml file",
2831
Self::MultiplePackagesNotSupported => {
@@ -31,6 +34,7 @@ impl Display for Error {
3134
Self::RustcNotFound => "Didn't find rustc.",
3235
Self::GlobPatternError(error) => error,
3336
Self::Glob(error) => return error.fmt(f),
37+
Self::UnexpectedWorkspace(path) => return write!(f, "Did not expect a `[workspace]` at {}", path.display()),
3438
Self::Io(path, error) => return write!(f, "{}: {}", path.display(), error),
3539
Self::Toml(file, error) => return write!(f, "{}: {}", file.display(), error),
3640
})

src/subcommand.rs

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ impl Subcommand {
2525
args.package.len() < 2,
2626
"Multiple packages are not supported yet by `cargo-subcommand`"
2727
);
28+
let package = args.package.get(0).map(|s| s.as_str());
2829
assert!(
2930
!args.workspace,
3031
"`--workspace` is not supported yet by `cargo-subcommand`"
@@ -46,15 +47,30 @@ impl Subcommand {
4647
})
4748
.transpose()?;
4849

49-
let (manifest_path, package) = utils::find_package(
50-
&manifest_path.unwrap_or_else(|| std::env::current_dir().unwrap()),
51-
args.package.get(0).map(|s| s.as_str()),
52-
)?;
50+
let search_path = manifest_path.map_or_else(
51+
|| std::env::current_dir().unwrap(),
52+
|manifest_path| manifest_path.parent().unwrap().to_owned(),
53+
);
54+
55+
// Scan the given and all parent directories for a Cargo.toml containing a workspace
56+
// TODO: If set, validate that the found workspace is either the given `--manifest-path`,
57+
// or contains the given `--manifest-path` as member.
58+
let workspace_manifest = utils::find_workspace(&search_path)?;
59+
60+
let (manifest_path, package) =
61+
if let Some((workspace_manifest_path, workspace)) = &workspace_manifest {
62+
// If a workspace was found, find packages relative to it
63+
utils::find_package_in_workspace(workspace_manifest_path, workspace, package)?
64+
} else {
65+
// Otherwise scan up the directories
66+
utils::find_package(&search_path, package)?
67+
};
68+
5369
let root_dir = manifest_path.parent().unwrap();
5470

5571
// TODO: Find, parse, and merge _all_ config files following the hierarchical structure:
5672
// https://doc.rust-lang.org/cargo/reference/config.html#hierarchical-structure
57-
let config = LocalizedConfig::find_cargo_config_for_workspace(&root_dir)?;
73+
let config = LocalizedConfig::find_cargo_config_for_workspace(root_dir)?;
5874
if let Some(config) = &config {
5975
config.set_env_vars().unwrap();
6076
}
@@ -76,9 +92,10 @@ impl Subcommand {
7692
});
7793

7894
let target_dir = target_dir.unwrap_or_else(|| {
79-
utils::find_workspace(&manifest_path, &package)
80-
.unwrap()
81-
.unwrap_or_else(|| manifest_path.clone())
95+
workspace_manifest
96+
.as_ref()
97+
.map(|(path, _)| path)
98+
.unwrap_or_else(|| &manifest_path)
8299
.parent()
83100
.unwrap()
84101
.join(utils::get_target_dir_name(config.as_deref()).unwrap())

src/utils.rs

Lines changed: 72 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,73 @@ pub fn list_rust_files(dir: &Path) -> Result<Vec<String>> {
1919
Ok(files)
2020
}
2121

22-
fn member(manifest: &Path, members: &[String], package: &str) -> Result<Option<PathBuf>> {
23-
let workspace_dir = manifest.parent().unwrap();
24-
for member in members {
25-
for manifest_dir in glob::glob(workspace_dir.join(member).to_str().unwrap())? {
22+
fn match_package_name(manifest: &Manifest, name: Option<&str>) -> Option<String> {
23+
if let Some(p) = &manifest.package {
24+
if let Some(name) = name {
25+
if name == p.name {
26+
return Some(p.name.clone());
27+
}
28+
} else {
29+
return Some(p.name.clone());
30+
}
31+
}
32+
33+
None
34+
}
35+
36+
/// Tries to find a package by the given `name` in the [workspace root] or member
37+
/// of the given [workspace] [`Manifest`].
38+
///
39+
/// When a workspace is not detected, call [`find_package()`] instead.
40+
///
41+
/// [workspace root]: https://doc.rust-lang.org/cargo/reference/workspaces.html#root-package
42+
/// [workspace]: https://doc.rust-lang.org/cargo/reference/workspaces.html#workspaces
43+
pub fn find_package_in_workspace(
44+
workspace_manifest_path: &Path,
45+
workspace_manifest: &Manifest,
46+
name: Option<&str>,
47+
) -> Result<(PathBuf, String)> {
48+
let workspace = workspace_manifest
49+
.workspace
50+
.as_ref()
51+
.ok_or(Error::ManifestNotAWorkspace)?;
52+
53+
// When building inside a workspace, require a package to be selected on the cmdline
54+
// as selecting the right package is non-trivial and selecting multiple packages
55+
// isn't currently supported yet. See the selection mechanism:
56+
// https://doc.rust-lang.org/cargo/reference/workspaces.html#package-selection
57+
let name = name.ok_or(Error::MultiplePackagesNotSupported)?;
58+
59+
// Check if the workspace manifest also contains a [package]
60+
if let Some(package) = match_package_name(workspace_manifest, Some(name)) {
61+
return Ok((workspace_manifest_path.to_owned(), package));
62+
}
63+
64+
// Check all member packages inside the workspace
65+
let workspace_root = workspace_manifest_path.parent().unwrap();
66+
for member in &workspace.members {
67+
for manifest_dir in glob::glob(workspace_root.join(member).to_str().unwrap())? {
2668
let manifest_path = manifest_dir?.join("Cargo.toml");
2769
let manifest = Manifest::parse_from_toml(&manifest_path)?;
28-
if let Some(p) = manifest.package.as_ref() {
29-
if p.name == package {
30-
return Ok(Some(manifest_path));
31-
}
70+
71+
// Workspace members cannot themselves be/contain a new workspace
72+
if manifest.workspace.is_some() {
73+
return Err(Error::UnexpectedWorkspace(manifest_path));
74+
}
75+
76+
if let Some(package) = match_package_name(&manifest, Some(name)) {
77+
return Ok((manifest_path, package));
3278
}
3379
}
3480
}
35-
Ok(None)
81+
82+
Err(Error::ManifestNotFound)
3683
}
3784

85+
/// Recursively walk up the directories until finding a `Cargo.toml`
86+
///
87+
/// When a workspace has been detected, [`find_package_in_workspace()`] to find packages
88+
/// instead (that are members of the given workspace).
3889
pub fn find_package(path: &Path, name: Option<&str>) -> Result<(PathBuf, String)> {
3990
let path = dunce::canonicalize(path).map_err(|e| Error::Io(path.to_owned(), e))?;
4091
for manifest_path in path
@@ -43,38 +94,29 @@ pub fn find_package(path: &Path, name: Option<&str>) -> Result<(PathBuf, String)
4394
.filter(|dir| dir.exists())
4495
{
4596
let manifest = Manifest::parse_from_toml(&manifest_path)?;
46-
if let Some(p) = manifest.package.as_ref() {
47-
if let Some(name) = name {
48-
if name == p.name {
49-
return Ok((manifest_path, p.name.clone()));
50-
}
51-
} else {
52-
return Ok((manifest_path, p.name.clone()));
53-
}
97+
98+
// This function shouldn't be called when a workspace exists.
99+
if manifest.workspace.is_some() {
100+
return Err(Error::UnexpectedWorkspace(manifest_path));
54101
}
55-
if let Some(w) = manifest.workspace.as_ref() {
56-
// TODO: This should also work if name is None - then all packages should simply be returned
57-
let name = name.ok_or(Error::MultiplePackagesNotSupported)?;
58-
if let Some(manifest_path) = member(&manifest_path, &w.members, name)? {
59-
return Ok((manifest_path, name.to_string()));
60-
}
102+
103+
if let Some(package) = match_package_name(&manifest, name) {
104+
return Ok((manifest_path, package));
61105
}
62106
}
63107
Err(Error::ManifestNotFound)
64108
}
65109

66-
pub fn find_workspace(manifest: &Path, name: &str) -> Result<Option<PathBuf>> {
67-
let dir = manifest.parent().unwrap();
68-
for manifest_path in dir
110+
/// Find the first `Cargo.toml` that contains a `[workspace]`
111+
pub fn find_workspace(potential_root: &Path) -> Result<Option<(PathBuf, Manifest)>> {
112+
for manifest_path in potential_root
69113
.ancestors()
70114
.map(|dir| dir.join("Cargo.toml"))
71115
.filter(|dir| dir.exists())
72116
{
73117
let manifest = Manifest::parse_from_toml(&manifest_path)?;
74-
if let Some(w) = manifest.workspace.as_ref() {
75-
if member(&manifest_path, &w.members, name)?.is_some() {
76-
return Ok(Some(manifest_path));
77-
}
118+
if manifest.workspace.is_some() {
119+
return Ok(Some((manifest_path, manifest)));
78120
}
79121
}
80122
Ok(None)

0 commit comments

Comments
 (0)