Skip to content

Commit 1a8bfdf

Browse files
committed
Update workspace manifest with new members.
When a user runs `cargo new` or `cargo init` within a workspace, Cargo will automatically add the new package to the members list in the workspace if necessary. The heuristic to add the new package is as follows: - If there is no `members` list in the workspace yet, a new `members` list is created. - If there is an `exclude` statement, Cargo checks if the new package should be excluded. If it doesn't match the `exclude` list, the package is added to the `members` list. - If there is a glob expression in the `members` list that matches the new package, the package is not added to the `members` list. - If the existent `members` list is sorted, Cargo tries to preserve the ordering when it adds the new package. This change doesn't try to format the resulting `members` list in any way, leaving the formatting decissions to the user. Signed-off-by: David Calavera <[email protected]>
1 parent c2167e2 commit 1a8bfdf

File tree

61 files changed

+438
-26
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+438
-26
lines changed

src/cargo/ops/cargo_new.rs

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use crate::core::{Edition, Shell, Workspace};
22
use crate::util::errors::CargoResult;
33
use crate::util::important_paths::find_root_manifest_for_wd;
4+
use crate::util::toml_mut::is_sorted;
45
use crate::util::{existing_vcs_repo, FossilRepo, GitRepo, HgRepo, PijulRepo};
56
use crate::util::{restricted_names, Config};
6-
use anyhow::{anyhow, Context as _};
7-
use cargo_util::paths;
7+
use anyhow::{anyhow, Context};
8+
use cargo_util::paths::{self, write_atomic};
89
use serde::de;
910
use serde::Deserialize;
1011
use std::collections::BTreeMap;
@@ -13,6 +14,7 @@ use std::io::{BufRead, BufReader, ErrorKind};
1314
use std::path::{Path, PathBuf};
1415
use std::str::FromStr;
1516
use std::{fmt, slice};
17+
use toml_edit::{Array, Value};
1618

