From b8a684674a6be0c4235834530527d5b085503357 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 15 Mar 2024 11:23:18 -0500 Subject: [PATCH 1/4] feat: Add 'open-namespaces' feature This is a step towards #13576 --- src/cargo/core/features.rs | 3 +++ src/doc/src/reference/unstable.md | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/cargo/core/features.rs b/src/cargo/core/features.rs index 76ccdadb6..f3290da10 100644 --- a/src/cargo/core/features.rs +++ b/src/cargo/core/features.rs @@ -504,6 +504,9 @@ features! { /// Allow setting trim-paths in a profile to control the sanitisation of file paths in build outputs. (unstable, trim_paths, "", "reference/unstable.html#profile-trim-paths-option"), + + /// Allow multiple packages to participate in the same API namespace + (unstable, open_namespaces, "", "reference/unstable.html#open-namespaces"), } /// Status and metadata for a single unstable feature. diff --git a/src/doc/src/reference/unstable.md b/src/doc/src/reference/unstable.md index b9bead0ee..f0c5ffed0 100644 --- a/src/doc/src/reference/unstable.md +++ b/src/doc/src/reference/unstable.md @@ -87,6 +87,7 @@ For the latest nightly, see the [nightly version] of this page. * [host-config](#host-config) --- Allows setting `[target]`-like configuration settings for host build targets. * [target-applies-to-host](#target-applies-to-host) --- Alters whether certain flags will be passed to host build targets. * [gc](#gc) --- Global cache garbage collection. + * [open-namespaces](#open-namespaces) --- Allow multiple packages to participate in the same API namespace * rustdoc * [rustdoc-map](#rustdoc-map) --- Provides mappings for documentation to link to external sites like [docs.rs](https://docs.rs/). * [scrape-examples](#scrape-examples) --- Shows examples within documentation. @@ -1518,6 +1519,20 @@ cargo clean gc --max-download-age=1week cargo clean gc --max-git-size=0 --max-download-size=100MB ``` +## open-namespaces + +* Tracking Issue: [#13576](https://github.com/rust-lang/cargo/issues/13576) + +Allow multiple packages to participate in the same API namespace + +This can be enabled like so: +```toml +cargo-features = ["open-namespaces"] + +[package] +# ... +``` + # Stabilized and removed features ## Compile progress From b3212855014d6fbc2d81e94d4bc63a75fe7808e3 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 15 Mar 2024 11:49:07 -0500 Subject: [PATCH 2/4] refactor(schema): Decouple forms of name validation --- crates/cargo-util-schemas/src/manifest/mod.rs | 4 ++-- .../cargo-util-schemas/src/restricted_names.rs | 16 +++++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/crates/cargo-util-schemas/src/manifest/mod.rs b/crates/cargo-util-schemas/src/manifest/mod.rs index 75f7f628b..158598ee9 100644 --- a/crates/cargo-util-schemas/src/manifest/mod.rs +++ b/crates/cargo-util-schemas/src/manifest/mod.rs @@ -1209,7 +1209,7 @@ str_newtype!(PackageName); impl> PackageName { /// Validated package name pub fn new(name: T) -> Result { - restricted_names::validate_package_name(name.as_ref(), "package name")?; + restricted_names::validate_package_name(name.as_ref())?; Ok(Self(name)) } } @@ -1231,7 +1231,7 @@ str_newtype!(RegistryName); impl> RegistryName { /// Validated registry name pub fn new(name: T) -> Result { - restricted_names::validate_package_name(name.as_ref(), "registry name")?; + restricted_names::validate_registry_name(name.as_ref())?; Ok(Self(name)) } } diff --git a/crates/cargo-util-schemas/src/restricted_names.rs b/crates/cargo-util-schemas/src/restricted_names.rs index 79d01ac9b..36df8ce46 100644 --- a/crates/cargo-util-schemas/src/restricted_names.rs +++ b/crates/cargo-util-schemas/src/restricted_names.rs @@ -33,13 +33,15 @@ enum ErrorKind { FeatureNameStartsWithDepColon(String), } -/// Check the base requirements for a package name. -/// -/// This can be used for other things than package names, to enforce some -/// level of sanity. Note that package names have other restrictions -/// elsewhere. `cargo new` has a few restrictions, such as checking for -/// reserved names. crates.io has even more restrictions. -pub(crate) fn validate_package_name(name: &str, what: &'static str) -> Result<()> { +pub(crate) fn validate_package_name(name: &str) -> Result<()> { + validate_name(name, "package name") +} + +pub(crate) fn validate_registry_name(name: &str) -> Result<()> { + validate_name(name, "registry name") +} + +pub(crate) fn validate_name(name: &str, what: &'static str) -> Result<()> { if name.is_empty() { return Err(ErrorKind::Empty(what).into()); } From 489dde11147e6d7a45325316a4921ed0723cc377 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 15 Mar 2024 12:36:09 -0500 Subject: [PATCH 3/4] test: Show open-namespaces behavior --- tests/testsuite/main.rs | 1 + tests/testsuite/open_namespaces.rs | 244 +++++++++++++++++++++++++++++ 2 files changed, 245 insertions(+) create mode 100644 tests/testsuite/open_namespaces.rs diff --git a/tests/testsuite/main.rs b/tests/testsuite/main.rs index 1ec140d76..d32398677 100644 --- a/tests/testsuite/main.rs +++ b/tests/testsuite/main.rs @@ -127,6 +127,7 @@ mod net_config; mod new; mod offline; mod old_cargos; +mod open_namespaces; mod out_dir; mod owner; mod package; diff --git a/tests/testsuite/open_namespaces.rs b/tests/testsuite/open_namespaces.rs new file mode 100644 index 000000000..e55818432 --- /dev/null +++ b/tests/testsuite/open_namespaces.rs @@ -0,0 +1,244 @@ +use cargo_test_support::project; +use cargo_test_support::registry::RegistryBuilder; + +#[cargo_test] +fn within_namespace_requires_feature() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo::bar" + version = "0.0.1" + edition = "2015" + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("read-manifest") + .masquerade_as_nightly_cargo(&["open-namespaces"]) + .with_status(101) + .with_stderr( + "\ +[ERROR] invalid character `:` in package name: `foo::bar`, characters must be Unicode XID characters (numbers, `-`, `_`, or most letters) + --> Cargo.toml:3:24 + | +3 | name = \"foo::bar\" + | ^^^^^^^^^^ + | +", + ) + .run() +} + +#[cargo_test] +fn implicit_lib_within_namespace() { + let p = project() + .file( + "Cargo.toml", + r#" + cargo-features = ["open-namespaces"] + + [package] + name = "foo::bar" + version = "0.0.1" + edition = "2015" + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("read-manifest") + .masquerade_as_nightly_cargo(&["open-namespaces"]) + .with_status(101) + .with_stderr( + "\ +[ERROR] invalid character `:` in package name: `foo::bar`, characters must be Unicode XID characters (numbers, `-`, `_`, or most letters) + --> Cargo.toml:5:24 + | +5 | name = \"foo::bar\" + | ^^^^^^^^^^ + | +", + ) + .run() +} + +#[cargo_test] +fn implicit_bin_within_namespace() { + let p = project() + .file( + "Cargo.toml", + r#" + cargo-features = ["open-namespaces"] + + [package] + name = "foo::bar" + version = "0.0.1" + edition = "2015" + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + + p.cargo("read-manifest") + .masquerade_as_nightly_cargo(&["open-namespaces"]) + .with_status(101) + .with_stderr( + "\ +[ERROR] invalid character `:` in package name: `foo::bar`, characters must be Unicode XID characters (numbers, `-`, `_`, or most letters) + --> Cargo.toml:5:24 + | +5 | name = \"foo::bar\" + | ^^^^^^^^^^ + | +", + ) + .run() +} + +#[cargo_test] +fn explicit_bin_within_namespace() { + let p = project() + .file( + "Cargo.toml", + r#" + cargo-features = ["open-namespaces"] + + [package] + name = "foo::bar" + version = "0.0.1" + edition = "2015" + + [[bin]] + name = "foo-bar" + "#, + ) + .file("src/lib.rs", "") + .file("src/foo-bar/main.rs", "fn main() {}") + .build(); + + p.cargo("read-manifest") + .masquerade_as_nightly_cargo(&["open-namespaces"]) + .with_status(101) + .with_stderr( + "\ +[ERROR] invalid character `:` in package name: `foo::bar`, characters must be Unicode XID characters (numbers, `-`, `_`, or most letters) + --> Cargo.toml:5:24 + | +5 | name = \"foo::bar\" + | ^^^^^^^^^^ + | +", + ) + .run() +} + +#[cargo_test] +#[cfg(unix)] +fn namespaced_script_name() { + let p = cargo_test_support::project() + .file( + "foo::bar.rs", + r#"--- +cargo-features = ["open-namespaces"] +package.edition = "2021" +--- + +fn main() {} +"#, + ) + .build(); + + p.cargo("read-manifest -Zscript --manifest-path foo::bar.rs") + .masquerade_as_nightly_cargo(&["script", "open-namespaces"]) + .with_json( + r#"{ + "authors": [], + "categories": [], + "default_run": null, + "dependencies": [], + "description": null, + "documentation": null, + "edition": "2021", + "features": {}, + "homepage": null, + "id": "path+file://[..]#foo--bar@0.0.0", + "keywords": [], + "license": null, + "license_file": null, + "links": null, + "manifest_path": "[CWD]/foo::bar.rs", + "metadata": null, + "name": "foo--bar", + "publish": [], + "readme": null, + "repository": null, + "rust_version": null, + "source": null, + "targets": [ + { + "crate_types": [ + "bin" + ], + "doc": true, + "doctest": false, + "edition": "2021", + "kind": [ + "bin" + ], + "name": "foo--bar", + "src_path": "[..]/foo::bar.rs", + "test": true + } + ], + "version": "0.0.0" +} +"#, + ) + .with_stderr( + "\ +", + ) + .run(); +} + +#[cargo_test] +fn publish_namespaced() { + let registry = RegistryBuilder::new().http_api().http_index().build(); + + let p = project() + .file( + "Cargo.toml", + r#" + cargo-features = ["open-namespaces"] + + [package] + name = "foo::bar" + version = "0.0.1" + edition = "2015" + authors = [] + license = "MIT" + description = "foo" + "#, + ) + .file("src/lib.rs", "fn main() {}") + .build(); + + p.cargo("publish") + .masquerade_as_nightly_cargo(&["script", "open-namespaces"]) + .replace_crates_io(registry.index_url()) + .with_status(101) + .with_stderr( + "\ +[ERROR] invalid character `:` in package name: `foo::bar`, characters must be Unicode XID characters (numbers, `-`, `_`, or most letters) + --> Cargo.toml:5:24 + | +5 | name = \"foo::bar\" + | ^^^^^^^^^^ + | +", + ) + .run(); +} From 9ea3f260a80da949c47197ccdc43fe781426de07 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 15 Mar 2024 11:52:41 -0500 Subject: [PATCH 4/4] feat(toml): Add support for open namespaces --- .../src/restricted_names.rs | 16 +- src/cargo/util/toml/mod.rs | 13 ++ tests/testsuite/build.rs | 8 +- tests/testsuite/open_namespaces.rs | 218 +++++++++++++----- 4 files changed, 198 insertions(+), 57 deletions(-) diff --git a/crates/cargo-util-schemas/src/restricted_names.rs b/crates/cargo-util-schemas/src/restricted_names.rs index 36df8ce46..40f22197a 100644 --- a/crates/cargo-util-schemas/src/restricted_names.rs +++ b/crates/cargo-util-schemas/src/restricted_names.rs @@ -34,7 +34,10 @@ enum ErrorKind { } pub(crate) fn validate_package_name(name: &str) -> Result<()> { - validate_name(name, "package name") + for part in name.split("::") { + validate_name(part, "package name")?; + } + Ok(()) } pub(crate) fn validate_registry_name(name: &str) -> Result<()> { @@ -86,6 +89,17 @@ pub(crate) fn validate_name(name: &str, what: &'static str) -> Result<()> { /// Ensure a package name is [valid][validate_package_name] pub(crate) fn sanitize_package_name(name: &str, placeholder: char) -> String { + let mut slug = String::new(); + for part in name.split("::") { + if !slug.is_empty() { + slug.push_str("::"); + } + slug.push_str(&sanitize_name(part, placeholder)); + } + slug +} + +pub(crate) fn sanitize_name(name: &str, placeholder: char) -> String { let mut slug = String::new(); let mut chars = name.chars(); while let Some(ch) = chars.next() { diff --git a/src/cargo/util/toml/mod.rs b/src/cargo/util/toml/mod.rs index 7470c333d..bdab8260c 100644 --- a/src/cargo/util/toml/mod.rs +++ b/src/cargo/util/toml/mod.rs @@ -247,6 +247,16 @@ pub fn prepare_for_publish( package_root: &Path, ) -> CargoResult { let gctx = ws.gctx(); + + if me + .cargo_features + .iter() + .flat_map(|f| f.iter()) + .any(|f| f == "open-namespaces") + { + anyhow::bail!("cannot publish with `open-namespaces`") + } + let mut package = me.package().unwrap().clone(); package.workspace = None; let current_resolver = package @@ -587,6 +597,9 @@ pub fn to_real_manifest( }; let package_name = package.name.trim(); + if package_name.contains(':') { + features.require(Feature::open_namespaces())?; + } let resolved_path = package_root.join("Cargo.toml"); diff --git a/tests/testsuite/build.rs b/tests/testsuite/build.rs index c7c751b50..23944d93e 100644 --- a/tests/testsuite/build.rs +++ b/tests/testsuite/build.rs @@ -467,18 +467,18 @@ fn cargo_compile_with_empty_package_name() { #[cargo_test] fn cargo_compile_with_invalid_package_name() { let p = project() - .file("Cargo.toml", &basic_manifest("foo::bar", "0.0.0")) + .file("Cargo.toml", &basic_manifest("foo@bar", "0.0.0")) .build(); p.cargo("build") .with_status(101) .with_stderr( "\ -[ERROR] invalid character `:` in package name: `foo::bar`, characters must be Unicode XID characters (numbers, `-`, `_`, or most letters) +[ERROR] invalid character `@` in package name: `foo@bar`, characters must be Unicode XID characters (numbers, `-`, `_`, or most letters) --> Cargo.toml:3:16 | -3 | name = \"foo::bar\" - | ^^^^^^^^^^ +3 | name = \"foo@bar\" + | ^^^^^^^^^ | ", ) diff --git a/tests/testsuite/open_namespaces.rs b/tests/testsuite/open_namespaces.rs index e55818432..1a1972602 100644 --- a/tests/testsuite/open_namespaces.rs +++ b/tests/testsuite/open_namespaces.rs @@ -20,14 +20,15 @@ fn within_namespace_requires_feature() { .masquerade_as_nightly_cargo(&["open-namespaces"]) .with_status(101) .with_stderr( - "\ -[ERROR] invalid character `:` in package name: `foo::bar`, characters must be Unicode XID characters (numbers, `-`, `_`, or most letters) - --> Cargo.toml:3:24 - | -3 | name = \"foo::bar\" - | ^^^^^^^^^^ - | -", + r#"error: failed to parse manifest at `[CWD]/Cargo.toml` + +Caused by: + feature `open-namespaces` is required + + The package requires the Cargo feature called `open-namespaces`, but that feature is not stabilized in this version of Cargo ([..]). + Consider adding `cargo-features = ["open-namespaces"]` to the top of Cargo.toml (above the [package] table) to tell Cargo you are opting in to use this unstable feature. + See https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#open-namespaces for more information about the status of this feature. +"#, ) .run() } @@ -51,17 +52,50 @@ fn implicit_lib_within_namespace() { p.cargo("read-manifest") .masquerade_as_nightly_cargo(&["open-namespaces"]) - .with_status(101) - .with_stderr( - "\ -[ERROR] invalid character `:` in package name: `foo::bar`, characters must be Unicode XID characters (numbers, `-`, `_`, or most letters) - --> Cargo.toml:5:24 - | -5 | name = \"foo::bar\" - | ^^^^^^^^^^ - | -", + .with_json( + r#"{ + "authors": [], + "categories": [], + "default_run": null, + "dependencies": [], + "description": null, + "documentation": null, + "edition": "2015", + "features": {}, + "homepage": null, + "id": "path+file://[..]#foo::bar@0.0.1", + "keywords": [], + "license": null, + "license_file": null, + "links": null, + "manifest_path": "[CWD]/Cargo.toml", + "metadata": null, + "name": "foo::bar", + "publish": null, + "readme": null, + "repository": null, + "rust_version": null, + "source": null, + "targets": [ + { + "crate_types": [ + "lib" + ], + "doc": true, + "doctest": true, + "edition": "2015", + "kind": [ + "lib" + ], + "name": "foo::bar", + "src_path": "[CWD]/src/lib.rs", + "test": true + } + ], + "version": "0.0.1" +}"#, ) + .with_stderr("") .run() } @@ -84,17 +118,50 @@ fn implicit_bin_within_namespace() { p.cargo("read-manifest") .masquerade_as_nightly_cargo(&["open-namespaces"]) - .with_status(101) - .with_stderr( - "\ -[ERROR] invalid character `:` in package name: `foo::bar`, characters must be Unicode XID characters (numbers, `-`, `_`, or most letters) - --> Cargo.toml:5:24 - | -5 | name = \"foo::bar\" - | ^^^^^^^^^^ - | -", + .with_json( + r#"{ + "authors": [], + "categories": [], + "default_run": null, + "dependencies": [], + "description": null, + "documentation": null, + "edition": "2015", + "features": {}, + "homepage": null, + "id": "path+file://[..]#foo::bar@0.0.1", + "keywords": [], + "license": null, + "license_file": null, + "links": null, + "manifest_path": "[CWD]/Cargo.toml", + "metadata": null, + "name": "foo::bar", + "publish": null, + "readme": null, + "repository": null, + "rust_version": null, + "source": null, + "targets": [ + { + "crate_types": [ + "bin" + ], + "doc": true, + "doctest": false, + "edition": "2015", + "kind": [ + "bin" + ], + "name": "foo::bar", + "src_path": "[CWD]/src/main.rs", + "test": true + } + ], + "version": "0.0.1" +}"#, ) + .with_stderr("") .run() } @@ -116,22 +183,69 @@ fn explicit_bin_within_namespace() { "#, ) .file("src/lib.rs", "") - .file("src/foo-bar/main.rs", "fn main() {}") + .file("src/bin/foo-bar/main.rs", "fn main() {}") .build(); p.cargo("read-manifest") .masquerade_as_nightly_cargo(&["open-namespaces"]) - .with_status(101) - .with_stderr( - "\ -[ERROR] invalid character `:` in package name: `foo::bar`, characters must be Unicode XID characters (numbers, `-`, `_`, or most letters) - --> Cargo.toml:5:24 - | -5 | name = \"foo::bar\" - | ^^^^^^^^^^ - | -", + .with_json( + r#"{ + "authors": [], + "categories": [], + "default_run": null, + "dependencies": [], + "description": null, + "documentation": null, + "edition": "2015", + "features": {}, + "homepage": null, + "id": "path+file://[..]#foo::bar@0.0.1", + "keywords": [], + "license": null, + "license_file": null, + "links": null, + "manifest_path": "[CWD]/Cargo.toml", + "metadata": null, + "name": "foo::bar", + "publish": null, + "readme": null, + "repository": null, + "rust_version": null, + "source": null, + "targets": [ + { + "crate_types": [ + "lib" + ], + "doc": true, + "doctest": true, + "edition": "2015", + "kind": [ + "lib" + ], + "name": "foo::bar", + "src_path": "[CWD]/src/lib.rs", + "test": true + }, + { + "crate_types": [ + "bin" + ], + "doc": true, + "doctest": false, + "edition": "2015", + "kind": [ + "bin" + ], + "name": "foo-bar", + "src_path": "[CWD]/src/bin/foo-bar/main.rs", + "test": true + } + ], + "version": "0.0.1" +}"#, ) + .with_stderr("") .run() } @@ -164,14 +278,14 @@ fn main() {} "edition": "2021", "features": {}, "homepage": null, - "id": "path+file://[..]#foo--bar@0.0.0", + "id": "path+file://[..]#foo::bar@0.0.0", "keywords": [], "license": null, "license_file": null, "links": null, "manifest_path": "[CWD]/foo::bar.rs", "metadata": null, - "name": "foo--bar", + "name": "foo::bar", "publish": [], "readme": null, "repository": null, @@ -188,7 +302,7 @@ fn main() {} "kind": [ "bin" ], - "name": "foo--bar", + "name": "foo::bar", "src_path": "[..]/foo::bar.rs", "test": true } @@ -197,14 +311,12 @@ fn main() {} } "#, ) - .with_stderr( - "\ -", - ) + .with_stderr("") .run(); } #[cargo_test] +#[cfg(unix)] // until we get proper packaging support fn publish_namespaced() { let registry = RegistryBuilder::new().http_api().http_index().build(); @@ -232,12 +344,14 @@ fn publish_namespaced() { .with_status(101) .with_stderr( "\ -[ERROR] invalid character `:` in package name: `foo::bar`, characters must be Unicode XID characters (numbers, `-`, `_`, or most letters) - --> Cargo.toml:5:24 - | -5 | name = \"foo::bar\" - | ^^^^^^^^^^ - | +[UPDATING] crates.io index +[WARNING] manifest has no documentation, homepage or repository. +See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info. + Packaging foo::bar v0.0.1 ([CWD]) +[ERROR] failed to prepare local package for uploading + +Caused by: + cannot publish with `open-namespaces` ", ) .run();