diff --git a/src/cargo/core/workspace.rs b/src/cargo/core/workspace.rs index 5015448a569..80313728eee 100644 --- a/src/cargo/core/workspace.rs +++ b/src/cargo/core/workspace.rs @@ -3,10 +3,12 @@ use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use std::slice; +use glob::glob; + use core::{Package, VirtualManifest, EitherManifest, SourceId}; use core::{PackageIdSpec, Dependency, Profile, Profiles}; use ops; -use util::{Config, CargoResult, Filesystem, human}; +use util::{Config, CargoResult, Filesystem, human, ChainError}; use util::paths; /// The core abstraction in Cargo for working with a workspace of crates. @@ -316,9 +318,24 @@ impl<'cfg> Workspace<'cfg> { }; if let Some(list) = members { + let root = root_manifest.parent().unwrap(); + + let mut expanded_list = Vec::new(); for path in list { - let root = root_manifest.parent().unwrap(); - let manifest_path = root.join(path).join("Cargo.toml"); + let pathbuf = root.join(path); + let expanded_paths = expand_member_path(&pathbuf)?; + + // If glob does not find any valid paths, then put the original + // path in the expanded list to maintain backwards compatibility. + if expanded_paths.is_empty() { + expanded_list.push(pathbuf); + } else { + expanded_list.extend(expanded_paths); + } + } + + for path in expanded_list { + let manifest_path = path.join("Cargo.toml"); self.find_path_deps(&manifest_path, &root_manifest, false)?; } } @@ -527,6 +544,21 @@ impl<'cfg> Workspace<'cfg> { } } +fn expand_member_path(path: &Path) -> CargoResult> { + let path = match path.to_str() { + Some(p) => p, + None => return Ok(Vec::new()), + }; + let res = glob(path).chain_error(|| { + human(format!("could not parse pattern `{}`", &path)) + })?; + res.map(|p| { + p.chain_error(|| { + human(format!("unable to match path to pattern `{}`", &path)) + }) + }).collect() +} + fn is_excluded(members: &Option>, exclude: &[String], root_path: &Path, diff --git a/src/cargo/util/errors.rs b/src/cargo/util/errors.rs index 3b1d35b6eee..dec97d0fe40 100644 --- a/src/cargo/util/errors.rs +++ b/src/cargo/util/errors.rs @@ -11,6 +11,7 @@ use core::TargetKind; use curl; use git2; +use glob; use semver; use serde_json; use term; @@ -370,6 +371,8 @@ from_error! { term::Error, num::ParseIntError, str::ParseBoolError, + glob::PatternError, + glob::GlobError, } impl From for Box { @@ -401,6 +404,8 @@ impl CargoError for ffi::NulError {} impl CargoError for term::Error {} impl CargoError for num::ParseIntError {} impl CargoError for str::ParseBoolError {} +impl CargoError for glob::PatternError {} +impl CargoError for glob::GlobError {} // ============================================================================= // Construction helpers diff --git a/src/doc/manifest.md b/src/doc/manifest.md index 6d6eb51d3ab..401b61987ec 100644 --- a/src/doc/manifest.md +++ b/src/doc/manifest.md @@ -387,7 +387,7 @@ as: [workspace] # Optional key, inferred if not present -members = ["path/to/member1", "path/to/member2"] +members = ["path/to/member1", "path/to/member2", "path/to/member3/*"] # Optional key, empty if not present exclude = ["path1", "path/to/dir2"] @@ -413,9 +413,12 @@ manifest, is responsible for defining the entire workspace. All `path` dependencies residing in the workspace directory become members. You can add additional packages to the workspace by listing them in the `members` key. Note that members of the workspaces listed explicitly will also have their path -dependencies included in the workspace. Finally, the `exclude` key can be used -to blacklist paths from being included in a workspace. This can be useful if -some path dependencies aren't desired to be in the workspace at all. +dependencies included in the workspace. Sometimes a project may have a lot of +workspace members and it can be onerous to keep up to date. The path dependency +can also use [globs][globs] to match multiple paths. Finally, the `exclude` +key can be used to blacklist paths from being included in a workspace. This can +be useful if some path dependencies aren't desired to be in the workspace at +all. The `package.workspace` manifest key (described above) is used in member crates to point at a workspace's root crate. If this key is omitted then it is inferred diff --git a/tests/workspaces.rs b/tests/workspaces.rs index 7df7bf8da3f..4670d28f77a 100644 --- a/tests/workspaces.rs +++ b/tests/workspaces.rs @@ -1378,3 +1378,94 @@ fn exclude_but_also_depend() { execs().with_status(0)); assert_that(&p.root().join("foo/bar/target"), existing_dir()); } + +#[test] +fn glob_syntax() { + let p = project("foo") + .file("Cargo.toml", r#" + [project] + name = "foo" + version = "0.1.0" + authors = [] + + [workspace] + members = ["crates/*"] + exclude = ["crates/qux"] + "#) + .file("src/main.rs", "fn main() {}") + .file("crates/bar/Cargo.toml", r#" + [project] + name = "bar" + version = "0.1.0" + authors = [] + workspace = "../.." + "#) + .file("crates/bar/src/main.rs", "fn main() {}") + .file("crates/baz/Cargo.toml", r#" + [project] + name = "baz" + version = "0.1.0" + authors = [] + workspace = "../.." + "#) + .file("crates/baz/src/main.rs", "fn main() {}") + .file("crates/qux/Cargo.toml", r#" + [project] + name = "qux" + version = "0.1.0" + authors = [] + "#) + .file("crates/qux/src/main.rs", "fn main() {}"); + p.build(); + + assert_that(p.cargo("build"), execs().with_status(0)); + assert_that(&p.bin("foo"), existing_file()); + assert_that(&p.bin("bar"), is_not(existing_file())); + assert_that(&p.bin("baz"), is_not(existing_file())); + + assert_that(p.cargo("build").cwd(p.root().join("crates/bar")), + execs().with_status(0)); + assert_that(&p.bin("foo"), existing_file()); + assert_that(&p.bin("bar"), existing_file()); + + assert_that(p.cargo("build").cwd(p.root().join("crates/baz")), + execs().with_status(0)); + assert_that(&p.bin("foo"), existing_file()); + assert_that(&p.bin("baz"), existing_file()); + + assert_that(p.cargo("build").cwd(p.root().join("crates/qux")), + execs().with_status(0)); + assert_that(&p.bin("qux"), is_not(existing_file())); + + assert_that(&p.root().join("Cargo.lock"), existing_file()); + assert_that(&p.root().join("crates/bar/Cargo.lock"), is_not(existing_file())); + assert_that(&p.root().join("crates/baz/Cargo.lock"), is_not(existing_file())); + assert_that(&p.root().join("crates/qux/Cargo.lock"), existing_file()); +} + +#[test] +fn glob_syntax_invalid_members() { + let p = project("foo") + .file("Cargo.toml", r#" + [project] + name = "foo" + version = "0.1.0" + authors = [] + + [workspace] + members = ["crates/*"] + "#) + .file("src/main.rs", "fn main() {}") + .file("crates/bar/src/main.rs", "fn main() {}"); + p.build(); + + assert_that(p.cargo("build"), + execs().with_status(101) + .with_stderr("\ +error: failed to read `[..]Cargo.toml` + +Caused by: + [..] +")); +} +