From 2a5633b6e168e0be6b0794b6da17bd7311d2b4c6 Mon Sep 17 00:00:00 2001 From: Joe Neeman Date: Mon, 10 Jun 2024 14:10:11 -0500 Subject: [PATCH 1/3] Add source overlays This adds a new mechanism for overlaying sources. An overlayed source returns packages from two (or more) sources. This functionality is not intended for public use, but it will be useful for packaging a workspace that contains inter-crate dependencies. Co-authored-by: Tor Hovland <55164+torhovland@users.noreply.github.com> --- src/cargo/sources/config.rs | 41 +++++++++- src/cargo/sources/mod.rs | 1 + src/cargo/sources/overlay.rs | 148 +++++++++++++++++++++++++++++++++++ 3 files changed, 186 insertions(+), 4 deletions(-) create mode 100644 src/cargo/sources/overlay.rs diff --git a/src/cargo/sources/config.rs b/src/cargo/sources/config.rs index a2e8051c326..4190501f3d3 100644 --- a/src/cargo/sources/config.rs +++ b/src/cargo/sources/config.rs @@ -5,6 +5,7 @@ //! sources to one another via the `replace-with` key in `.cargo/config`. use crate::core::{GitReference, PackageId, SourceId}; +use crate::sources::overlay::DependencyConfusionThreatOverlaySource; use crate::sources::source::Source; use crate::sources::{ReplacedSource, CRATES_IO_REGISTRY}; use crate::util::context::{self, ConfigRelativePath, OptValue}; @@ -24,6 +25,8 @@ pub struct SourceConfigMap<'gctx> { cfgs: HashMap, /// Mapping of [`SourceId`] to the source name. id2name: HashMap, + /// Mapping of sources to local registries that will be overlaid on them. + overlays: HashMap, gctx: &'gctx GlobalContext, } @@ -81,6 +84,18 @@ impl<'gctx> SourceConfigMap<'gctx> { base.add_config(key, value)?; } } + + Ok(base) + } + + /// Like [`SourceConfigMap::new`] but includes sources from source + /// replacement configurations. + pub fn new_with_overlays( + gctx: &'gctx GlobalContext, + overlays: impl IntoIterator, + ) -> CargoResult> { + let mut base = SourceConfigMap::new(gctx)?; + base.overlays = overlays.into_iter().collect(); Ok(base) } @@ -90,6 +105,7 @@ impl<'gctx> SourceConfigMap<'gctx> { let mut base = SourceConfigMap { cfgs: HashMap::new(), id2name: HashMap::new(), + overlays: HashMap::new(), gctx, }; base.add( @@ -136,7 +152,7 @@ impl<'gctx> SourceConfigMap<'gctx> { debug!("loading: {}", id); let Some(mut name) = self.id2name.get(&id) else { - return id.load(self.gctx, yanked_whitelist); + return self.load_overlaid(id, yanked_whitelist); }; let mut cfg_loc = ""; let orig_name = name; @@ -161,7 +177,7 @@ impl<'gctx> SourceConfigMap<'gctx> { name = s; cfg_loc = c; } - None if id == cfg.id => return id.load(self.gctx, yanked_whitelist), + None if id == cfg.id => return self.load_overlaid(id, yanked_whitelist), None => { break cfg.id.with_precise_from(id); } @@ -178,8 +194,8 @@ impl<'gctx> SourceConfigMap<'gctx> { } }; - let new_src = new_id.load( - self.gctx, + let new_src = self.load_overlaid( + new_id, &yanked_whitelist .iter() .map(|p| p.map_source(id, new_id)) @@ -215,6 +231,23 @@ restore the source replacement configuration to continue the build Ok(Box::new(ReplacedSource::new(id, new_id, new_src))) } + /// Gets the [`Source`] for a given [`SourceId`] without performing any source replacement. + fn load_overlaid( + &self, + id: SourceId, + yanked_whitelist: &HashSet, + ) -> CargoResult> { + let src = id.load(self.gctx, yanked_whitelist)?; + if let Some(overlay_id) = self.overlays.get(&id) { + let overlay = overlay_id.load(self.gctx(), yanked_whitelist)?; + Ok(Box::new(DependencyConfusionThreatOverlaySource::new( + overlay, src, + ))) + } else { + Ok(src) + } + } + /// Adds a source config with an associated name. fn add(&mut self, name: &str, cfg: SourceConfig) -> CargoResult<()> { if let Some(old_name) = self.id2name.insert(cfg.id, name.to_string()) { diff --git a/src/cargo/sources/mod.rs b/src/cargo/sources/mod.rs index c487aada522..9c98cc49eaa 100644 --- a/src/cargo/sources/mod.rs +++ b/src/cargo/sources/mod.rs @@ -39,6 +39,7 @@ pub use self::replaced::ReplacedSource; pub mod config; pub mod directory; pub mod git; +pub mod overlay; pub mod path; pub mod registry; pub mod replaced; diff --git a/src/cargo/sources/overlay.rs b/src/cargo/sources/overlay.rs new file mode 100644 index 00000000000..fcdca88df88 --- /dev/null +++ b/src/cargo/sources/overlay.rs @@ -0,0 +1,148 @@ +use std::task::ready; + +use tracing::debug; + +use crate::sources::IndexSummary; + +use super::source::{MaybePackage, Source}; + +/// A `Source` that overlays one source over another, pretending that the packages +/// available in the overlay are actually available in the other one. +/// +/// This is a massive footgun and a terrible idea, so we do not (and never will) +/// expose this publicly. However, it is useful for some very specific private +/// things, like locally verifying a bunch of packages at a time before any of +/// them have been published. +pub struct DependencyConfusionThreatOverlaySource<'gctx> { + // The overlay source. The naming here comes from the main application of this, + // where there is a remote registry that we overlay some local packages on. + local: Box, + // The source we're impersonating. + remote: Box, +} + +impl<'gctx> DependencyConfusionThreatOverlaySource<'gctx> { + pub fn new(local: Box, remote: Box) -> Self { + debug!( + "overlaying {} on {}", + local.source_id().as_url(), + remote.source_id().as_url() + ); + Self { local, remote } + } +} + +impl<'gctx> Source for DependencyConfusionThreatOverlaySource<'gctx> { + fn source_id(&self) -> crate::core::SourceId { + self.remote.source_id() + } + + fn supports_checksums(&self) -> bool { + self.local.supports_checksums() && self.remote.supports_checksums() + } + + fn requires_precise(&self) -> bool { + self.local.requires_precise() || self.remote.requires_precise() + } + + fn query( + &mut self, + dep: &crate::core::Dependency, + kind: super::source::QueryKind, + f: &mut dyn FnMut(super::IndexSummary), + ) -> std::task::Poll> { + let local_source = self.local.source_id(); + let remote_source = self.remote.source_id(); + + let local_dep = dep.clone().map_source(remote_source, local_source); + let mut local_packages = std::collections::HashSet::new(); + let mut local_callback = |index: IndexSummary| { + let index = index.map_summary(|s| s.map_source(local_source, remote_source)); + local_packages.insert(index.as_summary().clone()); + f(index) + }; + ready!(self.local.query(&local_dep, kind, &mut local_callback))?; + + let mut package_collision = None; + let mut remote_callback = |index: IndexSummary| { + if local_packages.contains(index.as_summary()) { + package_collision = Some(index.as_summary().clone()); + } + f(index) + }; + ready!(self.remote.query(dep, kind, &mut remote_callback))?; + + if let Some(collision) = package_collision { + std::task::Poll::Ready(Err(anyhow::anyhow!( + "found a package in the remote registry and the local overlay: {}@{}", + collision.name(), + collision.version() + ))) + } else { + std::task::Poll::Ready(Ok(())) + } + } + + fn invalidate_cache(&mut self) { + self.local.invalidate_cache(); + self.remote.invalidate_cache(); + } + + fn set_quiet(&mut self, quiet: bool) { + self.local.set_quiet(quiet); + self.remote.set_quiet(quiet); + } + + fn download( + &mut self, + package: crate::core::PackageId, + ) -> crate::CargoResult { + let local_source = self.local.source_id(); + let remote_source = self.remote.source_id(); + + self.local + .download(package.map_source(remote_source, local_source)) + .map(|maybe_pkg| match maybe_pkg { + MaybePackage::Ready(pkg) => { + MaybePackage::Ready(pkg.map_source(local_source, remote_source)) + } + x => x, + }) + .or_else(|_| self.remote.download(package)) + } + + fn finish_download( + &mut self, + pkg_id: crate::core::PackageId, + contents: Vec, + ) -> crate::CargoResult { + // The local registry should never return MaybePackage::Download from `download`, so any + // downloads that need to be finished come from the remote registry. + self.remote.finish_download(pkg_id, contents) + } + + fn fingerprint(&self, pkg: &crate::core::Package) -> crate::CargoResult { + Ok(pkg.package_id().version().to_string()) + } + + fn describe(&self) -> String { + self.remote.describe() + } + + fn add_to_yanked_whitelist(&mut self, pkgs: &[crate::core::PackageId]) { + self.local.add_to_yanked_whitelist(pkgs); + self.remote.add_to_yanked_whitelist(pkgs); + } + + fn is_yanked( + &mut self, + pkg: crate::core::PackageId, + ) -> std::task::Poll> { + self.remote.is_yanked(pkg) + } + + fn block_until_ready(&mut self) -> crate::CargoResult<()> { + self.local.block_until_ready()?; + self.remote.block_until_ready() + } +} From 04c963a993e58c946e1f56987daf30f5101e1380 Mon Sep 17 00:00:00 2001 From: Joe Neeman Date: Mon, 10 Jun 2024 14:11:34 -0500 Subject: [PATCH 2/3] Add workspace support for source overlays Adds workspace configuration options (not user-exposed) for overlaying sources. --- crates/xtask-bump-check/src/xtask.rs | 8 ++--- src/cargo/core/registry.rs | 6 ++-- src/cargo/core/workspace.rs | 44 +++++++++++++++++++++++++++- src/cargo/ops/cargo_add/mod.rs | 2 +- src/cargo/ops/cargo_package.rs | 4 +-- src/cargo/ops/cargo_update.rs | 8 ++--- src/cargo/ops/resolve.rs | 4 +-- 7 files changed, 60 insertions(+), 16 deletions(-) diff --git a/crates/xtask-bump-check/src/xtask.rs b/crates/xtask-bump-check/src/xtask.rs index 07148e554f5..e602d5df3a7 100644 --- a/crates/xtask-bump-check/src/xtask.rs +++ b/crates/xtask-bump-check/src/xtask.rs @@ -16,7 +16,6 @@ use std::fs; use std::task; use cargo::core::dependency::Dependency; -use cargo::core::registry::PackageRegistry; use cargo::core::Package; use cargo::core::Registry; use cargo::core::SourceId; @@ -137,7 +136,7 @@ fn bump_check(args: &clap::ArgMatches, gctx: &cargo::util::GlobalContext) -> Car let mut needs_bump = Vec::new(); - check_crates_io(gctx, &changed_members, &mut needs_bump)?; + check_crates_io(&ws, &changed_members, &mut needs_bump)?; if let Some(referenced_commit) = referenced_commit.as_ref() { status(&format!("compare against `{}`", referenced_commit.id()))?; @@ -385,12 +384,13 @@ fn symmetric_diff<'a>( /// /// Assumption: We always release a version larger than all existing versions. fn check_crates_io<'a>( - gctx: &GlobalContext, + ws: &Workspace<'a>, changed_members: &HashMap<&'a str, &'a Package>, needs_bump: &mut Vec<&'a Package>, ) -> CargoResult<()> { + let gctx = ws.gctx(); let source_id = SourceId::crates_io(gctx)?; - let mut registry = PackageRegistry::new(gctx)?; + let mut registry = ws.package_registry()?; let _lock = gctx.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?; registry.lock_patches(); gctx.shell().status( diff --git a/src/cargo/core/registry.rs b/src/cargo/core/registry.rs index 192369c4884..bcbd819905a 100644 --- a/src/cargo/core/registry.rs +++ b/src/cargo/core/registry.rs @@ -194,8 +194,10 @@ pub struct LockedPatchDependency { } impl<'gctx> PackageRegistry<'gctx> { - pub fn new(gctx: &'gctx GlobalContext) -> CargoResult> { - let source_config = SourceConfigMap::new(gctx)?; + pub fn new_with_source_config( + gctx: &'gctx GlobalContext, + source_config: SourceConfigMap<'gctx>, + ) -> CargoResult> { Ok(PackageRegistry { gctx, sources: SourceMap::new(), diff --git a/src/cargo/core/workspace.rs b/src/cargo/core/workspace.rs index 1dc75c881e4..d1999aafc2f 100644 --- a/src/cargo/core/workspace.rs +++ b/src/cargo/core/workspace.rs @@ -20,7 +20,7 @@ use crate::core::{ }; use crate::core::{EitherManifest, Package, SourceId, VirtualManifest}; use crate::ops; -use crate::sources::{PathSource, CRATES_IO_INDEX, CRATES_IO_REGISTRY}; +use crate::sources::{PathSource, SourceConfigMap, CRATES_IO_INDEX, CRATES_IO_REGISTRY}; use crate::util::edit_distance; use crate::util::errors::{CargoResult, ManifestError}; use crate::util::interning::InternedString; @@ -109,6 +109,9 @@ pub struct Workspace<'gctx> { /// Workspace-level custom metadata custom_metadata: Option, + + /// Local overlay configuration. See [`crate::sources::overlay`]. + local_overlays: HashMap, } // Separate structure for tracking loaded packages (to avoid loading anything @@ -237,6 +240,7 @@ impl<'gctx> Workspace<'gctx> { resolve_behavior: ResolveBehavior::V1, resolve_honors_rust_version: false, custom_metadata: None, + local_overlays: HashMap::new(), } } @@ -1674,6 +1678,44 @@ impl<'gctx> Workspace<'gctx> { // Cargo to panic, see issue #10545. self.is_member(&unit.pkg) && !(unit.target.for_host() || unit.pkg.proc_macro()) } + + /// Adds a local package registry overlaying a `SourceId`. + /// + /// See [`crate::sources::overlay::DependencyConfusionThreatOverlaySource`] for why you shouldn't use this. + pub fn add_local_overlay(&mut self, id: SourceId, registry_path: PathBuf) { + self.local_overlays.insert(id, registry_path); + } + + /// Builds a package registry that reflects this workspace configuration. + pub fn package_registry(&self) -> CargoResult> { + let source_config = + SourceConfigMap::new_with_overlays(self.gctx(), self.local_overlays()?)?; + PackageRegistry::new_with_source_config(self.gctx(), source_config) + } + + /// Returns all the configured local overlays, including the ones from our secret environment variable. + fn local_overlays(&self) -> CargoResult> { + let mut ret = self + .local_overlays + .iter() + .map(|(id, path)| Ok((*id, SourceId::for_local_registry(path)?))) + .collect::>>()?; + + if let Ok(overlay) = self + .gctx + .get_env("__CARGO_TEST_DEPENDENCY_CONFUSION_VULNERABILITY_DO_NOT_USE_THIS") + { + let (url, path) = overlay.split_once('=').ok_or(anyhow::anyhow!( + "invalid overlay format. I won't tell you why; you shouldn't be using it anyway" + ))?; + ret.push(( + SourceId::from_url(url)?, + SourceId::for_local_registry(path.as_ref())?, + )); + } + + Ok(ret.into_iter()) + } } impl<'gctx> Packages<'gctx> { diff --git a/src/cargo/ops/cargo_add/mod.rs b/src/cargo/ops/cargo_add/mod.rs index 1781d4c9359..a8fa97afe07 100644 --- a/src/cargo/ops/cargo_add/mod.rs +++ b/src/cargo/ops/cargo_add/mod.rs @@ -78,7 +78,7 @@ pub fn add(workspace: &Workspace<'_>, options: &AddOptions<'_>) -> CargoResult<( ); } - let mut registry = PackageRegistry::new(options.gctx)?; + let mut registry = workspace.package_registry()?; let deps = { let _lock = options diff --git a/src/cargo/ops/cargo_package.rs b/src/cargo/ops/cargo_package.rs index f70dd56a822..c9fc22c7930 100644 --- a/src/cargo/ops/cargo_package.rs +++ b/src/cargo/ops/cargo_package.rs @@ -9,7 +9,7 @@ use std::task::Poll; use crate::core::compiler::{BuildConfig, CompileMode, DefaultExecutor, Executor}; use crate::core::manifest::Target; use crate::core::resolver::CliFeatures; -use crate::core::{registry::PackageRegistry, resolver::HasDevUnits}; +use crate::core::resolver::HasDevUnits; use crate::core::{Feature, PackageIdSpecQuery, Shell, Verbosity, Workspace}; use crate::core::{Package, PackageId, PackageSet, Resolve, SourceId}; use crate::sources::PathSource; @@ -472,7 +472,7 @@ fn build_lock(ws: &Workspace<'_>, publish_pkg: &Package) -> CargoResult let orig_resolve = ops::load_pkg_lockfile(ws)?; let tmp_ws = Workspace::ephemeral(publish_pkg.clone(), ws.gctx(), None, true)?; - let mut tmp_reg = PackageRegistry::new(ws.gctx())?; + let mut tmp_reg = ws.package_registry()?; let mut new_resolve = ops::resolve_with_previous( &mut tmp_reg, &tmp_ws, diff --git a/src/cargo/ops/cargo_update.rs b/src/cargo/ops/cargo_update.rs index 35902aaad55..463af374ede 100644 --- a/src/cargo/ops/cargo_update.rs +++ b/src/cargo/ops/cargo_update.rs @@ -32,7 +32,7 @@ pub struct UpdateOptions<'a> { } pub fn generate_lockfile(ws: &Workspace<'_>) -> CargoResult<()> { - let mut registry = PackageRegistry::new(ws.gctx())?; + let mut registry = ws.package_registry()?; let previous_resolve = None; let mut resolve = ops::resolve_with_previous( &mut registry, @@ -73,7 +73,7 @@ pub fn update_lockfile(ws: &Workspace<'_>, opts: &UpdateOptions<'_>) -> CargoRes // Precise option specified, so calculate a previous_resolve required // by precise package update later. Some(_) => { - let mut registry = PackageRegistry::new(opts.gctx)?; + let mut registry = ws.package_registry()?; ops::resolve_with_previous( &mut registry, ws, @@ -88,7 +88,7 @@ pub fn update_lockfile(ws: &Workspace<'_>, opts: &UpdateOptions<'_>) -> CargoRes } } }; - let mut registry = PackageRegistry::new(opts.gctx)?; + let mut registry = ws.package_registry()?; let mut to_avoid = HashSet::new(); if opts.to_update.is_empty() { @@ -226,7 +226,7 @@ pub fn upgrade_manifests( // that we're synchronized against other Cargos. let _lock = gctx.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?; - let mut registry = PackageRegistry::new(gctx)?; + let mut registry = ws.package_registry()?; registry.lock_patches(); for member in ws.members_mut().sorted() { diff --git a/src/cargo/ops/resolve.rs b/src/cargo/ops/resolve.rs index f0274da6412..bfa10496d46 100644 --- a/src/cargo/ops/resolve.rs +++ b/src/cargo/ops/resolve.rs @@ -116,7 +116,7 @@ version. This may also occur with an optional dependency that is not enabled."; /// This is a simple interface used by commands like `clean`, `fetch`, and /// `package`, which don't specify any options or features. pub fn resolve_ws<'a>(ws: &Workspace<'a>, dry_run: bool) -> CargoResult<(PackageSet<'a>, Resolve)> { - let mut registry = PackageRegistry::new(ws.gctx())?; + let mut registry = ws.package_registry()?; let resolve = resolve_with_registry(ws, &mut registry, dry_run)?; let packages = get_resolved_packages(&resolve, registry)?; Ok((packages, resolve)) @@ -142,7 +142,7 @@ pub fn resolve_ws_with_opts<'gctx>( force_all_targets: ForceAllTargets, dry_run: bool, ) -> CargoResult> { - let mut registry = PackageRegistry::new(ws.gctx())?; + let mut registry = ws.package_registry()?; let (resolve, resolved_with_overrides) = if ws.ignore_lock() { let add_patches = true; let resolve = None; From ba9dd1ea2e6859a3a931a24a0908c4f8e8b3d9f0 Mon Sep 17 00:00:00 2001 From: Joe Neeman Date: Mon, 10 Jun 2024 14:12:05 -0500 Subject: [PATCH 3/3] Adds tests for source overlays. --- crates/cargo-test-support/src/lib.rs | 11 + crates/cargo-test-support/src/registry.rs | 7 +- tests/testsuite/main.rs | 1 + tests/testsuite/registry_overlay.rs | 308 ++++++++++++++++++++++ 4 files changed, 326 insertions(+), 1 deletion(-) create mode 100644 tests/testsuite/registry_overlay.rs diff --git a/crates/cargo-test-support/src/lib.rs b/crates/cargo-test-support/src/lib.rs index 751a0a7d9b5..09644298316 100644 --- a/crates/cargo-test-support/src/lib.rs +++ b/crates/cargo-test-support/src/lib.rs @@ -877,6 +877,17 @@ impl Execs { self } + pub fn overlay_registry(&mut self, url: &Url, path: &str) -> &mut Self { + if let Some(ref mut p) = self.process_builder { + let env_value = format!("{}={}", url, path); + p.env( + "__CARGO_TEST_DEPENDENCY_CONFUSION_VULNERABILITY_DO_NOT_USE_THIS", + env_value, + ); + } + self + } + pub fn enable_split_debuginfo_packed(&mut self) -> &mut Self { self.env("CARGO_PROFILE_DEV_SPLIT_DEBUGINFO", "packed") .env("CARGO_PROFILE_TEST_SPLIT_DEBUGINFO", "packed") diff --git a/crates/cargo-test-support/src/registry.rs b/crates/cargo-test-support/src/registry.rs index f8b4b144782..6ddd59e38a8 100644 --- a/crates/cargo-test-support/src/registry.rs +++ b/crates/cargo-test-support/src/registry.rs @@ -1649,7 +1649,12 @@ impl Package { /// Returns the path to the compressed package file. pub fn archive_dst(&self) -> PathBuf { if self.local { - registry_path().join(format!("{}-{}.crate", self.name, self.vers)) + let path = if self.alternative { + alt_registry_path() + } else { + registry_path() + }; + path.join(format!("{}-{}.crate", self.name, self.vers)) } else if self.alternative { alt_dl_path() .join(&self.name) diff --git a/tests/testsuite/main.rs b/tests/testsuite/main.rs index a335ce6bef2..f27f2aae158 100644 --- a/tests/testsuite/main.rs +++ b/tests/testsuite/main.rs @@ -152,6 +152,7 @@ mod publish_lockfile; mod read_manifest; mod registry; mod registry_auth; +mod registry_overlay; mod rename_deps; mod replace; mod required_features; diff --git a/tests/testsuite/registry_overlay.rs b/tests/testsuite/registry_overlay.rs new file mode 100644 index 00000000000..20bb0a1e73f --- /dev/null +++ b/tests/testsuite/registry_overlay.rs @@ -0,0 +1,308 @@ +//! Tests for local-registry sources. + +use cargo_test_support::project; +use cargo_test_support::registry::{Package, RegistryBuilder, TestRegistry}; + +fn setup() -> (TestRegistry, String) { + let alt = RegistryBuilder::new().alternative().build(); + ( + RegistryBuilder::new().http_index().build(), + alt.index_url() + .to_file_path() + .unwrap() + .into_os_string() + .into_string() + .unwrap(), + ) +} + +#[cargo_test] +fn overlay_hit() { + let (reg, alt_path) = setup(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + baz = "0.1.0" + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + + // baz is only in the local registry, but it gets found + Package::new("baz", "0.1.1") + .alternative(true) + .local(true) + .publish(); + + p.cargo("check") + .overlay_registry(®.index_url(), &alt_path) + .run(); +} + +#[cargo_test] +fn registry_version_wins() { + let (reg, alt_path) = setup(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + baz = "0.1.0" + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + + // The latest one is in the main registry, so it will get chosen. + Package::new("baz", "0.1.1").publish(); + Package::new("baz", "0.1.0") + .alternative(true) + .local(true) + .publish(); + + p.cargo("check") + .overlay_registry(®.index_url(), &alt_path) + .with_stderr_data( + "\ +[UPDATING] [..] +[LOCKING] 2 packages to latest compatible versions +[DOWNLOADING] crates ... +[DOWNLOADED] baz v0.1.1 (registry [..]) +[CHECKING] baz v0.1.1 +[CHECKING] foo v0.0.1 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s +", + ) + .run(); +} + +#[cargo_test] +fn overlay_version_wins() { + let (reg, alt_path) = setup(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + baz = "0.1.0" + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + + // The latest one is in the overlay registry, so it will get chosen. + Package::new("baz", "0.1.0").publish(); + Package::new("baz", "0.1.1") + .alternative(true) + .local(true) + .publish(); + + p.cargo("check") + .overlay_registry(®.index_url(), &alt_path) + .with_stderr_data( + "\ +[UPDATING] [..] +[LOCKING] 2 packages to latest compatible versions +[UNPACKING] baz v0.1.1 (registry [..]) +[CHECKING] baz v0.1.1 +[CHECKING] foo v0.0.1 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s +", + ) + .run(); +} + +#[cargo_test] +fn version_collision() { + let (reg, alt_path) = setup(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + baz = "0.1.0" + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + + // The one we want is in the main registry. + Package::new("baz", "0.1.1").publish(); + Package::new("baz", "0.1.1") + .alternative(true) + .local(true) + .publish(); + + p.cargo("check") + .overlay_registry(®.index_url(), &alt_path) + .with_status(101) + .with_stderr_data( + "\ +[UPDATING] [..] +[ERROR] failed to get `baz` [..] + +Caused by: + failed to query replaced source registry `crates-io` + +Caused by: + found a package in the remote registry and the local overlay: baz@0.1.1 +", + ) + .run(); +} + +#[cargo_test] +fn local_depends_on_old_registry_package() { + let (reg, alt_path) = setup(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + baz = "0.1.0" + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + + Package::new("baz", "0.0.1").publish(); + // A new local package can depend on an older version in the registry. + Package::new("baz", "0.1.1") + .dep("baz", "=0.0.1") + .alternative(true) + .local(true) + .publish(); + + p.cargo("check") + .overlay_registry(®.index_url(), &alt_path) + .run(); +} + +#[cargo_test] +fn registry_dep_depends_on_new_local_package() { + let (reg, alt_path) = setup(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + registry-package = "0.1.0" + workspace-package = "0.0.1" + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + + Package::new("registry-package", "0.1.0") + .dep("workspace-package", "0.1.0") + .publish(); + // The local overlay contains an updated version of workspace-package + Package::new("workspace-package", "0.1.1") + .alternative(true) + .local(true) + .publish(); + + // The registry contains older versions of workspace-package (one of which + // we depend on directly). + Package::new("workspace-package", "0.1.0").publish(); + Package::new("workspace-package", "0.0.1").publish(); + + p.cargo("check") + .overlay_registry(®.index_url(), &alt_path) + .with_stderr_data( + "\ +[UPDATING] [..] +[LOCKING] 4 packages to latest compatible versions +[ADDING] workspace-package v0.0.1 (latest: v0.1.1) +[DOWNLOADING] crates ... +[UNPACKING] [..] +[DOWNLOADED] [..] +[DOWNLOADED] [..] +[CHECKING] workspace-package v0.1.1 +[CHECKING] workspace-package v0.0.1 +[CHECKING] registry-package v0.1.0 +[CHECKING] foo v0.0.1 [..] +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s +", + ) + .run(); +} + +// Test that we can overlay on top of alternate registries, not just crates-io. +// Since the test framework only supports a single alternate registry, we repurpose +// the dummy crates-io as the registry to overlay on top. +#[cargo_test] +fn alt_registry() { + let alt = RegistryBuilder::new().http_index().alternative().build(); + let crates_io = RegistryBuilder::new().build(); + let crates_io_path = crates_io + .index_url() + .to_file_path() + .unwrap() + .into_os_string() + .into_string() + .unwrap(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + baz = { version = "0.1.0", registry = "alternative" } + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + + // This package isn't used, but publishing it forces the creation of the registry index. + Package::new("bar", "0.0.1").local(true).publish(); + Package::new("baz", "0.1.1").alternative(true).publish(); + + p.cargo("check") + .overlay_registry(&alt.index_url(), &crates_io_path) + .run(); +}