Skip to content

Add --workspace option to cargo new and cargo init #8411

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 60 additions & 29 deletions src/cargo/ops/cargo_new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ pub struct NewOptions {
pub enum NewProjectKind {
Bin,
Lib,
Workspace,
}

impl NewProjectKind {
Expand All @@ -76,6 +77,7 @@ impl fmt::Display for NewProjectKind {
match *self {
NewProjectKind::Bin => "binary (application)",
NewProjectKind::Lib => "library",
NewProjectKind::Workspace => "workspace",
}
.fmt(f)
}
Expand All @@ -92,7 +94,7 @@ struct MkOptions<'a> {
path: &'a Path,
name: &'a str,
source_files: Vec<SourceFileInformation>,
bin: bool,
kind: NewProjectKind,
edition: Option<&'a str>,
registry: Option<&'a str>,
}
Expand All @@ -102,16 +104,20 @@ impl NewOptions {
version_control: Option<VersionControl>,
bin: bool,
lib: bool,
workspace: bool,
path: PathBuf,
name: Option<String>,
edition: Option<String>,
registry: Option<String>,
) -> CargoResult<NewOptions> {
let kind = match (bin, lib) {
(true, true) => anyhow::bail!("can't specify both lib and binary outputs"),
(false, true) => NewProjectKind::Lib,
// default to bin
(_, false) => NewProjectKind::Bin,
let kind = if bin && lib || bin && workspace || lib && workspace {
anyhow::bail!("can't specify multiple templates")
} else if lib {
NewProjectKind::Lib
} else if workspace {
NewProjectKind::Workspace
} else {
NewProjectKind::Bin
};

let opts = NewOptions {
Expand Down Expand Up @@ -154,7 +160,12 @@ fn get_name<'a>(path: &'a Path, opts: &'a NewOptions) -> CargoResult<&'a str> {
})
}

fn check_name(name: &str, name_help: &str, has_bin: bool, shell: &mut Shell) -> CargoResult<()> {
fn check_name(
name: &str,
name_help: &str,
kind: NewProjectKind,
shell: &mut Shell,
) -> CargoResult<()> {
restricted_names::validate_package_name(name, "crate name", name_help)?;

if restricted_names::is_keyword(name) {
Expand All @@ -165,7 +176,7 @@ fn check_name(name: &str, name_help: &str, has_bin: bool, shell: &mut Shell) ->
);
}
if restricted_names::is_conflicting_artifact_name(name) {
if has_bin {
if kind.is_bin() {
anyhow::bail!(
"the name `{}` cannot be used as a crate name, \
it conflicts with cargo's build directory names{}",
Expand Down Expand Up @@ -336,19 +347,22 @@ cannot automatically generate Cargo.toml as the main target would be ambiguous",
Ok(())
}

fn plan_new_source_file(bin: bool, package_name: String) -> SourceFileInformation {
if bin {
SourceFileInformation {
fn plan_new_source_file(
kind: NewProjectKind,
package_name: String,
) -> Option<SourceFileInformation> {
match kind {
NewProjectKind::Bin => Some(SourceFileInformation {
relative_path: "src/main.rs".to_string(),
target_name: package_name,
bin: true,
}
} else {
SourceFileInformation {
}),
NewProjectKind::Lib => Some(SourceFileInformation {
relative_path: "src/lib.rs".to_string(),
target_name: package_name,
bin: false,
}
}),
_ => None,
}
}

Expand All @@ -363,14 +377,18 @@ pub fn new(opts: &NewOptions, config: &Config) -> CargoResult<()> {
}

let name = get_name(path, opts)?;
check_name(name, "", opts.kind.is_bin(), &mut config.shell())?;
check_name(name, "", opts.kind, &mut config.shell())?;

let source_files = plan_new_source_file(opts.kind, name.to_string())
.map(|s| vec![s])
.unwrap_or_else(|| vec![]);

let mkopts = MkOptions {
version_control: opts.version_control,
path,
name,
source_files: vec![plan_new_source_file(opts.kind.is_bin(), name.to_string())],
bin: opts.kind.is_bin(),
source_files,
kind: opts.kind,
edition: opts.edition.as_deref(),
registry: opts.registry.as_deref(),
};
Expand Down Expand Up @@ -404,20 +422,30 @@ pub fn init(opts: &NewOptions, config: &Config) -> CargoResult<()> {
detect_source_paths_and_types(path, name, &mut src_paths_types)?;

if src_paths_types.is_empty() {
src_paths_types.push(plan_new_source_file(opts.kind.is_bin(), name.to_string()));
if let Some(source_file) = plan_new_source_file(opts.kind, name.to_string()) {
src_paths_types.push(source_file)
}
} else {
// --bin option may be ignored if lib.rs or src/lib.rs present
// Maybe when doing `cargo init --bin` inside a library package stub,
// user may mean "initialize for library, but also add binary target"
}
let has_bin = src_paths_types.iter().any(|x| x.bin);

let kind = if src_paths_types.is_empty() {
NewProjectKind::Workspace
} else if src_paths_types.iter().any(|x| x.bin) {
NewProjectKind::Bin
} else {
NewProjectKind::Lib
};

// If --name is already used to override, no point in suggesting it
// again as a fix.
let name_help = match opts.name {
Some(_) => "",
None => "\nuse --name to override crate name",
};
check_name(name, name_help, has_bin, &mut config.shell())?;
check_name(name, name_help, kind, &mut config.shell())?;

let mut version_control = opts.version_control;

Expand Down Expand Up @@ -459,7 +487,7 @@ pub fn init(opts: &NewOptions, config: &Config) -> CargoResult<()> {
version_control,
path,
name,
bin: has_bin,
kind,
source_files: src_paths_types,
edition: opts.edition.as_deref(),
registry: opts.registry.as_deref(),
Expand Down Expand Up @@ -618,7 +646,7 @@ fn mk(config: &Config, opts: &MkOptions<'_>) -> CargoResult<()> {
// both `ignore` and `hgignore` are in sync.
let mut ignore = IgnoreList::new();
ignore.push("/target", "^target/");
if !opts.bin {
if !opts.kind.is_bin() {
ignore.push("Cargo.lock", "glob:Cargo.lock");
}

Expand Down Expand Up @@ -681,9 +709,8 @@ path = {}

// Create `Cargo.toml` file with necessary `[lib]` and `[[bin]]` sections, if needed.

paths::write(
&path.join("Cargo.toml"),
format!(
let toml_base = match opts.kind {
NewProjectKind::Lib | NewProjectKind::Bin => format!(
r#"[package]
name = "{}"
version = "0.1.0"
Expand All @@ -708,9 +735,13 @@ edition = {}
None => "".to_string(),
},
cargotoml_path_specifier
)
.as_bytes(),
)?;
),
NewProjectKind::Workspace => r#"[workspace]
members = []"#
.to_string(),
};

paths::write(&path.join("Cargo.toml"), toml_base.as_bytes())?;

// Create all specified source files (with respective parent directories) if they don't exist.

Expand Down
2 changes: 2 additions & 0 deletions src/cargo/util/command_prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ pub trait AppExt: Sized {
)
._arg(opt("bin", "Use a binary (application) template [default]"))
._arg(opt("lib", "Use a library template"))
._arg(opt("workspace", "Use a workspace template"))
._arg(
opt("edition", "Edition to set for the crate generated")
.possible_values(&["2015", "2018"])
Expand Down Expand Up @@ -526,6 +527,7 @@ pub trait ArgMatchesExt {
vcs,
self._is_present("bin"),
self._is_present("lib"),
self._is_present("workspace"),
self.value_of_path("path", config).unwrap(),
self._value_of("name").map(|s| s.to_string()),
self._value_of("edition").map(|s| s.to_string()),
Expand Down
59 changes: 58 additions & 1 deletion tests/testsuite/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,45 @@ fn simple_bin() {
.is_file());
}

#[cargo_test]
fn simple_workspace() {
cargo_process("init --workspace --vcs git")
.env("USER", "foo")
.with_stderr("[CREATED] workspace package")
.run();

assert!(paths::root().join("Cargo.toml").is_file());
assert!(!paths::root().join("src").is_dir());
assert!(paths::root().join(".gitignore").is_file());

cargo_process("build")
.with_status(101)
.with_stderr(
"[ERROR] manifest path `[..]` contains no package: The manifest is virtual, and the \
workspace has no members.",
)
.run();
}

#[cargo_test]
fn workspace_ignores_edition() {
cargo_process("init --workspace --edition 2015")
.env("USER", "foo")
.with_stderr("[CREATED] workspace package")
.run();

let toml = paths::root().join("Cargo.toml");

assert!(toml.is_file());

let contents = fs::read_to_string(&toml).unwrap();
assert_eq!(
contents,
r#"[workspace]
members = []"#
);
}

#[cargo_test]
fn simple_git_ignore_exists() {
// write a .gitignore file with two entries
Expand Down Expand Up @@ -120,7 +159,25 @@ fn both_lib_and_bin() {
cargo_process("init --lib --bin")
.env("USER", "foo")
.with_status(101)
.with_stderr("[ERROR] can't specify both lib and binary outputs")
.with_stderr("[ERROR] can't specify multiple templates")
.run();
}

#[cargo_test]
fn both_lib_and_workspace() {
cargo_process("init --lib --workspace")
.env("USER", "foo")
.with_status(101)
.with_stderr("[ERROR] can't specify multiple templates")
.run();
}

#[cargo_test]
fn both_bin_and_workspace() {
cargo_process("init --bin --workspace")
.env("USER", "foo")
.with_status(101)
.with_stderr("[ERROR] can't specify multiple templates")
.run();
}

Expand Down
61 changes: 60 additions & 1 deletion tests/testsuite/new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,71 @@ fn simple_bin() {
.is_file());
}

#[cargo_test]
fn simple_workspace() {
cargo_process("new --workspace --vcs git foo")
.env("USER", "foo")
.with_stderr("[CREATED] workspace `foo` package")
.run();

assert!(paths::root().join("foo").is_dir());
assert!(paths::root().join("foo/Cargo.toml").is_file());
assert!(!paths::root().join("foo/src").is_dir());
assert!(paths::root().join("foo/.gitignore").is_file());

cargo_process("build")
.cwd(&paths::root().join("foo"))
.with_status(101)
.with_stderr(
"[ERROR] manifest path `[..]` contains no package: The manifest is virtual, and the \
workspace has no members.",
)
.run();
}

#[cargo_test]
fn workspace_ignores_edition() {
cargo_process("new --workspace --edition 2015 foo")
.env("USER", "foo")
.with_stderr("[CREATED] workspace `foo` package")
.run();

let toml = paths::root().join("foo/Cargo.toml");

assert!(toml.is_file());

let contents = fs::read_to_string(&toml).unwrap();
assert_eq!(
contents,
r#"[workspace]
members = []"#
);
}

#[cargo_test]
fn both_lib_and_bin() {
cargo_process("new --lib --bin foo")
.env("USER", "foo")
.with_status(101)
.with_stderr("[ERROR] can't specify both lib and binary outputs")
.with_stderr("[ERROR] can't specify multiple templates")
.run();
}

#[cargo_test]
fn both_lib_and_workspace() {
cargo_process("new --lib --workspace foo")
.env("USER", "foo")
.with_status(101)
.with_stderr("[ERROR] can't specify multiple templates")
.run();
}

#[cargo_test]
fn both_bin_and_workspace() {
cargo_process("new --bin --workspace foo")
.env("USER", "foo")
.with_status(101)
.with_stderr("[ERROR] can't specify multiple templates")
.run();
}

Expand Down