1719
#[derive(Clone, Copy, Debug, PartialEq)]
1820
pub enum VersionControl {
@@ -809,7 +811,7 @@ fn mk(config: &Config, opts: &MkOptions<'_>) -> CargoResult<()> {
809811
// Sometimes the root manifest is not a valid manifest, so we only try to parse it if it is.
810812
// This should not block the creation of the new project. It is only a best effort to
811813
// inherit the workspace package keys.
812-
if let Ok(workspace_document) = root_manifest.parse::<toml_edit::Document>() {
814+
if let Ok(mut workspace_document) = root_manifest.parse::<toml_edit::Document>() {
813815
if let Some(workspace_package_keys) = workspace_document
814816
.get("workspace")
815817
.and_then(|workspace| workspace.get("package"))
@@ -832,6 +834,13 @@ fn mk(config: &Config, opts: &MkOptions<'_>) -> CargoResult<()> {
832834
table["workspace"] = toml_edit::value(true);
833835
manifest["lints"] = toml_edit::Item::Table(table);
834836
}
837+
838+
// Try to add the new package to the workspace members.
839+
update_manifest_with_new_member(
840+
&root_manifest_path,
841+
&mut workspace_document,
842+
opts.path,
843+
)?;
835844
}
836845
}
837846

@@ -931,3 +940,95 @@ fn update_manifest_with_inherited_workspace_package_keys(
931940
try_remove_and_inherit_package_key(key, manifest);
932941
}
933942
}
943+
944+
/// Adds the new package member to the [workspace.members] array.
945+
/// - It first checks if the name matches any element in [workspace.exclude],
946+
/// and it ignores the name if there is a match.
947+
/// - Then it check if the name matches any element already in [workspace.members],
948+
/// and it ignores the name if there is a match.
949+
/// - If [workspace.members] doesn't exist in the manifest, it will add a new section
950+
/// with the new package in it.
951+
fn update_manifest_with_new_member(
952+
root_manifest_path: &Path,
953+
workspace_document: &mut toml_edit::Document,
954+
package_path: &Path,
955+
) -> CargoResult<()> {
956+
// Find the relative path for the package from the workspace root directory.
957+
let workspace_root = root_manifest_path.parent().with_context(|| {
958+
format!(
959+
"workspace root manifest doesn't have a parent directory `{}`",
960+
root_manifest_path.display()
961+
)
962+
})?;
963+
let relpath = pathdiff::diff_paths(package_path, workspace_root).with_context(|| {
964+
format!(
965+
"path comparison requires two absolute paths; package_path: `{}`, workspace_path: `{}`",
966+
package_path.display(),
967+
workspace_root.display()
968+
)
969+
})?;
970+
971+
let mut components = Vec::new();
972+
for comp in relpath.iter() {
973+
let comp = comp.to_str().with_context(|| {
974+
format!("invalid unicode component in path `{}`", relpath.display())
975+
})?;
976+
components.push(comp);
977+
}
978+
let display_path = components.join("/");
979+
980+
// Don't add the new package to the workspace's members
981+
// if there is an exclusion match for it.
982+
if let Some(exclude) = workspace_document
983+
.get("workspace")
984+
.and_then(|workspace| workspace.get("exclude"))
985+
.and_then(|exclude| exclude.as_array())
986+
{
987+
for member in exclude {
988+
let pat = member
989+
.as_str()
990+
.with_context(|| format!("invalid non-string exclude path `{}`", member))?;
991+
if pat == display_path {
992+
return Ok(());
993+
}
994+
}
995+
}
996+
997+
// If the members element already exist, check if one of the patterns
998+
// in the array already includes the new package's relative path.
999+
// - Add the relative path if the members don't match the new package's path.
1000+
// - Create a new members array if there are no members element in the workspace yet.
1001+
if let Some(members) = workspace_document
1002+
.get_mut("workspace")
1003+
.and_then(|workspace| workspace.get_mut("members"))
1004+
.and_then(|members| members.as_array_mut())
1005+
{
1006+
for member in members.iter() {
1007+
let pat = member
1008+
.as_str()
1009+
.with_context(|| format!("invalid non-string member `{}`", member))?;
1010+
let pattern = glob::Pattern::new(pat)
1011+
.with_context(|| format!("cannot build glob pattern from `{}`", pat))?;
1012+
1013+
if pattern.matches(&display_path) {
1014+
return Ok(());
1015+
}
1016+
}
1017+
1018+
let was_sorted = is_sorted(members.iter().map(Value::as_str));
1019+
members.push(&display_path);
1020+
if was_sorted {
1021+
members.sort_by(|lhs, rhs| lhs.as_str().cmp(&rhs.as_str()));
1022+
}
1023+
} else {
1024+
let mut array = Array::new();
1025+
array.push(&display_path);
1026+
1027+
workspace_document["workspace"]["members"] = toml_edit::value(array);
1028+
}
1029+
1030+
write_atomic(
1031+
&root_manifest_path,
1032+
workspace_document.to_string().to_string().as_bytes(),
1033+
)
1034+
}

tests/testsuite/cargo_init/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,4 @@ mod simple_hg_ignore_exists;
4242
mod simple_lib;
4343
mod unknown_flags;
4444
mod with_argument;
45+
mod workspace_add_member;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[workspace]
2+
resolver = "2"
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
use cargo_test_support::compare::assert_ui;
2+
use cargo_test_support::prelude::*;
3+
use cargo_test_support::Project;
4+
5+
use cargo_test_support::curr_dir;
6+
7+
#[cargo_test]
8+
fn case() {
9+
let project = Project::from_template(curr_dir!().join("in"));
10+
let project_root = &project.root();
11+
12+
snapbox::cmd::Command::cargo_ui()
13+
.arg_line("init --bin --vcs none")
14+
.current_dir(project_root.join("crates").join("foo"))
15+
.assert()
16+
.success()
17+
.stdout_matches_path(curr_dir!().join("stdout.log"))
18+
.stderr_matches_path(curr_dir!().join("stderr.log"));
19+
20+
assert_ui().subset_matches(curr_dir!().join("out"), &project_root);
21+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[workspace]
2+
resolver = "2"
3+
members = ["crates/foo"]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
name = "foo"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
8+
[dependencies]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fn main() {
2+
println!("Hello, world!");
3+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Created binary (application) package

tests/testsuite/cargo_init/workspace_add_member/stdout.log

Whitespace-only changes.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[workspace]
2+
resolver = "2"
3+
4+
[workspace.package]
5+
authors = ["Rustaceans"]
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
use cargo_test_support::compare::assert_ui;
2+
use cargo_test_support::curr_dir;
3+
use cargo_test_support::CargoCommand;
4+
use cargo_test_support::Project;
5+
6+
#[cargo_test]
7+
fn case() {
8+
let project = Project::from_template(curr_dir!().join("in"));
9+
let project_root = project.root();
10+
let cwd = &project_root;
11+
12+
snapbox::cmd::Command::cargo_ui()
13+
.arg("new")
14+
.args(["crates/foo"])
15+
.current_dir(cwd)
16+
.assert()
17+
.success()
18+
.stdout_matches_path(curr_dir!().join("stdout.log"))
19+
.stderr_matches_path(curr_dir!().join("stderr.log"));
20+
21+
assert_ui().subset_matches(curr_dir!().join("out"), &project_root);
22+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[workspace]
2+
resolver = "2"
3+
members = ["crates/foo"]
4+
5+
[workspace.package]
6+
authors = ["Rustaceans"]
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[package]
2+
name = "foo"
3+
version = "0.1.0"
4+
edition = "2021"
5+
authors.workspace = true
6+
7+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8+
9+
[dependencies]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fn main() {
2+
println!("Hello, world!");
3+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Created binary (application) `crates/foo` package

tests/testsuite/cargo_new/add_members_to_workspace_format_previous_items/stdout.log

Whitespace-only changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[workspace]
2+
resolver = "2"
3+
members = ["crates/bar", "crates/qux"]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
name = "bar"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
8+
[dependencies]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fn main() {
2+
println!("Hello, world!");
3+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
name = "qux"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
8+
[dependencies]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fn main() {
2+
println!("Hello, world!");
3+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
use cargo_test_support::compare::assert_ui;
2+
use cargo_test_support::curr_dir;
3+
use cargo_test_support::CargoCommand;
4+
use cargo_test_support::Project;
5+
6+
#[cargo_test]
7+
fn case() {
8+
let project = Project::from_template(curr_dir!().join("in"));
9+
let project_root = project.root();
10+
let cwd = &project_root;
11+
12+
snapbox::cmd::Command::cargo_ui()
13+
.arg("new")
14+
.args(["crates/foo"])
15+
.current_dir(cwd)
16+
.assert()
17+
.success()
18+
.stdout_matches_path(curr_dir!().join("stdout.log"))
19+
.stderr_matches_path(curr_dir!().join("stderr.log"));
20+
21+
assert_ui().subset_matches(curr_dir!().join("out"), &project_root);
22+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[workspace]
2+
resolver = "2"
3+
members = ["crates/bar", "crates/foo", "crates/qux"]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
name = "foo"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
8+
[dependencies]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fn main() {
2+
println!("Hello, world!");
3+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Created binary (application) `crates/foo` package

tests/testsuite/cargo_new/add_members_to_workspace_format_sorted/stdout.log

Whitespace-only changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[workspace]
2+
resolver = "2"
3+
members = []
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
use cargo_test_support::compare::assert_ui;
2+
use cargo_test_support::curr_dir;
3+
use cargo_test_support::CargoCommand;
4+
use cargo_test_support::Project;
5+
6+
#[cargo_test]
7+
fn case() {
8+
let project = Project::from_template(curr_dir!().join("in"));
9+
let project_root = project.root();
10+
let cwd = &project_root;
11+
let package_path = cwd.join("crates").join("foo");
12+
13+
snapbox::cmd::Command::cargo_ui()
14+
.arg("new")
15+
.args([package_path])
16+
.current_dir(cwd)
17+
.assert()
18+
.success()
19+
.stdout_matches_path(curr_dir!().join("stdout.log"))
20+
.stderr_matches_path(curr_dir!().join("stderr.log"));
21+
22+
assert_ui().subset_matches(curr_dir!().join("out"), &project_root);
23+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[workspace]
2+
resolver = "2"
3+
members = ["crates/foo"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
name = "foo"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
8+
[dependencies]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fn main() {
2+
println!("Hello, world!");
3+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Created binary (application) `[ROOT]/case/crates/foo` package

tests/testsuite/cargo_new/add_members_to_workspace_with_absolute_package_path/stdout.log

Whitespace-only changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[workspace]
2+
resolver = "2"
3+
members = []
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
use cargo_test_support::compare::assert_ui;
2+
use cargo_test_support::curr_dir;
3+
use cargo_test_support::CargoCommand;
4+
use cargo_test_support::Project;
5+
6+
#[cargo_test]
7+
fn case() {
8+
let project = Project::from_template(curr_dir!().join("in"));
9+
let project_root = project.root();
10+
let cwd = &project_root;
11+
12+
snapbox::cmd::Command::cargo_ui()
13+
.arg("new")
14+
.args(["crates/foo"])
15+
.current_dir(cwd)
16+
.assert()
17+
.success()
18+
.stdout_matches_path(curr_dir!().join("stdout.log"))
19+
.stderr_matches_path(curr_dir!().join("stderr.log"));
20+
21+
assert_ui().subset_matches(curr_dir!().join("out"), &project_root);
22+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[workspace]
2+
resolver = "2"
3+
members = ["crates/foo"]

0 commit comments

Comments
 (0)