diff --git a/src/cargo/core/workspace.rs b/src/cargo/core/workspace.rs index bdf06d60757..390972366b0 100644 --- a/src/cargo/core/workspace.rs +++ b/src/cargo/core/workspace.rs @@ -1129,16 +1129,22 @@ impl<'gctx> Workspace<'gctx> { let current_dir = self.current_manifest.parent().unwrap(); let root_pkg = self.packages.get(root); - // FIXME: Make this more generic by using a relative path resolver between member and root. - let members_msg = match current_dir.strip_prefix(root_dir) { - Ok(rel) => format!( + // Use pathdiff to handle finding the relative path between the current package + // and the workspace root. This usually does a good job of handling `..` and + // other weird things. + // Normalize paths first to ensure `../` components are resolved if possible, + // which helps `diff_paths` find the most direct relative path. + let current_dir = paths::normalize_path(current_dir); + let root_dir = paths::normalize_path(root_dir); + let members_msg = match pathdiff::diff_paths(¤t_dir, &root_dir) { + Some(rel) => format!( "this may be fixable by adding `{}` to the \ `workspace.members` array of the manifest \ located at: {}", rel.display(), root.display() ), - Err(_) => format!( + None => format!( "this may be fixable by adding a member to \ the `workspace.members` array of the \ manifest located at: {}", diff --git a/tests/testsuite/workspaces.rs b/tests/testsuite/workspaces.rs index 0f8ab785baf..4b87aacd026 100644 --- a/tests/testsuite/workspaces.rs +++ b/tests/testsuite/workspaces.rs @@ -2755,3 +2755,126 @@ fn fix_only_check_manifest_path_member() { "#]]) .run(); } + +#[cargo_test] +fn error_if_member_is_outside_root() { + // This test ensures that if a member is physically outside the workspace root, + // we get a helpful error message. + // + // Setup: + // root/Cargo.toml (workspace, members = []) + // member/Cargo.toml (package, workspace = "../root") + + let _root = project() + .at("root") + .file( + "Cargo.toml", + r#" + [package] + name = "root" + version = "0.1.0" + edition = "2015" + + [workspace] + members = [] + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + + let member = project() + .at("member") + .file( + "Cargo.toml", + r#" + [package] + name = "member" + version = "0.1.0" + edition = "2015" + workspace = "../root" + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + + // The error message should reflect that these paths are unrelated (Old Behavior) + member.cargo("build") + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] current package believes it's in a workspace when it's not: +current: [ROOT]/member/Cargo.toml +workspace: [ROOT]/root/Cargo.toml + +this may be fixable by adding `../member` to the `workspace.members` array of the manifest located at: [ROOT]/root/Cargo.toml +Alternatively, to keep it out of the workspace, add the package to the `workspace.exclude` array, or add an empty `[workspace]` table to the package's manifest. + +"#]]) + .run(); +} + +#[cargo_test] +fn error_if_manifest_path_is_relative() { + // This test simulates running cargo usage with a relative --manifest-path + // that includes `..` to verify normalization and suggestions. + // + // Directory structure: + // root/Cargo.toml + // root/subdir/ + // outside/Cargo.toml (workspace = "../root") + + let root = project() + .at("root") + .file( + "Cargo.toml", + r#" + [package] + name = "root" + version = "0.1.0" + edition = "2015" + + [workspace] + members = [] + "#, + ) + .file("src/main.rs", "fn main() {}") + .file("subdir/file", "") + .build(); + + let _outside = project() + .at("outside") + .file( + "Cargo.toml", + r#" + [package] + name = "outside" + version = "0.1.0" + edition = "2015" + workspace = "../root" + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + + // Run from `root/subdir` pointing to `../../outside/Cargo.toml` + // The workspace root is at `root`. + // The package is at `outside`. + // Relative path from root to outside is `../outside`. + + // We execute inside `root`, but targeting the outside package. + root.cargo("build") + .cwd(root.root().join("subdir")) + .arg("-v") + .arg("--manifest-path") + .arg("../../outside/Cargo.toml") + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] current package believes it's in a workspace when it's not: +current: [ROOT]/outside/Cargo.toml +workspace: [ROOT]/root/Cargo.toml + +this may be fixable by adding `../outside` to the `workspace.members` array of the manifest located at: [ROOT]/root/Cargo.toml +Alternatively, to keep it out of the workspace, add the package to the `workspace.exclude` array, or add an empty `[workspace]` table to the package's manifest. + +"#]]) + .run(); +}