diff --git a/src/bin/package.rs b/src/bin/package.rs index 31e3330ad5f..9c91583d0e8 100644 --- a/src/bin/package.rs +++ b/src/bin/package.rs @@ -61,6 +61,7 @@ pub fn execute(options: Options, config: &mut Config) -> CliResult { allow_dirty: options.flag_allow_dirty, target: options.flag_target.as_ref().map(|t| &t[..]), jobs: options.flag_jobs, + registry: None, })?; Ok(()) } diff --git a/src/cargo/core/manifest.rs b/src/cargo/core/manifest.rs index 53c70baf917..0ebd0a39d6d 100644 --- a/src/cargo/core/manifest.rs +++ b/src/cargo/core/manifest.rs @@ -29,7 +29,7 @@ pub struct Manifest { include: Vec, metadata: ManifestMetadata, profiles: Profiles, - publish: bool, + publish: Option>, replace: Vec<(PackageIdSpec, Dependency)>, patch: HashMap>, workspace: WorkspaceConfig, @@ -240,7 +240,7 @@ impl Manifest { links: Option, metadata: ManifestMetadata, profiles: Profiles, - publish: bool, + publish: Option>, replace: Vec<(PackageIdSpec, Dependency)>, patch: HashMap>, workspace: WorkspaceConfig, @@ -277,7 +277,7 @@ impl Manifest { pub fn version(&self) -> &Version { self.package_id().version() } pub fn warnings(&self) -> &[DelayedWarning] { &self.warnings } pub fn profiles(&self) -> &Profiles { &self.profiles } - pub fn publish(&self) -> bool { self.publish } + pub fn publish(&self) -> &Option> { &self.publish } pub fn replace(&self) -> &[(PackageIdSpec, Dependency)] { &self.replace } pub fn original(&self) -> &TomlManifest { &self.original } pub fn patch(&self) -> &HashMap> { &self.patch } diff --git a/src/cargo/core/package.rs b/src/cargo/core/package.rs index 885dad937ef..1edc51b03ea 100644 --- a/src/cargo/core/package.rs +++ b/src/cargo/core/package.rs @@ -108,7 +108,7 @@ impl Package { /// Get the package authors pub fn authors(&self) -> &Vec { &self.manifest.metadata().authors } /// Whether the package is set to publish - pub fn publish(&self) -> bool { self.manifest.publish() } + pub fn publish(&self) -> &Option> { self.manifest.publish() } /// Whether the package uses a custom build script for any target pub fn has_custom_build(&self) -> bool { @@ -134,10 +134,10 @@ impl Package { } } - pub fn to_registry_toml(&self) -> String { + pub fn to_registry_toml(&self) -> CargoResult { let manifest = self.manifest().original().prepare_for_publish(); - let toml = toml::to_string(&manifest).unwrap(); - format!("\ + let toml = toml::to_string(&manifest)?; + Ok(format!("\ # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO\n\ #\n\ # When uploading crates to the registry Cargo will automatically\n\ @@ -151,7 +151,7 @@ impl Package { # will likely look very different (and much more reasonable)\n\ \n\ {}\ - ", toml) + ", toml)) } } diff --git a/src/cargo/core/source/source_id.rs b/src/cargo/core/source/source_id.rs index 955ecca2107..350437055e5 100644 --- a/src/cargo/core/source/source_id.rs +++ b/src/cargo/core/source/source_id.rs @@ -32,6 +32,8 @@ struct SourceIdInner { kind: Kind, // e.g. the exact git revision of the specified branch for a Git Source precise: Option, + /// Name of the registry source for alternative registries + name: Option, } /// The possible kinds of code source. Along with a URL, this fully defines the @@ -72,6 +74,7 @@ impl SourceId { canonical_url: git::canonicalize_url(&url)?, url: url, precise: None, + name: None, }), }; Ok(source_id) @@ -190,6 +193,7 @@ impl SourceId { canonical_url: git::canonicalize_url(&url)?, url: url, precise: None, + name: Some(key.to_string()), }), }) } @@ -211,11 +215,16 @@ impl SourceId { /// Is this source from a registry (either local or not) pub fn is_registry(&self) -> bool { match self.inner.kind { - Kind::Registry | Kind::LocalRegistry => true, - _ => false, + Kind::Registry | Kind::LocalRegistry => true, + _ => false, } } + /// Is this source from an alternative registry + pub fn is_alt_registry(&self) -> bool { + self.is_registry() && self.inner.name.is_some() + } + /// Is this source from a git repository pub fn is_git(&self) -> bool { match self.inner.kind { diff --git a/src/cargo/ops/cargo_package.rs b/src/cargo/ops/cargo_package.rs index f12ce12f35a..04c3f6d971c 100644 --- a/src/cargo/ops/cargo_package.rs +++ b/src/cargo/ops/cargo_package.rs @@ -23,13 +23,17 @@ pub struct PackageOpts<'cfg> { pub verify: bool, pub jobs: Option, pub target: Option<&'cfg str>, + pub registry: Option, } pub fn package(ws: &Workspace, opts: &PackageOpts) -> CargoResult> { let pkg = ws.current()?; let config = ws.config(); - if !pkg.manifest().features().activated().is_empty() { + + // Allow packaging if a registry has been provided, or if there are no nightly + // features enabled. + if opts.registry.is_none() && !pkg.manifest().features().activated().is_empty() { bail!("cannot package or publish crates which activate nightly-only \ cargo features") } @@ -251,7 +255,7 @@ fn tar(ws: &Workspace, })?; let mut header = Header::new_ustar(); - let toml = pkg.to_registry_toml(); + let toml = pkg.to_registry_toml()?; header.set_path(&path)?; header.set_entry_type(EntryType::file()); header.set_mode(0o644); diff --git a/src/cargo/ops/registry.rs b/src/cargo/ops/registry.rs index d8364c28d29..18f2d5cea6a 100644 --- a/src/cargo/ops/registry.rs +++ b/src/cargo/ops/registry.rs @@ -42,10 +42,16 @@ pub struct PublishOpts<'cfg> { pub fn publish(ws: &Workspace, opts: &PublishOpts) -> CargoResult<()> { let pkg = ws.current()?; - if !pkg.publish() { - bail!("some crates cannot be published.\n\ - `{}` is marked as unpublishable", pkg.name()); + if let &Some(ref allowed_registries) = pkg.publish() { + if !match opts.registry { + Some(ref registry) => allowed_registries.contains(registry), + None => false, + } { + bail!("some crates cannot be published.\n\ + `{}` is marked as unpublishable", pkg.name()); + } } + if !pkg.manifest().patch().is_empty() { bail!("published crates cannot contain [patch] sections"); } @@ -66,11 +72,12 @@ pub fn publish(ws: &Workspace, opts: &PublishOpts) -> CargoResult<()> { allow_dirty: opts.allow_dirty, target: opts.target, jobs: opts.jobs, + registry: opts.registry.clone(), })?.unwrap(); // Upload said tarball to the specified destination opts.config.shell().status("Uploading", pkg.package_id().to_string())?; - transmit(opts.config, pkg, tarball.file(), &mut registry, opts.dry_run)?; + transmit(opts.config, pkg, tarball.file(), &mut registry, ®_id, opts.dry_run)?; Ok(()) } @@ -86,10 +93,14 @@ fn verify_dependencies(pkg: &Package, registry_src: &SourceId) } } else if dep.source_id() != registry_src { if dep.source_id().is_registry() { - bail!("crates cannot be published to crates.io with dependencies sourced from other\n\ - registries either publish `{}` on crates.io or pull it into this repository\n\ - and specify it with a path and version\n\ - (crate `{}` is pulled from {}", dep.name(), dep.name(), dep.source_id()); + // Block requests to send to a registry if it is not an alternative + // registry + if !registry_src.is_alt_registry() { + bail!("crates cannot be published to crates.io with dependencies sourced from other\n\ + registries either publish `{}` on crates.io or pull it into this repository\n\ + and specify it with a path and version\n\ + (crate `{}` is pulled from {})", dep.name(), dep.name(), dep.source_id()); + } } else { bail!("crates cannot be published to crates.io with dependencies sourced from \ a repository\neither publish `{}` as its own crate on crates.io and \ @@ -106,8 +117,19 @@ fn transmit(config: &Config, pkg: &Package, tarball: &File, registry: &mut Registry, + registry_id: &SourceId, dry_run: bool) -> CargoResult<()> { + let deps = pkg.dependencies().iter().map(|dep| { + + // If the dependency is from a different registry, then include the + // registry in the dependency. + let dep_registry = if dep.source_id() != registry_id { + Some(dep.source_id().url().to_string()) + } else { + None + }; + NewCrateDependency { optional: dep.is_optional(), default_features: dep.uses_default_features(), @@ -120,6 +142,7 @@ fn transmit(config: &Config, Kind::Build => "build", Kind::Development => "dev", }.to_string(), + registry: dep_registry, } }).collect::>(); let manifest = pkg.manifest(); @@ -222,9 +245,10 @@ pub fn registry(config: &Config, index: index_config, } = registry_configuration(config, registry.clone())?; let token = token.or(token_config); - let sid = match (index_config, index) { - (Some(index), _) | (None, Some(index)) => SourceId::for_registry(&index.to_url()?)?, - (None, None) => SourceId::crates_io(config)?, + let sid = match (index_config, index, registry) { + (_, _, Some(registry)) => SourceId::alt_registry(config, ®istry)?, + (Some(index), _, _) | (None, Some(index), _) => SourceId::for_registry(&index.to_url()?)?, + (None, None, _) => SourceId::crates_io(config)?, }; let api_host = { let mut src = RegistrySource::remote(&sid, config); diff --git a/src/cargo/util/toml/mod.rs b/src/cargo/util/toml/mod.rs index 8f53ae15a64..a2b33bfb196 100644 --- a/src/cargo/util/toml/mod.rs +++ b/src/cargo/util/toml/mod.rs @@ -200,6 +200,8 @@ pub struct DetailedTomlDependency { #[derive(Debug, Deserialize, Serialize)] pub struct TomlManifest { + #[serde(rename = "cargo-features")] + cargo_features: Option>, package: Option>, project: Option>, profile: Option, @@ -223,8 +225,6 @@ pub struct TomlManifest { patch: Option>>, workspace: Option, badges: Option>>, - #[serde(rename = "cargo-features")] - cargo_features: Option>, } #[derive(Deserialize, Serialize, Clone, Debug, Default)] @@ -381,6 +381,44 @@ impl<'de> de::Deserialize<'de> for StringOrBool { } } +#[derive(Clone, Debug, Serialize)] +#[serde(untagged)] +pub enum VecStringOrBool { + VecString(Vec), + Bool(bool), +} + +impl<'de> de::Deserialize<'de> for VecStringOrBool { + fn deserialize(deserializer: D) -> Result + where D: de::Deserializer<'de> + { + struct Visitor; + + impl<'de> de::Visitor<'de> for Visitor { + type Value = VecStringOrBool; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a boolean or vector of strings") + } + + fn visit_seq(self, v: V) -> Result + where V: de::SeqAccess<'de> + { + let seq = de::value::SeqAccessDeserializer::new(v); + Vec::deserialize(seq).map(VecStringOrBool::VecString) + } + + fn visit_bool(self, b: bool) -> Result + where E: de::Error, + { + Ok(VecStringOrBool::Bool(b)) + } + } + + deserializer.deserialize_any(Visitor) + } +} + #[derive(Deserialize, Serialize, Clone, Debug)] pub struct TomlProject { name: String, @@ -390,7 +428,7 @@ pub struct TomlProject { links: Option, exclude: Option>, include: Option>, - publish: Option, + publish: Option, workspace: Option, #[serde(rename = "im-a-teapot")] im_a_teapot: Option, @@ -655,7 +693,16 @@ impl TomlManifest { } }; let profiles = build_profiles(&me.profile); - let publish = project.publish.unwrap_or(true); + let publish = match project.publish { + Some(VecStringOrBool::VecString(ref vecstring)) => { + features.require(Feature::alternative_registries()).chain_err(|| { + "the `publish` manifest key is unstable for anything other than a value of true or false" + })?; + Some(vecstring.clone()) + }, + Some(VecStringOrBool::Bool(false)) => Some(vec![]), + None | Some(VecStringOrBool::Bool(true)) => None, + }; let mut manifest = Manifest::new(summary, targets, exclude, diff --git a/src/crates-io/lib.rs b/src/crates-io/lib.rs index 4ccfea2d8a9..9645be81a6f 100644 --- a/src/crates-io/lib.rs +++ b/src/crates-io/lib.rs @@ -100,6 +100,8 @@ pub struct NewCrateDependency { pub version_req: String, pub target: Option, pub kind: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub registry: Option, } #[derive(Deserialize)] diff --git a/tests/alt-registry.rs b/tests/alt-registry.rs index 42770296ea0..430a37fcc53 100644 --- a/tests/alt-registry.rs +++ b/tests/alt-registry.rs @@ -72,6 +72,41 @@ fn depend_on_alt_registry() { dir = p.url()))); } +#[test] +fn depend_on_alt_registry_depends_on_same_registry_no_index() { + let p = project("foo") + .file("Cargo.toml", r#" + cargo-features = ["alternative-registries"] + + [project] + name = "foo" + version = "0.0.1" + authors = [] + + [dependencies.bar] + version = "0.0.1" + registry = "alternative" + "#) + .file("src/main.rs", "fn main() {}") + .build(); + + Package::new("baz", "0.0.1").alternative(true).publish(); + Package::new("bar", "0.0.1").dep("baz", "0.0.1").alternative(true).publish(); + + assert_that(p.cargo("build").masquerade_as_nightly_cargo(), + execs().with_status(0).with_stderr(&format!("\ +[UPDATING] registry `{reg}` +[DOWNLOADING] [..] v0.0.1 (registry `file://[..]`) +[DOWNLOADING] [..] v0.0.1 (registry `file://[..]`) +[COMPILING] baz v0.0.1 (registry `file://[..]`) +[COMPILING] bar v0.0.1 (registry `file://[..]`) +[COMPILING] foo v0.0.1 ({dir}) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..] secs +", + dir = p.url(), + reg = registry::alt_registry()))); +} + #[test] fn depend_on_alt_registry_depends_on_same_registry() { let p = project("foo") @@ -107,7 +142,6 @@ fn depend_on_alt_registry_depends_on_same_registry() { reg = registry::alt_registry()))); } - #[test] fn depend_on_alt_registry_depends_on_crates_io() { let p = project("foo") @@ -192,7 +226,30 @@ fn registry_incompatible_with_git() { } #[test] -fn cannot_publish_with_registry_dependency() { +fn cannot_publish_to_crates_io_with_registry_dependency() { + let p = project("foo") + .file("Cargo.toml", r#" + cargo-features = ["alternative-registries"] + [project] + name = "foo" + version = "0.0.1" + authors = [] + [dependencies.bar] + version = "0.0.1" + registry = "alternative" + "#) + .file("src/main.rs", "fn main() {}") + .build(); + + Package::new("bar", "0.0.1").alternative(true).publish(); + + assert_that(p.cargo("publish").masquerade_as_nightly_cargo() + .arg("--index").arg(registry::registry().to_string()), + execs().with_status(101)); +} + +#[test] +fn publish_with_registry_dependency() { let p = project("foo") .file("Cargo.toml", r#" cargo-features = ["alternative-registries"] @@ -211,9 +268,14 @@ fn cannot_publish_with_registry_dependency() { Package::new("bar", "0.0.1").alternative(true).publish(); + // Login so that we have the token available + assert_that(p.cargo("login").masquerade_as_nightly_cargo() + .arg("--registry").arg("alternative").arg("TOKEN").arg("-Zunstable-options"), + execs().with_status(0)); + assert_that(p.cargo("publish").masquerade_as_nightly_cargo() - .arg("--index").arg(registry::alt_registry().to_string()), - execs().with_status(101)); + .arg("--registry").arg("alternative").arg("-Zunstable-options"), + execs().with_status(0)); } #[test] @@ -288,6 +350,8 @@ fn block_publish_due_to_no_token() { fn publish_to_alt_registry() { let p = project("foo") .file("Cargo.toml", r#" + cargo-features = ["alternative-registries"] + [project] name = "foo" version = "0.0.1" @@ -301,7 +365,7 @@ fn publish_to_alt_registry() { // Login so that we have the token available assert_that(p.cargo("login").masquerade_as_nightly_cargo() - .arg("--registry").arg("alternative").arg("TOKEN").arg("-Zunstable-options"), + .arg("--registry").arg("alternative").arg("TOKEN").arg("-Zunstable-options"), execs().with_status(0)); // Now perform the actual publish @@ -312,3 +376,34 @@ fn publish_to_alt_registry() { // Ensure that the crate is uploaded assert!(alt_dl_path().join("api/v1/crates/new").exists()); } + +#[test] +fn publish_with_crates_io_dep() { + let p = project("foo") + .file("Cargo.toml", r#" + cargo-features = ["alternative-registries"] + + [project] + name = "foo" + version = "0.0.1" + authors = ["me"] + license = "MIT" + description = "foo" + + [dependencies.bar] + version = "0.0.1" + "#) + .file("src/main.rs", "fn main() {}") + .build(); + + Package::new("bar", "0.0.1").publish(); + + // Login so that we have the token available + assert_that(p.cargo("login").masquerade_as_nightly_cargo() + .arg("--registry").arg("alternative").arg("TOKEN").arg("-Zunstable-options"), + execs().with_status(0)); + + assert_that(p.cargo("publish").masquerade_as_nightly_cargo() + .arg("--registry").arg("alternative").arg("-Zunstable-options"), + execs().with_status(0)); +} diff --git a/tests/cargotest/support/publish.rs b/tests/cargotest/support/publish.rs index b82e5d0d02c..c827be78065 100644 --- a/tests/cargotest/support/publish.rs +++ b/tests/cargotest/support/publish.rs @@ -10,10 +10,21 @@ use url::Url; pub fn setup() -> Repository { let config = paths::root().join(".cargo/config"); t!(fs::create_dir_all(config.parent().unwrap())); - t!(t!(File::create(&config)).write_all(br#" + t!(t!(File::create(&config)).write_all(format!(r#" [registry] token = "api-token" + + [registries.alternative] + index = "{registry}" + "#, registry = registry().to_string()).as_bytes())); + + let credentials = paths::root().join("home/.cargo/credentials"); + t!(fs::create_dir_all(credentials.parent().unwrap())); + t!(t!(File::create(&credentials)).write_all(br#" + [alternative] + token = "api-token" "#)); + t!(fs::create_dir_all(&upload_path().join("api/v1/crates"))); repo(®istry_path()) diff --git a/tests/publish.rs b/tests/publish.rs index d63087fe292..d4af2e99a85 100644 --- a/tests/publish.rs +++ b/tests/publish.rs @@ -7,6 +7,7 @@ use std::io::prelude::*; use std::fs::File; use std::io::SeekFrom; +use cargotest::ChannelChanger; use cargotest::support::git::repo; use cargotest::support::paths; use cargotest::support::{project, execs, publish}; @@ -500,3 +501,148 @@ See [..] // Ensure the API request wasn't actually made assert!(!publish::upload_path().join("api/v1/crates/new").exists()); } + +#[test] +fn block_publish_feature_not_enabled() { + publish::setup(); + + let p = project("foo") + .file("Cargo.toml", r#" + [project] + name = "foo" + version = "0.0.1" + authors = [] + license = "MIT" + description = "foo" + publish = [ + "test" + ] + "#) + .file("src/main.rs", "fn main() {}") + .build(); + + assert_that(p.cargo("publish").masquerade_as_nightly_cargo() + .arg("--registry").arg("alternative").arg("-Zunstable-options"), + execs().with_status(101).with_stderr("\ +error: failed to parse manifest at `[..]` + +Caused by: + the `publish` manifest key is unstable for anything other than a value of true or false + +Caused by: + feature `alternative-registries` is required + +consider adding `cargo-features = [\"alternative-registries\"]` to the manifest +")); +} + +#[test] +fn registry_not_in_publish_list() { + publish::setup(); + + let p = project("foo") + .file("Cargo.toml", r#" + cargo-features = ["alternative-registries"] + + [project] + name = "foo" + version = "0.0.1" + authors = [] + license = "MIT" + description = "foo" + publish = [ + "test" + ] + "#) + .file("src/main.rs", "fn main() {}") + .build(); + + assert_that(p.cargo("publish").masquerade_as_nightly_cargo() + .arg("--registry").arg("alternative").arg("-Zunstable-options"), + execs().with_status(101).with_stderr("\ +[ERROR] some crates cannot be published. +`foo` is marked as unpublishable +")); +} + +#[test] +fn publish_empty_list() { + publish::setup(); + + let p = project("foo") + .file("Cargo.toml", r#" + cargo-features = ["alternative-registries"] + + [project] + name = "foo" + version = "0.0.1" + authors = [] + license = "MIT" + description = "foo" + publish = [] + "#) + .file("src/main.rs", "fn main() {}") + .build(); + + assert_that(p.cargo("publish").masquerade_as_nightly_cargo() + .arg("--registry").arg("alternative").arg("-Zunstable-options"), + execs().with_status(101).with_stderr("\ +[ERROR] some crates cannot be published. +`foo` is marked as unpublishable +")); +} + +#[test] +fn publish_allowed_registry() { + publish::setup(); + + let p = project("foo").build(); + + let _ = repo(&paths::root().join("foo")) + .file("Cargo.toml", r#" + cargo-features = ["alternative-registries"] + + [project] + name = "foo" + version = "0.0.1" + authors = [] + license = "MIT" + description = "foo" + documentation = "foo" + homepage = "foo" + publish = ["alternative"] + "#) + .file("src/main.rs", "fn main() {}") + .build(); + + assert_that(p.cargo("publish").masquerade_as_nightly_cargo() + .arg("--registry").arg("alternative").arg("-Zunstable-options"), + execs().with_status(0)); +} + +#[test] +fn block_publish_no_registry() { + publish::setup(); + + let p = project("foo") + .file("Cargo.toml", r#" + cargo-features = ["alternative-registries"] + + [project] + name = "foo" + version = "0.0.1" + authors = [] + license = "MIT" + description = "foo" + publish = [] + "#) + .file("src/main.rs", "fn main() {}") + .build(); + + assert_that(p.cargo("publish").masquerade_as_nightly_cargo() + .arg("--index").arg(publish::registry().to_string()), + execs().with_status(101).with_stderr("\ +[ERROR] some crates cannot be published. +`foo` is marked as unpublishable +")); +}