Skip to content

Commit 9ee82c9

Browse files
authored
Use $PWD and --manifest-path to select a default package in workspaces (#25)
* Use `$PWD` and `--manifest-path` to select a default package in workspaces This restores original, prevalent `--manifest-path` usage to select a package containing the APK crate in a `[workspace]`, instead of using `-p` for that. * utils: Support path canonicalization for empty strings When calling `.parent()` on a filename the result is `""`, which should be treated as PWD (`"."`) but `dunce::canonicalize()` ends up in a "No such file or directory". * Canonicalize `--manifest-path` so that a workspace is always found `.ancestors()` only calls `.parent()` on a `Path` which walks up until the string empty, but this isn't the root of the filesystem if the path wasn't absolute. * Use workspace members to select package by `--manifest-path` or `$PWD` Instead of relying on non-workspace logic. * Pre-parse all workspace manifests to check for errors * Pre-parse all member manifests for `ByName` selection too * Reject subdirectory manifests that are not members of the workspace The previous implementation had one fatal flaw: having a non-workspace `Cargo.toml` in a subdirectory, with a `[workspace]` defined some directories higher is invalid when that `[workspace]` doesn't include the given `Cargo.toml` subdirectory/package. `cargo` rejects this with a `current package believes it's in a workspace when it's not`, and we should do the same instead of having an `unwrap_or_else` that falls back to using the workspace root `Cargo.toml` as a package (especially if it might not contain a `[package]` at all). This would for example fail when running `x new template` in the repo directory, `cd`'ing into it and invoking `x build`. Instead of complaining about `template/Cargo.toml` not being part of the workspace, it detects the workspace and falls back to building the root package because the `template` directory appears to just be a subdirectory of the root without defining a subpackage along it. Since our root doesn't define a `[package]`, the supposed-to-be-infallible `unwrap()` on `manifest.package` below fails.
1 parent fb7a414 commit 9ee82c9

File tree

4 files changed

+193
-88
lines changed

4 files changed

+193
-88
lines changed

src/error.rs

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,15 @@ pub enum Error {
1111
ManifestNotFound,
1212
RustcNotFound,
1313
ManifestPathNotFound,
14-
MultiplePackagesNotSupported,
1514
GlobPatternError(&'static str),
1615
Glob(GlobError),
1716
UnexpectedWorkspace(PathBuf),
17+
NoPackageInManifest(PathBuf),
18+
PackageNotFound(PathBuf, String),
19+
ManifestNotInWorkspace {
20+
manifest: PathBuf,
21+
workspace_manifest: PathBuf,
22+
},
1823
Io(PathBuf, IoError),
1924
Toml(PathBuf, TomlError),
2025
}
@@ -25,16 +30,48 @@ impl Display for Error {
2530
fn fmt(&self, f: &mut Formatter) -> FmtResult {
2631
f.write_str(match self {
2732
Self::InvalidArgs => "Invalid args.",
28-
Self::ManifestNotAWorkspace => "The provided Cargo.toml does not contain a `[workspace]`",
33+
Self::ManifestNotAWorkspace => {
34+
"The provided Cargo.toml does not contain a `[workspace]`"
35+
}
2936
Self::ManifestNotFound => "Didn't find Cargo.toml.",
3037
Self::ManifestPathNotFound => "The manifest-path must be a path to a Cargo.toml file",
31-
Self::MultiplePackagesNotSupported => {
32-
"Multiple packages are not yet supported (ie. in a `[workspace]`). Use `-p` to select a specific package."
33-
}
3438
Self::RustcNotFound => "Didn't find rustc.",
3539
Self::GlobPatternError(error) => error,
3640
Self::Glob(error) => return error.fmt(f),
37-
Self::UnexpectedWorkspace(path) => return write!(f, "Did not expect a `[workspace]` at {}", path.display()),
41+
Self::UnexpectedWorkspace(path) => {
42+
return write!(f, "Did not expect a `[workspace]` at `{}`", path.display())
43+
}
44+
Self::NoPackageInManifest(manifest) => {
45+
return write!(
46+
f,
47+
"Failed to parse manifest at `{}`: virtual manifests must be configured with `[workspace]`",
48+
manifest.display()
49+
)
50+
}
51+
Self::PackageNotFound(workspace, name) => {
52+
return write!(
53+
f,
54+
"package `{}` not found in workspace `{}`",
55+
name,
56+
workspace.display()
57+
)
58+
}
59+
Self::ManifestNotInWorkspace {
60+
manifest,
61+
workspace_manifest,
62+
} => {
63+
return write!(f, "current package believes it's in a workspace when it's not:
64+
current: {}
65+
workspace: {workspace_manifest_path}
66+
67+
this may be fixable by adding `{package_subpath}` to the `workspace.members` array of the manifest located at: {workspace_manifest_path}
68+
Alternatively, to keep it out of the workspace, add an empty `[workspace]` table to the package's manifest.",
69+
// TODO: Parse workspace.exclude and add back "add the package to the `workspace.exclude` array, or"
70+
manifest.display(),
71+
package_subpath = manifest.parent().unwrap().strip_prefix(workspace_manifest.parent().unwrap()).unwrap().display(),
72+
workspace_manifest_path = workspace_manifest.display(),
73+
)
74+
},
3875
Self::Io(path, error) => return write!(f, "{}: {}", path.display(), error),
3976
Self::Toml(file, error) => return write!(f, "{}: {}", file.display(), error),
4077
})

src/manifest.rs

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
use crate::error::{Error, Result};
21
use serde::Deserialize;
3-
use std::path::Path;
2+
use std::{
3+
collections::HashMap,
4+
path::{Path, PathBuf},
5+
};
6+
7+
use crate::error::{Error, Result};
8+
use crate::utils;
49

510
#[derive(Clone, Debug, Deserialize)]
611
pub struct Manifest {
@@ -13,10 +18,75 @@ impl Manifest {
1318
let contents = std::fs::read_to_string(path).map_err(|e| Error::Io(path.to_owned(), e))?;
1419
toml::from_str(&contents).map_err(|e| Error::Toml(path.to_owned(), e))
1520
}
21+
22+
/// Returns a mapping from manifest directory to manifest path and loaded manifest
23+
pub fn members(&self, workspace_root: &Path) -> Result<HashMap<PathBuf, (PathBuf, Manifest)>> {
24+
let workspace = self
25+
.workspace
26+
.as_ref()
27+
.ok_or(Error::ManifestNotAWorkspace)?;
28+
let workspace_root = utils::canonicalize(workspace_root)?;
29+
30+
// Check all member packages inside the workspace
31+
let mut all_members = HashMap::new();
32+
33+
for member in &workspace.members {
34+
for manifest_dir in glob::glob(workspace_root.join(member).to_str().unwrap())? {
35+
let manifest_dir = manifest_dir?;
36+
let manifest_path = manifest_dir.join("Cargo.toml");
37+
let manifest = Manifest::parse_from_toml(&manifest_path)?;
38+
39+
// Workspace members cannot themselves be/contain a new workspace
40+
if manifest.workspace.is_some() {
41+
return Err(Error::UnexpectedWorkspace(manifest_path));
42+
}
43+
44+
// And because they cannot contain a [workspace], they may not be a virtual manifest
45+
// and must hence contain [package]
46+
if manifest.package.is_none() {
47+
return Err(Error::NoPackageInManifest(manifest_path));
48+
}
49+
50+
all_members.insert(manifest_dir, (manifest_path, manifest));
51+
}
52+
}
53+
54+
Ok(all_members)
55+
}
56+
57+
/// Returns `self` if it contains `[package]` but not `[workspace]`, (i.e. it cannot be
58+
/// a workspace nor a virtual manifest), and describes a package named `name` if not [`None`].
59+
pub fn map_nonvirtual_package(
60+
self,
61+
manifest_path: PathBuf,
62+
name: Option<&str>,
63+
) -> Result<(PathBuf, Self)> {
64+
if self.workspace.is_some() {
65+
return Err(Error::UnexpectedWorkspace(manifest_path));
66+
}
67+
68+
if let Some(package) = &self.package {
69+
if let Some(name) = name {
70+
if package.name == name {
71+
Ok((manifest_path, self))
72+
} else {
73+
Err(Error::PackageNotFound(manifest_path, name.into()))
74+
}
75+
} else {
76+
Ok((manifest_path, self))
77+
}
78+
} else {
79+
Err(Error::NoPackageInManifest(manifest_path))
80+
}
81+
}
1682
}
1783

1884
#[derive(Clone, Debug, Deserialize)]
85+
#[serde(rename_all = "kebab-case")]
1986
pub struct Workspace {
87+
#[serde(default)]
88+
pub default_members: Vec<String>,
89+
#[serde(default)]
2090
pub members: Vec<String>,
2191
}
2292

src/subcommand.rs

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -49,23 +49,26 @@ impl Subcommand {
4949
.transpose()?;
5050

5151
let search_path = manifest_path.map_or_else(
52-
|| std::env::current_dir().unwrap(),
53-
|manifest_path| manifest_path.parent().unwrap().to_owned(),
54-
);
52+
|| std::env::current_dir().map_err(|e| Error::Io(PathBuf::new(), e)),
53+
|manifest_path| utils::canonicalize(manifest_path.parent().unwrap()),
54+
)?;
5555

56-
// Scan the given and all parent directories for a Cargo.toml containing a workspace
57-
// TODO: If set, validate that the found workspace is either the given `--manifest-path`,
58-
// or contains the given `--manifest-path` as member.
56+
// Scan up the directories based on --manifest-path and the working directory to find a Cargo.toml
57+
let potential_manifest = utils::find_manifest(&search_path)?;
58+
// Perform the same scan, but for a Cargo.toml containing [workspace]
5959
let workspace_manifest = utils::find_workspace(&search_path)?;
6060

61-
let (manifest_path, manifest) = if let Some((workspace_manifest_path, workspace)) =
62-
&workspace_manifest
63-
{
64-
// If a workspace was found, find packages relative to it
65-
utils::find_package_manifest_in_workspace(workspace_manifest_path, workspace, package)?
66-
} else {
67-
// Otherwise scan up the directories
68-
utils::find_package_manifest(&search_path, package)?
61+
let (manifest_path, manifest) = {
62+
if let Some(workspace_manifest) = &workspace_manifest {
63+
utils::find_package_manifest_in_workspace(
64+
workspace_manifest,
65+
potential_manifest,
66+
package,
67+
)?
68+
} else {
69+
let (manifest_path, manifest) = potential_manifest;
70+
manifest.map_nonvirtual_package(manifest_path, package)?
71+
}
6972
};
7073

7174
// The manifest is known to contain a package at this point

src/utils.rs

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

22-
fn match_package_name(manifest: &Manifest, name: Option<&str>) -> bool {
23-
if let Some(p) = &manifest.package {
24-
if let Some(name) = name {
25-
if name == p.name {
26-
return true;
27-
}
28-
} else {
29-
return true;
30-
}
22+
pub fn canonicalize(mut path: &Path) -> Result<PathBuf> {
23+
if path == Path::new("") {
24+
path = Path::new(".");
3125
}
32-
33-
false
26+
dunce::canonicalize(path).map_err(|e| Error::Io(path.to_owned(), e))
3427
}
3528

3629
/// 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_manifest()`] instead.
30+
/// of the given [workspace] [`Manifest`], and possibly falls back to a potential
31+
/// manifest based on the working directory or `--manifest-path` as found by
32+
/// [`find_manifest()`] and passed as argument to `potential_manifest`.
4033
///
4134
/// [workspace root]: https://doc.rust-lang.org/cargo/reference/workspaces.html#root-package
4235
/// [workspace]: https://doc.rust-lang.org/cargo/reference/workspaces.html#workspaces
4336
pub fn find_package_manifest_in_workspace(
44-
workspace_manifest_path: &Path,
45-
workspace_manifest: &Manifest,
46-
name: Option<&str>,
37+
(workspace_manifest_path, workspace_manifest): &(PathBuf, Manifest),
38+
(potential_manifest_path, potential_manifest): (PathBuf, Manifest),
39+
package_name: Option<&str>,
4740
) -> Result<(PathBuf, Manifest)> {
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)?;
41+
let potential_manifest_dir = potential_manifest_path.parent().unwrap();
42+
let workspace_manifest_dir = workspace_manifest_path.parent().unwrap();
5843

59-
// Check if the workspace manifest also contains a [package]
60-
if match_package_name(workspace_manifest, Some(name)) {
61-
return Ok((
62-
workspace_manifest_path.to_owned(),
63-
workspace_manifest.clone(),
64-
));
44+
let workspace_members = workspace_manifest.members(workspace_manifest_dir)?;
45+
// Make sure the found workspace includes the manifest "specified" by the user via --manifest-path or $PWD
46+
if workspace_manifest_path != &potential_manifest_path
47+
&& !workspace_members.contains_key(potential_manifest_dir)
48+
{
49+
return Err(Error::ManifestNotInWorkspace {
50+
manifest: potential_manifest_path,
51+
workspace_manifest: workspace_manifest_path.clone(),
52+
});
6553
}
6654

67-
// Check all member packages inside the workspace
68-
let workspace_root = workspace_manifest_path.parent().unwrap();
69-
for member in &workspace.members {
70-
for manifest_dir in glob::glob(workspace_root.join(member).to_str().unwrap())? {
71-
let manifest_path = manifest_dir?.join("Cargo.toml");
72-
let manifest = Manifest::parse_from_toml(&manifest_path)?;
55+
match package_name {
56+
// Any package in the workspace can be used if `-p` is used
57+
Some(name) => {
58+
// Check if the workspace manifest also contains a [package]
59+
if let Some(package) = &workspace_manifest.package {
60+
if package.name == name {
61+
return Ok((
62+
workspace_manifest_path.to_owned(),
63+
workspace_manifest.clone(),
64+
));
65+
}
66+
}
7367

74-
// Workspace members cannot themselves be/contain a new workspace
75-
if manifest.workspace.is_some() {
76-
return Err(Error::UnexpectedWorkspace(manifest_path));
68+
// Check all member packages inside the workspace
69+
for (_manifest_dir, (manifest_path, manifest)) in workspace_members {
70+
// .members() already checked for it having a package
71+
let package = manifest.package.as_ref().unwrap();
72+
if package.name == name {
73+
return Ok((manifest_path, manifest));
74+
}
7775
}
7876

79-
if match_package_name(&manifest, Some(name)) {
80-
return Ok((manifest_path, manifest));
77+
Err(Error::PackageNotFound(
78+
workspace_manifest_path.clone(),
79+
name.to_owned(),
80+
))
81+
}
82+
// Otherwise use the manifest we just found, as long as it contains `[package]`
83+
None => {
84+
if potential_manifest.package.is_none() {
85+
return Err(Error::NoPackageInManifest(potential_manifest_path));
8186
}
87+
Ok((potential_manifest_path, potential_manifest))
8288
}
8389
}
84-
85-
Err(Error::ManifestNotFound)
8690
}
8791

8892
/// Recursively walk up the directories until finding a `Cargo.toml`
89-
///
90-
/// When a workspace has been detected, [`find_package_manifest_in_workspace()`] to find packages
91-
/// instead (that are members of the given workspace).
92-
pub fn find_package_manifest(path: &Path, name: Option<&str>) -> Result<(PathBuf, Manifest)> {
93-
let path = dunce::canonicalize(path).map_err(|e| Error::Io(path.to_owned(), e))?;
94-
for manifest_path in path
93+
pub fn find_manifest(path: &Path) -> Result<(PathBuf, Manifest)> {
94+
let path = canonicalize(path)?;
95+
let manifest_path = path
9596
.ancestors()
9697
.map(|dir| dir.join("Cargo.toml"))
97-
.filter(|dir| dir.exists())
98-
{
99-
let manifest = Manifest::parse_from_toml(&manifest_path)?;
98+
.find(|manifest| manifest.exists())
99+
.ok_or(Error::ManifestNotFound)?;
100100

101-
// This function shouldn't be called when a workspace exists.
102-
if manifest.workspace.is_some() {
103-
return Err(Error::UnexpectedWorkspace(manifest_path));
104-
}
101+
let manifest = Manifest::parse_from_toml(&manifest_path)?;
105102

106-
if match_package_name(&manifest, name) {
107-
return Ok((manifest_path, manifest));
108-
}
109-
}
110-
Err(Error::ManifestNotFound)
103+
Ok((manifest_path, manifest))
111104
}
112105

113-
/// Find the first `Cargo.toml` that contains a `[workspace]`
106+
/// Recursively walk up the directories until finding a `Cargo.toml`
107+
/// that contains a `[workspace]`
114108
pub fn find_workspace(potential_root: &Path) -> Result<Option<(PathBuf, Manifest)>> {
109+
let potential_root = canonicalize(potential_root)?;
115110
for manifest_path in potential_root
116111
.ancestors()
117112
.map(|dir| dir.join("Cargo.toml"))
118-
.filter(|dir| dir.exists())
113+
.filter(|manifest| manifest.exists())
119114
{
120115
let manifest = Manifest::parse_from_toml(&manifest_path)?;
121116
if manifest.workspace.is_some() {

0 commit comments

Comments
 (0)