diff --git a/src/cargo/core/dependency.rs b/src/cargo/core/dependency.rs index c748ee9c511..2d8b4d03853 100644 --- a/src/cargo/core/dependency.rs +++ b/src/cargo/core/dependency.rs @@ -258,6 +258,12 @@ impl Dependency { .set_source_id(id.source_id().clone()) } + /// Returns whether this is a "locked" dependency, basically whether it has + /// an exact version req. + pub fn is_locked(&self) -> bool { + // Kind of a hack to figure this out, but it works! + self.inner.req.to_string().starts_with("=") + } /// Returns false if the dependency is only used to build the local package. pub fn is_transitive(&self) -> bool { @@ -292,6 +298,12 @@ impl Dependency { self.matches_id(sum.package_id()) } + /// Returns true if the package (`sum`) can fulfill this dependency request. + pub fn matches_ignoring_source(&self, sum: &Summary) -> bool { + self.name() == sum.package_id().name() && + self.version_req().matches(sum.package_id().version()) + } + /// Returns true if the package (`id`) can fulfill this dependency request. pub fn matches_id(&self, id: &PackageId) -> bool { self.inner.name == id.name() && diff --git a/src/cargo/core/manifest.rs b/src/cargo/core/manifest.rs index 14087106492..662a5cbe4ce 100644 --- a/src/cargo/core/manifest.rs +++ b/src/cargo/core/manifest.rs @@ -5,6 +5,7 @@ use std::rc::Rc; use semver::Version; use serde::ser; +use url::Url; use core::{Dependency, PackageId, Summary, SourceId, PackageIdSpec}; use core::WorkspaceConfig; @@ -28,6 +29,7 @@ pub struct Manifest { profiles: Profiles, publish: bool, replace: Vec<(PackageIdSpec, Dependency)>, + patch: HashMap>, workspace: WorkspaceConfig, original: Rc, } @@ -35,6 +37,7 @@ pub struct Manifest { #[derive(Clone, Debug)] pub struct VirtualManifest { replace: Vec<(PackageIdSpec, Dependency)>, + patch: HashMap>, workspace: WorkspaceConfig, profiles: Profiles, } @@ -225,6 +228,7 @@ impl Manifest { profiles: Profiles, publish: bool, replace: Vec<(PackageIdSpec, Dependency)>, + patch: HashMap>, workspace: WorkspaceConfig, original: Rc) -> Manifest { Manifest { @@ -238,6 +242,7 @@ impl Manifest { profiles: profiles, publish: publish, replace: replace, + patch: patch, workspace: workspace, original: original, } @@ -257,6 +262,7 @@ impl Manifest { pub fn publish(&self) -> bool { self.publish } pub fn replace(&self) -> &[(PackageIdSpec, Dependency)] { &self.replace } pub fn original(&self) -> &TomlManifest { &self.original } + pub fn patch(&self) -> &HashMap> { &self.patch } pub fn links(&self) -> Option<&str> { self.links.as_ref().map(|s| &s[..]) } @@ -284,10 +290,12 @@ impl Manifest { impl VirtualManifest { pub fn new(replace: Vec<(PackageIdSpec, Dependency)>, + patch: HashMap>, workspace: WorkspaceConfig, profiles: Profiles) -> VirtualManifest { VirtualManifest { replace: replace, + patch: patch, workspace: workspace, profiles: profiles, } @@ -297,6 +305,10 @@ impl VirtualManifest { &self.replace } + pub fn patch(&self) -> &HashMap> { + &self.patch + } + pub fn workspace_config(&self) -> &WorkspaceConfig { &self.workspace } diff --git a/src/cargo/core/registry.rs b/src/cargo/core/registry.rs index 25524fbbf3d..1edaa305f06 100644 --- a/src/cargo/core/registry.rs +++ b/src/cargo/core/registry.rs @@ -1,5 +1,8 @@ use std::collections::HashMap; +use semver::VersionReq; +use url::Url; + use core::{Source, SourceId, SourceMap, Summary, Dependency, PackageId}; use core::PackageSet; use util::{Config, profile}; @@ -77,6 +80,7 @@ pub struct PackageRegistry<'cfg> { locked: LockedMap, source_config: SourceConfigMap<'cfg>, + patches: HashMap>, } type LockedMap = HashMap)>>>; @@ -97,6 +101,7 @@ impl<'cfg> PackageRegistry<'cfg> { overrides: Vec::new(), source_config: source_config, locked: HashMap::new(), + patches: HashMap::new(), }) } @@ -175,6 +180,39 @@ impl<'cfg> PackageRegistry<'cfg> { sub_vec.push((id, deps)); } + pub fn patch(&mut self, url: &Url, deps: &[Dependency]) -> CargoResult<()> { + let deps = deps.iter().map(|dep| { + let mut summaries = self.query_vec(dep)?.into_iter(); + let summary = match summaries.next() { + Some(summary) => summary, + None => { + bail!("patch for `{}` in `{}` did not resolve to any crates", + dep.name(), url) + } + }; + if summaries.next().is_some() { + bail!("patch for `{}` in `{}` resolved to more than one candidate", + dep.name(), url) + } + if summary.package_id().source_id().url() == url { + bail!("patch for `{}` in `{}` points to the same source, but \ + patches must point to different sources", + dep.name(), url); + } + Ok(summary) + }).collect::>>().chain_err(|| { + format!("failed to resolve patches for `{}`", url) + })?; + + self.patches.insert(url.clone(), deps); + + Ok(()) + } + + pub fn patches(&self) -> &HashMap> { + &self.patches + } + fn load(&mut self, source_id: &SourceId, kind: Kind) -> CargoResult<()> { (|| { let source = self.source_config.load(source_id)?; @@ -222,7 +260,7 @@ impl<'cfg> PackageRegistry<'cfg> { /// possible. If we're unable to map a dependency though, we just pass it on /// through. pub fn lock(&self, summary: Summary) -> Summary { - lock(&self.locked, summary) + lock(&self.locked, &self.patches, summary) } fn warn_bad_override(&self, @@ -274,39 +312,97 @@ impl<'cfg> Registry for PackageRegistry<'cfg> { fn query(&mut self, dep: &Dependency, f: &mut FnMut(Summary)) -> CargoResult<()> { - // Ensure the requested source_id is loaded - self.ensure_loaded(dep.source_id(), Kind::Normal).chain_err(|| { - format!("failed to load source for a dependency \ - on `{}`", dep.name()) - })?; - - let (override_summary, n, to_warn) = { // Look for an override and get ready to query the real source. let override_summary = self.query_overrides(&dep)?; - let source = self.sources.get_mut(dep.source_id()); - match (override_summary, source) { - (Some(_), None) => bail!("override found but no real ones"), - (None, None) => return Ok(()), - - // If we don't have an override then we just ship everything - // upstairs after locking the summary - (None, Some(source)) => { - let locked = &self.locked; - return source.query(dep, &mut |summary| f(lock(locked, summary))) + + // Next up on our list of candidates is to check the `[patch]` + // section of the manifest. Here we look through all patches + // relevant to the source that `dep` points to, and then we match + // name/version. Note that we don't use `dep.matches(..)` because + // the patches, by definition, come from a different source. + // This means that `dep.matches(..)` will always return false, when + // what we really care about is the name/version match. + let mut patches = Vec::::new(); + if let Some(extra) = self.patches.get(dep.source_id().url()) { + patches.extend(extra.iter().filter(|s| { + dep.matches_ignoring_source(s) + }).cloned()); + } + + // A crucial feature of the `[patch]` feature is that we *don't* + // query the actual registry if we have a "locked" dependency. A + // locked dep basically just means a version constraint of `=a.b.c`, + // and because patches take priority over the actual source then if + // we have a candidate we're done. + if patches.len() == 1 && dep.is_locked() { + let patch = patches.remove(0); + match override_summary { + Some(summary) => (summary, 1, Some(patch)), + None => { + f(patch); + return Ok(()) + } } + } else { + if patches.len() > 0 { + debug!("found {} patches with an unlocked dep, \ + looking at sources", patches.len()); + } + + // Ensure the requested source_id is loaded + self.ensure_loaded(dep.source_id(), Kind::Normal).chain_err(|| { + format!("failed to load source for a dependency \ + on `{}`", dep.name()) + })?; + + let source = self.sources.get_mut(dep.source_id()); + match (override_summary, source) { + (Some(_), None) => bail!("override found but no real ones"), + (None, None) => return Ok(()), + + // If we don't have an override then we just ship + // everything upstairs after locking the summary + (None, Some(source)) => { + for patch in patches.iter() { + f(patch.clone()); + } + + // Our sources shouldn't ever come back to us with two + // summaries that have the same version. We could, + // however, have an `[patch]` section which is in use + // to override a version in the registry. This means + // that if our `summary` in this loop has the same + // version as something in `patches` that we've + // already selected, then we skip this `summary`. + let locked = &self.locked; + let all_patches = &self.patches; + return source.query(dep, &mut |summary| { + for patch in patches.iter() { + let patch = patch.package_id().version(); + if summary.package_id().version() == patch { + return + } + } + f(lock(locked, all_patches, summary)) + }) + } - // If we have an override summary then we query the source to sanity - // check its results. We don't actually use any of the summaries it - // gives us though. - (Some(override_summary), Some(source)) => { - let mut n = 0; - let mut to_warn = None; - source.query(dep, &mut |summary| { - n += 1; - to_warn = Some(summary); - })?; - (override_summary, n, to_warn) + // If we have an override summary then we query the source + // to sanity check its results. We don't actually use any of + // the summaries it gives us though. + (Some(override_summary), Some(source)) => { + if patches.len() > 0 { + bail!("found patches and a path override") + } + let mut n = 0; + let mut to_warn = None; + source.query(dep, &mut |summary| { + n += 1; + to_warn = Some(summary); + })?; + (override_summary, n, to_warn) + } } } }; @@ -321,7 +417,9 @@ impl<'cfg> Registry for PackageRegistry<'cfg> { } } -fn lock(locked: &LockedMap, summary: Summary) -> Summary { +fn lock(locked: &LockedMap, + patches: &HashMap>, + summary: Summary) -> Summary { let pair = locked.get(summary.source_id()).and_then(|map| { map.get(summary.name()) }).and_then(|vec| { @@ -335,7 +433,7 @@ fn lock(locked: &LockedMap, summary: Summary) -> Summary { Some(&(ref precise, _)) => summary.override_id(precise.clone()), None => summary, }; - summary.map_dependencies(|mut dep| { + summary.map_dependencies(|dep| { trace!("\t{}/{}/{}", dep.name(), dep.version_req(), dep.source_id()); @@ -362,6 +460,7 @@ fn lock(locked: &LockedMap, summary: Summary) -> Summary { let locked = locked_deps.iter().find(|id| dep.matches_id(id)); if let Some(locked) = locked { trace!("\tfirst hit on {}", locked); + let mut dep = dep.clone(); dep.lock_to(locked); return dep } @@ -375,17 +474,43 @@ fn lock(locked: &LockedMap, summary: Summary) -> Summary { }).and_then(|vec| { vec.iter().find(|&&(ref id, _)| dep.matches_id(id)) }); - match v { - Some(&(ref id, _)) => { - trace!("\tsecond hit on {}", id); - dep.lock_to(id); + if let Some(&(ref id, _)) = v { + trace!("\tsecond hit on {}", id); + let mut dep = dep.clone(); + dep.lock_to(id); + return dep + } + + // Finally we check to see if any registered patches correspond to + // this dependency. + let v = patches.get(dep.source_id().url()).map(|vec| { + let dep2 = dep.clone(); + let mut iter = vec.iter().filter(move |s| { + dep2.name() == s.package_id().name() && + dep2.version_req().matches(s.package_id().version()) + }); + (iter.next(), iter) + }); + if let Some((Some(summary), mut remaining)) = v { + assert!(remaining.next().is_none()); + let patch_source = summary.package_id().source_id(); + let patch_locked = locked.get(patch_source).and_then(|m| { + m.get(summary.package_id().name()) + }).map(|list| { + list.iter().any(|&(ref id, _)| id == summary.package_id()) + }).unwrap_or(false); + + if patch_locked { + trace!("\tthird hit on {}", summary.package_id()); + let req = VersionReq::exact(summary.package_id().version()); + let mut dep = dep.clone(); + dep.set_version_req(req); return dep } - None => { - trace!("\tremaining unlocked"); - dep - } } + + trace!("\tnope, unlocked"); + return dep }) } diff --git a/src/cargo/core/resolver/encode.rs b/src/cargo/core/resolver/encode.rs index 3a93c204706..4ca01c49a31 100644 --- a/src/cargo/core/resolver/encode.rs +++ b/src/cargo/core/resolver/encode.rs @@ -17,6 +17,14 @@ pub struct EncodableResolve { /// `root` is optional to allow forward compatibility. root: Option, metadata: Option, + + #[serde(default, skip_serializing_if = "Patch::is_empty")] + patch: Patch, +} + +#[derive(Serialize, Deserialize, Debug, Default)] +struct Patch { + unused: Vec, } pub type Metadata = BTreeMap; @@ -153,6 +161,15 @@ impl EncodableResolve { metadata.remove(&k); } + let mut unused_patches = Vec::new(); + for pkg in self.patch.unused { + let id = match pkg.source.as_ref().or(path_deps.get(&pkg.name)) { + Some(src) => PackageId::new(&pkg.name, &pkg.version, src)?, + None => continue, + }; + unused_patches.push(id); + } + Ok(Resolve { graph: g, empty_features: HashSet::new(), @@ -160,6 +177,7 @@ impl EncodableResolve { replacements: replacements, checksums: checksums, metadata: metadata, + unused_patches: unused_patches, }) } } @@ -190,10 +208,12 @@ fn build_path_deps(ws: &Workspace) -> HashMap { config: &Config, ret: &mut HashMap, visited: &mut HashSet) { - let replace = pkg.manifest().replace(); + let replace = pkg.manifest().replace().iter().map(|p| &p.1); + let patch = pkg.manifest().patch().values().flat_map(|v| v); let deps = pkg.dependencies() .iter() - .chain(replace.iter().map(|p| &p.1)) + .chain(replace) + .chain(patch) .map(|d| d.source_id()) .filter(|id| !visited.contains(id) && id.is_path()) .filter_map(|id| id.url().to_file_path().ok()) @@ -209,6 +229,12 @@ fn build_path_deps(ws: &Workspace) -> HashMap { } } +impl Patch { + fn is_empty(&self) -> bool { + self.unused.is_empty() + } +} + #[derive(Serialize, Deserialize, Debug, PartialOrd, Ord, PartialEq, Eq)] pub struct EncodableDependency { name: String, @@ -325,10 +351,23 @@ impl<'a, 'cfg> ser::Serialize for WorkspaceResolve<'a, 'cfg> { Some(root) if self.use_root_key => Some(encodable_resolve_node(&root, self.resolve)), _ => None, }; + + let patch = Patch { + unused: self.resolve.unused_patches().iter().map(|id| { + EncodableDependency { + name: id.name().to_string(), + version: id.version().to_string(), + source: encode_source(id.source_id()), + dependencies: None, + replace: None, + } + }).collect(), + }; EncodableResolve { package: Some(encodable), root: root, metadata: metadata, + patch: patch, }.serialize(s) } } @@ -349,30 +388,27 @@ fn encodable_resolve_node(id: &PackageId, resolve: &Resolve) } }; - let source = if id.source_id().is_path() { - None - } else { - Some(id.source_id().clone()) - }; - EncodableDependency { name: id.name().to_string(), version: id.version().to_string(), - source: source, + source: encode_source(id.source_id()), dependencies: deps, replace: replace, } } fn encodable_package_id(id: &PackageId) -> EncodablePackageId { - let source = if id.source_id().is_path() { - None - } else { - Some(id.source_id().with_precise(None)) - }; EncodablePackageId { name: id.name().to_string(), version: id.version().to_string(), - source: source, + source: encode_source(id.source_id()).map(|s| s.with_precise(None)), + } +} + +fn encode_source(id: &SourceId) -> Option { + if id.is_path() { + None + } else { + Some(id.clone()) } } diff --git a/src/cargo/core/resolver/mod.rs b/src/cargo/core/resolver/mod.rs index fba3384c284..145ef6ea6cb 100644 --- a/src/cargo/core/resolver/mod.rs +++ b/src/cargo/core/resolver/mod.rs @@ -53,6 +53,7 @@ use std::ops::Range; use std::rc::Rc; use semver; +use url::Url; use core::{PackageId, Registry, SourceId, Summary, Dependency}; use core::PackageIdSpec; @@ -78,6 +79,7 @@ pub struct Resolve { features: HashMap>, checksums: HashMap>, metadata: Metadata, + unused_patches: Vec, } pub struct Deps<'a> { @@ -111,6 +113,16 @@ struct Candidate { } impl Resolve { + pub fn register_used_patches(&mut self, + patches: &HashMap>) { + for summary in patches.values().flat_map(|v| v) { + if self.iter().any(|id| id == summary.package_id()) { + continue + } + self.unused_patches.push(summary.package_id().clone()); + } + } + pub fn merge_from(&mut self, previous: &Resolve) -> CargoResult<()> { // Given a previous instance of resolve, it should be forbidden to ever // have a checksums which *differ*. If the same package id has differing @@ -224,6 +236,10 @@ unable to verify that `{0}` is the same as when the lockfile was generated pub fn query(&self, spec: &str) -> CargoResult<&PackageId> { PackageIdSpec::query_str(spec, self.iter()) } + + pub fn unused_patches(&self) -> &[PackageId] { + &self.unused_patches + } } impl fmt::Debug for Resolve { @@ -341,6 +357,7 @@ pub fn resolve(summaries: &[(Summary, Method)], features: cx.resolve_features.iter().map(|(k, v)| { (k.clone(), v.clone()) }).collect(), + unused_patches: Vec::new(), }; for summary in cx.activations.values() diff --git a/src/cargo/core/workspace.rs b/src/cargo/core/workspace.rs index a953f72c4d2..5f93baa856b 100644 --- a/src/cargo/core/workspace.rs +++ b/src/cargo/core/workspace.rs @@ -4,6 +4,7 @@ use std::path::{Path, PathBuf}; use std::slice; use glob::glob; +use url::Url; use core::{Package, VirtualManifest, EitherManifest, SourceId}; use core::{PackageIdSpec, Dependency, Profile, Profiles}; @@ -222,6 +223,20 @@ impl<'cfg> Workspace<'cfg> { } } + /// Returns the root [patch] section of this workspace. + /// + /// This may be from a virtual crate or an actual crate. + pub fn root_patch(&self) -> &HashMap> { + let path = match self.root_manifest { + Some(ref p) => p, + None => &self.current_manifest, + }; + match *self.packages.get(path) { + MaybePackage::Package(ref p) => p.manifest().patch(), + MaybePackage::Virtual(ref v) => v.patch(), + } + } + /// Returns an iterator over all packages in this workspace pub fn members<'a>(&'a self) -> Members<'a, 'cfg> { Members { diff --git a/src/cargo/ops/lockfile.rs b/src/cargo/ops/lockfile.rs index 1138be745e1..53374f85182 100644 --- a/src/cargo/ops/lockfile.rs +++ b/src/cargo/ops/lockfile.rs @@ -72,6 +72,15 @@ pub fn write_pkg_lockfile(ws: &Workspace, resolve: &Resolve) -> CargoResult<()> emit_package(dep, &mut out); } + if let Some(patch) = toml.get("patch") { + let list = patch["unused"].as_array().unwrap(); + for entry in list { + out.push_str("[[patch.unused]]\n"); + emit_package(entry.as_table().unwrap(), &mut out); + out.push_str("\n"); + } + } + if let Some(meta) = toml.get("metadata") { out.push_str("[metadata]\n"); out.push_str(&meta.to_string()); diff --git a/src/cargo/ops/registry.rs b/src/cargo/ops/registry.rs index 42ecdf6bf08..ddbf99896b1 100644 --- a/src/cargo/ops/registry.rs +++ b/src/cargo/ops/registry.rs @@ -44,10 +44,13 @@ pub fn publish(ws: &Workspace, opts: &PublishOpts) -> CargoResult<()> { bail!("some crates cannot be published.\n\ `{}` is marked as unpublishable", pkg.name()); } + if pkg.manifest().patch().len() > 0 { + bail!("published crates cannot contain [patch] sections"); + } let (mut registry, reg_id) = registry(opts.config, - opts.token.clone(), - opts.index.clone())?; + opts.token.clone(), + opts.index.clone())?; verify_dependencies(pkg, ®_id)?; // Prepare a tarball, with a non-surpressable warning if metadata diff --git a/src/cargo/ops/resolve.rs b/src/cargo/ops/resolve.rs index 8a84071b42a..d4387e49a38 100644 --- a/src/cargo/ops/resolve.rs +++ b/src/cargo/ops/resolve.rs @@ -130,6 +130,13 @@ pub fn resolve_with_previous<'a>(registry: &mut PackageRegistry, .filter(|s| !s.is_registry())); } + let ref keep = |p: &&'a PackageId| { + !to_avoid_sources.contains(&p.source_id()) && match to_avoid { + Some(set) => !set.contains(p), + None => true, + } + }; + // In the case where a previous instance of resolve is available, we // want to lock as many packages as possible to the previous version // without disturbing the graph structure. To this end we perform @@ -153,14 +160,37 @@ pub fn resolve_with_previous<'a>(registry: &mut PackageRegistry, // still matches the locked version. if let Some(r) = previous { trace!("previous: {:?}", r); - for node in r.iter().filter(|p| keep(p, to_avoid, &to_avoid_sources)) { + for node in r.iter().filter(keep) { let deps = r.deps_not_replaced(node) - .filter(|p| keep(p, to_avoid, &to_avoid_sources)) + .filter(keep) .cloned().collect(); registry.register_lock(node.clone(), deps); } } + for (url, patches) in ws.root_patch() { + let previous = match previous { + Some(r) => r, + None => { + registry.patch(url, patches)?; + continue + } + }; + let patches = patches.iter().map(|dep| { + let unused = previous.unused_patches(); + let candidates = previous.iter().chain(unused); + match candidates.filter(keep).find(|id| dep.matches_id(id)) { + Some(id) => { + let mut dep = dep.clone(); + dep.lock_to(id); + dep + } + None => dep.clone(), + } + }).collect::>(); + registry.patch(url, &patches)?; + } + let mut summaries = Vec::new(); for member in ws.members() { registry.add_sources(&[member.package_id().source_id().clone()])?; @@ -214,9 +244,7 @@ pub fn resolve_with_previous<'a>(registry: &mut PackageRegistry, Some(r) => { root_replace.iter().map(|&(ref spec, ref dep)| { for (key, val) in r.replacements().iter() { - if spec.matches(key) && - dep.matches_id(val) && - keep(&val, to_avoid, &to_avoid_sources) { + if spec.matches(key) && dep.matches_id(val) && keep(&val) { let mut dep = dep.clone(); dep.lock_to(val); return (spec.clone(), dep) @@ -228,21 +256,14 @@ pub fn resolve_with_previous<'a>(registry: &mut PackageRegistry, None => root_replace.to_vec(), }; - let mut resolved = resolver::resolve(&summaries, &replace, registry)?; + let mut resolved = resolver::resolve(&summaries, + &replace, + registry)?; + resolved.register_used_patches(registry.patches()); if let Some(previous) = previous { resolved.merge_from(previous)?; } return Ok(resolved); - - fn keep<'a>(p: &&'a PackageId, - to_avoid_packages: Option<&HashSet<&'a PackageId>>, - to_avoid_sources: &HashSet<&'a SourceId>) - -> bool { - !to_avoid_sources.contains(&p.source_id()) && match to_avoid_packages { - Some(set) => !set.contains(p), - None => true, - } - } } /// Read the `paths` configuration variable to discover all path overrides that diff --git a/src/cargo/util/toml/mod.rs b/src/cargo/util/toml/mod.rs index 78b86ec91f6..84eb68f6514 100644 --- a/src/cargo/util/toml/mod.rs +++ b/src/cargo/util/toml/mod.rs @@ -5,11 +5,12 @@ use std::path::{Path, PathBuf}; use std::rc::Rc; use std::str; -use toml; use semver::{self, VersionReq}; use serde::ser; use serde::de::{self, Deserialize}; use serde_ignored; +use toml; +use url::Url; use core::{SourceId, Profiles, PackageIdSpec, GitReference, WorkspaceConfig}; use core::{Summary, Manifest, Target, Dependency, PackageId}; @@ -218,6 +219,7 @@ pub struct TomlManifest { features: Option>>, target: Option>, replace: Option>, + patch: Option>>, workspace: Option, badges: Option>>, } @@ -463,6 +465,7 @@ impl TomlManifest { }).collect() }), replace: None, + patch: None, workspace: None, badges: self.badges.clone(), }; @@ -530,6 +533,7 @@ impl TomlManifest { let mut deps = Vec::new(); let replace; + let patch; { @@ -585,6 +589,7 @@ impl TomlManifest { } replace = me.replace(&mut cx)?; + patch = me.patch(&mut cx)?; } { @@ -646,6 +651,7 @@ impl TomlManifest { profiles, publish, replace, + patch, workspace_config, me.clone()); if project.license_file.is_some() && project.license.is_some() { @@ -689,16 +695,19 @@ impl TomlManifest { let mut nested_paths = Vec::new(); let mut warnings = Vec::new(); let mut deps = Vec::new(); - let replace = me.replace(&mut Context { - pkgid: None, - deps: &mut deps, - source_id: source_id, - nested_paths: &mut nested_paths, - config: config, - warnings: &mut warnings, - platform: None, - root: root - })?; + let (replace, patch) = { + let mut cx = Context { + pkgid: None, + deps: &mut deps, + source_id: source_id, + nested_paths: &mut nested_paths, + config: config, + warnings: &mut warnings, + platform: None, + root: root + }; + (me.replace(&mut cx)?, me.patch(&mut cx)?) + }; let profiles = build_profiles(&me.profile); let workspace_config = match me.workspace { Some(ref config) => { @@ -711,11 +720,14 @@ impl TomlManifest { bail!("virtual manifests must be configured with [workspace]"); } }; - Ok((VirtualManifest::new(replace, workspace_config, profiles), nested_paths)) + Ok((VirtualManifest::new(replace, patch, workspace_config, profiles), nested_paths)) } fn replace(&self, cx: &mut Context) -> CargoResult> { + if self.patch.is_some() && self.replace.is_some() { + bail!("cannot specify both [replace] and [patch]"); + } let mut replace = Vec::new(); for (spec, replacement) in self.replace.iter().flat_map(|x| x) { let mut spec = PackageIdSpec::parse(spec).chain_err(|| { @@ -750,6 +762,21 @@ impl TomlManifest { Ok(replace) } + fn patch(&self, cx: &mut Context) + -> CargoResult>> { + let mut patch = HashMap::new(); + for (url, deps) in self.patch.iter().flat_map(|x| x) { + let url = match &url[..] { + "crates-io" => CRATES_IO.parse().unwrap(), + _ => url.to_url()?, + }; + patch.insert(url, deps.iter().map(|(name, dep)| { + dep.to_dependency(name, cx, None) + }).collect::>>()?); + } + Ok(patch) + } + fn maybe_custom_build(&self, build: &Option, package_root: &Path) diff --git a/src/doc/manifest.md b/src/doc/manifest.md index 4a5501f5126..fd344f6db2b 100644 --- a/src/doc/manifest.md +++ b/src/doc/manifest.md @@ -424,8 +424,8 @@ properties: root crate's `Cargo.toml`. * The lock file for all crates in the workspace resides next to the root crate's `Cargo.toml`. -* The `[replace]` section in `Cargo.toml` is only recognized at the workspace - root crate, it's ignored in member crates' manifests. +* The `[patch]` and `[replace]` sections in `Cargo.toml` are only recognized + at the workspace root crate, they are ignored in member crates' manifests. [RFC 1525]: https://github.com/rust-lang/rfcs/blob/master/text/1525-cargo-workspace.md @@ -629,6 +629,42 @@ includes them. You can read more about the different crate types in the [Rust Reference Manual](https://doc.rust-lang.org/reference/linkage.html) +# The `[patch]` Section + +This section of Cargo.toml can be used to [override dependencies][replace] with +other copies. The syntax is similar to the `[dependencies]` section: + +```toml +[patch.crates-io] +foo = { git = 'https://github.com/example/foo' } +bar = { path = 'my/local/bar' } +``` + +The `[patch]` table is made of dependency-like sub-tables. Each key after +`[patch]` is a URL of the source that's being patched, or `crates-io` if +you're modifying the https://crates.io registry. In the example above +`crates-io` could be replaced with a git URL such as +`https://github.com/rust-lang-nursery/log`. + +Each entry in these tables is a normal dependency specification, the same as +found in the `[dependencies]` section of the manifest. The dependencies listed +in the `[patch]` section are resolved and used to patch the source at the +URL specified. The above manifest snippet patches the `crates-io` source (e.g. +crates.io itself) with the `foo` crate and `bar` crate. + +Sources can be patched with versions of crates that do not exist, and they can +also be patched with versions of crates that already exist. If a source is +patched with a crate version that already exists in the source, then the +source's original crate is replaced. + +More information about overriding dependencies can be found in the [overriding +dependencies][replace] section of the documentation and [RFC 1969] for the +technical specification of this feature. Note that the `[patch]` feature will +first become available in Rust 1.20, set to be released on 2017-08-31. + +[RFC 1969]: https://github.com/rust-lang/rfcs/pull/1969 +[replace]: specifying-dependencies.html#overriding-dependencies + # The `[replace]` Section This section of Cargo.toml can be used to [override dependencies][replace] with @@ -650,5 +686,3 @@ source (e.g. git or a local path). More information about overriding dependencies can be found in the [overriding dependencies][replace] section of the documentation. - -[replace]: specifying-dependencies.html#overriding-dependencies diff --git a/src/doc/specifying-dependencies.md b/src/doc/specifying-dependencies.md index 248d4fe024f..a4b0c342e34 100644 --- a/src/doc/specifying-dependencies.md +++ b/src/doc/specifying-dependencies.md @@ -165,139 +165,251 @@ hello_utils = { path = "hello_utils", version = "0.1.0" } # Overriding dependencies -Sometimes you may want to override one of Cargo’s dependencies. For example -let's say you're working on a project using the -[`uuid`](https://crates.io/crates/uuid) crate which depends on -[`rand`](https://crates.io/crates/rand). You've discovered there's a bug in -`rand`, however, and it's already fixed upstream but hasn't been published yet. -You'd like to test out the fix, so let's first take a look at what your -`Cargo.toml` will look like: +There are a number of methods in Cargo to support overriding dependencies and +otherwise controlling the dependency graph. These options are typically, though, +only available at the workspace level and aren't propagated through +dependencies. In other words, "applications" have the ability to override +dependencies but "libraries" do not. + +The desire to override a dependency or otherwise alter some dependencies can +arise through a number of scenarios. Most of them, however, boil down to the +ability to to work with a crate before it's been published to crates.io. For +example: + +* A crate you're working on is also used in a much larger application you're + working on, and you'd like to test a bug fix to the library inside of the + larger application. +* An upstream crate you don't work on has a new feature or a bug fix on the + master branch of its git repository which you'd like to test out. +* You're about to publish a new major version of your crate, but you'd like to + do integration testing across an entire project to ensure the new major + version works. +* You've submitted a fix to an upstream crate for a bug you found, but you'd + like to immediately have your application start depending on the fixed version + of the crate to avoid blocking on the bug fix getting merged. + +These scenarios are currently all solved with the [`[patch]` manifest +section][patch-section]. Note that the `[patch]` feature is not yet currently +stable and will be released on 2017-08-31. Historically some of these scenarios +have been solved with [the `[replace]` section][replace-section], but we'll +document the `[patch]` section here. + +[patch-section]: manifest.html#the-patch-section +[replace-section]: manifest.html#the-replace-section + +### Testing a bugfix + +Let's say you're working with the [`uuid`] crate but while you're working on it +you discover a bug. You are, however, quite enterprising so you decide to also +try out to fix the bug! Originally your manifest will look like: + +[`uuid`](https://crates.io/crates/uuid) ```toml [package] -name = "my-awesome-crate" -version = "0.2.0" -authors = ["The Rust Project Developers"] +name = "my-library" +version = "0.1.0" +authors = ["..."] [dependencies] -uuid = "0.2" +uuid = "1.0" ``` -To override the `rand` dependency of `uuid`, we'll leverage the [`[replace]` -section][replace-section] of `Cargo.toml` by appending this to the end: +First thing we'll do is to clone the [`uuid` repository][uuid-repository] +locally via: -[replace-section]: manifest.html#the-replace-section +```shell +$ git clone https://github.com/rust-lang-nursery/uuid +``` + +Next we'll edit the manifest of `my-library` to contain: ```toml -[replace] -"rand:0.3.14" = { git = 'https://github.com/rust-lang-nursery/rand' } +[patch.crates-io] +uuid = { path = "../path/to/uuid" } ``` -This indicates that the version of `rand` we're currently using, 0.3.14, will be -replaced with the master branch of `rand` on GitHub. Next time when you execute -`cargo build` Cargo will take care of checking out this repository and hooking -the `uuid` crate up to the new version. - -Note that a restriction of `[replace]`, however, is that the replaced crate must -not only have the same name but also the same version. This means that if the -`master` branch of `rand` has migrated to, for example, 0.4.3, you'll need to -follow a few extra steps to test out the crate: - -1. Fork the upstream repository to your account -2. Create a branch which starts from the 0.3.14 release (likely tagged as - 0.3.14) -3. Identify the fix of the bug at hand and cherry-pick it onto your branch -4. Update `[replace]` to point to your git repository and branch - -This technique can also be useful when testing out new features for a -dependency. Following the workflow above you can have a branch where you add -features, and then once it's ready to go you can send a PR to the upstream -repository. While you're waiting for the PR to get merged you can continue to -work locally with a `[replace]`, and then once the PR is merged and published -you can remove `[replace]` and use the newly-published version. - -Note: The `Cargo.lock` file will list two versions of the replaced crate: one -for the original crate, and one for the version specified in `[replace]`. -`cargo build -v` can verify that only one version is used in the build. +Here we declare that we're *patching* the source `crates-io` with a new +dependency. This will effectively add the local checked out version of `uuid` to +the crates.io registry for our local project. -### Overriding with local dependencies +Next up we need to ensure that our lock file is updated to use this new version +of `uuid` so our project uses the locally checked out copy instead of one from +crates.io. The way `[patch]` works is that it'll load the dependency at +`../path/to/uuid` and then whenever crates.io is queried for versions of `uuid` +it'll *also* return the local version. -Sometimes you're only temporarily working on a crate and you don't want to have -to modify `Cargo.toml` like with the `[replace]` section above. For this use -case Cargo offers a much more limited version of overrides called **path -overrides**. +This means that the version number of the local checkout is significant and will +affect whether the patch is used. Our manifest declared `uuid = "1.0"` which +means we'll only resolve to `>= 1.0.0, < 2.0.0`, and Cargo's greedy resolution +algorithm also means that we'll resolve to the maximum version within that +range. Typically this doesn't matter as the version of the git repository will +already be greater or match the maximum version published on crates.io, but it's +important to keep this in mind! -Similar to before, let’s say you’re working on a project, -[`uuid`](https://crates.io/crates/uuid), which depends on -[`rand`](https://crates.io/crates/rand). This time you're the one who finds a -bug in `rand`, and you want to write a patch and be able to test out your patch -by using your version of `rand` in `uuid`. Here’s what `uuid`’s `Cargo.toml` -looks like: +In any case, typically all you need to do now is: + +```shell +$ cargo build + Compiling uuid v1.0.0 (file://.../uuid) + Compiling my-library v0.1.0 (file://.../my-library) + Finished dev [unoptimized + debuginfo] target(s) in 0.32 secs +``` + +And that's it! You're now building with the local version of `uuid` (note the +`file://` in the build output). If you don't see the `file://` version getting +built then you may need to run `cargo update -p uuid --precise $version` where +`$version` is the version of the locally checked out copy of `uuid`. + +Once you've fixed the bug you originally found the next thing you'll want to do +is to likely submit that as a pull request to the `uuid` crate itself. Once +you've done this then you can also update the `[patch]` section. The listing +inside of `[patch]` is just like the `[dependencies]` section, so once your pull +request is merged you could change your `path` dependency to: + +```toml +[patch.crates-io] +uuid = { git = 'https://github.com/rust-lang-nursery/uuid' } +``` + +[uuid-repository]: https://github.com/rust-lang-nursery/uuid + +### Working with an unpublished minor version + +Let's now shift gears a bit from bug fixes to adding features. While working on +`my-library` you discover that a whole new feature is needed in the `uuid` +crate. You've implemented this feature, tested it locally above with `[patch]`, +and submitted a pull request. Let's go over how you continue to use and test it +before it's actually published. + +Let's also say that the current version of `uuid` on crates.io is `1.0.0`, but +since then the master branch of the git repository has updated to `1.0.1`. This +branch includes your new feature you submitted previously. To use this +repository we'll edit our `Cargo.toml` to look like ```toml [package] -name = "uuid" -version = "0.2.2" -authors = ["The Rust Project Developers"] +name = "my-library" +version = "0.1.0" +authors = ["..."] [dependencies] -rand = { version = "0.3", optional = true } +uuid = "1.0.1" + +[patch.crates-io] +uuid = { git = 'https://github.com/rust-lang-nursery/uuid' } ``` -You check out a local copy of `rand`, let’s say in your `~/src` directory: +Note that our local dependency on `uuid` has been updated to `1.0.1` as it's +what we'll actually require once the crate is published. This version doesn't +exist on crates.io, though, so we provide it with the `[patch]` section of the +manifest. -```shell -$ cd ~/src -$ git clone https://github.com/rust-lang-nursery/rand +Now when our library is built it'll fetch `uuid` from the git repository and +resolve to 1.0.1 inside the repository instead of trying to download a version +from crates.io. Once 1.0.1 is published on crates.io the `[patch]` section can +be deleted. + +It's also worth nothing that `[patch]` applies *transitively*. Let's say you use +`my-library` in a larger project, such as: + +```toml +[package] +name = "my-binary" +version = "0.1.0" +authors = ["..."] + +[dependencies] +my-library = { git = 'https://example.com/git/my-library' } +uuid = "1.0" + +[patch.crates-io] +uuid = { git = 'https://github.com/rust-lang-nursery/uuid' } ``` -A path override is communicated to Cargo through the `.cargo/config` -configuration mechanism. If Cargo finds this configuration when building your -package, it will use the override on your local machine instead of the source -specified in your `Cargo.toml`. +Remember that `[patch]` is only applicable at the *top level* so we consumers of +`my-library` have to repeat the `[patch]` section if necessary. Here, though, +the new `uuid` crate applies to *both* our dependency on `uuid` and the +`my-library -> uuid` dependency. The `uuid` crate will be resolved to one +version for this entire crate graph, 1.0.1, and it'll be pulled from the git +repository. -Cargo looks for a directory named `.cargo` up the directory hierarchy of -your project. If your project is in `/path/to/project/uuid`, -it will search for a `.cargo` in: +### Prepublishing a breaking change -* `/path/to/project/uuid` -* `/path/to/project` -* `/path/to` -* `/path` -* `/` +As a final scenario, let's take a look at working with a new major version of a +crate, typically accompanied with breaking changes. Sticking with our previous +crates, this means that we're going to be creating version 2.0.0 of the `uuid` +crate. After we've submitted all changes upstream we can update our manifest for +`my-library` to look like: -This allows you to specify your overrides in a parent directory that -includes commonly used packages that you work on locally and share them -with all projects. +```toml +[dependencies] +uuid = "2.0" -To specify overrides, create a `.cargo/config` file in some ancestor of -your project’s directory (common places to put it is in the root of -your code directory or in your home directory). +[patch.crates-io] +uuid = { git = "https://github.com/rust-lang-nursery/uuid", branch = "2.0.0" } +``` -Inside that file, put this: +And that's it! Like with the previous example the 2.0.0 version doesn't actually +exist on crates.io but we can still put it in through a git dependency through +the usage of the `[patch]` section. As a thought exercise let's take another +look at the `my-binary` manifest from above again as well: ```toml -paths = ["/path/to/project/rand"] +[package] +name = "my-binary" +version = "0.1.0" +authors = ["..."] + +[dependencies] +my-library = { git = 'https://example.com/git/my-library' } +uuid = "1.0" + +[patch.crates-io] +uuid = { git = 'https://github.com/rust-lang-nursery/uuid', version = '2.0.0' } +``` + +Note that this will actually resolve to two versions of the `uuid` crate. The +`my-binary` crate will continue to use the 1.x.y series of the `uuid` crate but +the `my-library` crate will use the 2.0.0 version of `uuid`. This will allow you +to gradually roll out breaking changes to a crate through a dependency graph +without being force to update everything all at once. + +### Overriding with local dependencies + +Sometimes you're only temporarily working on a crate and you don't want to have +to modify `Cargo.toml` like with the `[patch]` section above. For this use +case Cargo offers a much more limited version of overrides called **path +overrides**. + +Path overrides are specified through `.cargo/config` instead of `Cargo.toml`, +and you can find [more documentation about this configuration][config-docs]. +Inside of `.cargo/config` you'll specify a key called `paths`: + +[config-docs]: config.html + +```toml +paths = ["/path/to/uuid"] ``` This array should be filled with directories that contain a `Cargo.toml`. In -this instance, we’re just adding `rand`, so it will be the only one that’s -overridden. This path must be an absolute path. +this instance, we’re just adding `uuid`, so it will be the only one that’s +overridden. This path can be either absolute or relative to the directory that +contains the `.cargo` folder. -Path overrides are more restricted than the `[replace]` section, however, in +Path overrides are more restricted than the `[patch]` section, however, in that they cannot change the structure of the dependency graph. When a -replacement is applied it must be the case that the previous set of dependencies -all match exactly and can be used for the replacement. For example this means -that path overrides cannot be used to test out adding a dependency to a crate, -instead `[replace]` must be used in that situation. +path replacement is used then the previous set of dependencies +must all match exactly to the new `Cargo.toml` specification. For example this +means that path overrides cannot be used to test out adding a dependency to a +crate, instead `[patch]` must be used in that situation. As a result usage of a +path override is typically isolated to quick bug fixes rather than larger +changes. Note: using a local configuration to override paths will only work for crates that have been published to [crates.io]. You cannot use this feature to tell Cargo how to find local unpublished crates. -More information about local configuration can be found in the [configuration -documentation](config.html). - # Platform specific dependencies diff --git a/tests/cargotest/support/mod.rs b/tests/cargotest/support/mod.rs index 7b4639d824c..e7376b11604 100644 --- a/tests/cargotest/support/mod.rs +++ b/tests/cargotest/support/mod.rs @@ -177,10 +177,14 @@ impl ProjectBuilder { pub fn file>(mut self, path: B, body: &str) -> ProjectBuilder { - self.files.push(FileBuilder::new(self.root.join(path), body)); + self._file(path.as_ref(), body); self } + fn _file(&mut self, path: &Path, body: &str) { + self.files.push(FileBuilder::new(self.root.join(path), body)); + } + pub fn change_file(&self, path: &str, body: &str) { assert!(self.is_build.get()); FileBuilder::new(self.root.join(path), body).mk() @@ -350,10 +354,14 @@ impl Execs { } pub fn with_stderr(mut self, expected: S) -> Execs { - self.expect_stderr = Some(expected.to_string()); + self._with_stderr(&expected); self } + fn _with_stderr(&mut self, expected: &ToString) { + self.expect_stderr = Some(expected.to_string()); + } + pub fn with_status(mut self, expected: i32) -> Execs { self.expect_exit_code = Some(expected); self diff --git a/tests/patch.rs b/tests/patch.rs new file mode 100644 index 00000000000..c333d4b5991 --- /dev/null +++ b/tests/patch.rs @@ -0,0 +1,744 @@ +#[macro_use] +extern crate cargotest; +extern crate hamcrest; +extern crate toml; + +use std::fs::{self, File}; +use std::io::{Read, Write}; + +use cargotest::support::git; +use cargotest::support::paths; +use cargotest::support::registry::Package; +use cargotest::support::{execs, project}; +use hamcrest::assert_that; + +#[test] +fn replace() { + Package::new("foo", "0.1.0").publish(); + Package::new("deep-foo", "0.1.0") + .file("src/lib.rs", r#" + extern crate foo; + pub fn deep() { + foo::foo(); + } + "#) + .dep("foo", "0.1.0") + .publish(); + + let p = project("bar") + .file("Cargo.toml", r#" + [package] + name = "bar" + version = "0.0.1" + authors = [] + + [dependencies] + foo = "0.1.0" + deep-foo = "0.1.0" + + [patch.crates-io] + foo = { path = "foo" } + "#) + .file("src/lib.rs", " + extern crate foo; + extern crate deep_foo; + pub fn bar() { + foo::foo(); + deep_foo::deep(); + } + ") + .file("foo/Cargo.toml", r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + "#) + .file("foo/src/lib.rs", r#" + pub fn foo() {} + "#); + + assert_that(p.cargo_process("build"), + execs().with_status(0).with_stderr("\ +[UPDATING] registry `file://[..]` +[DOWNLOADING] deep-foo v0.1.0 ([..]) +[COMPILING] foo v0.1.0 (file://[..]) +[COMPILING] deep-foo v0.1.0 +[COMPILING] bar v0.0.1 (file://[..]) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..] +")); + + assert_that(p.cargo("build"),//.env("RUST_LOG", "trace"), + execs().with_status(0).with_stderr("[FINISHED] [..]")); +} + +#[test] +fn nonexistent() { + Package::new("baz", "0.1.0").publish(); + + let p = project("bar") + .file("Cargo.toml", r#" + [package] + name = "bar" + version = "0.0.1" + authors = [] + + [dependencies] + foo = "0.1.0" + + [patch.crates-io] + foo = { path = "foo" } + "#) + .file("src/lib.rs", " + extern crate foo; + pub fn bar() { + foo::foo(); + } + ") + .file("foo/Cargo.toml", r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + "#) + .file("foo/src/lib.rs", r#" + pub fn foo() {} + "#); + + assert_that(p.cargo_process("build"), + execs().with_status(0).with_stderr("\ +[UPDATING] registry `file://[..]` +[COMPILING] foo v0.1.0 (file://[..]) +[COMPILING] bar v0.0.1 (file://[..]) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..] +")); + assert_that(p.cargo("build"), + execs().with_status(0).with_stderr("[FINISHED] [..]")); +} + +#[test] +fn patch_git() { + let foo = git::repo(&paths::root().join("override")) + .file("Cargo.toml", r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + "#) + .file("src/lib.rs", ""); + foo.build(); + + let p = project("bar") + .file("Cargo.toml", &format!(r#" + [package] + name = "bar" + version = "0.0.1" + authors = [] + + [dependencies] + foo = {{ git = '{}' }} + + [patch.'{0}'] + foo = {{ path = "foo" }} + "#, foo.url())) + .file("src/lib.rs", " + extern crate foo; + pub fn bar() { + foo::foo(); + } + ") + .file("foo/Cargo.toml", r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + "#) + .file("foo/src/lib.rs", r#" + pub fn foo() {} + "#); + + assert_that(p.cargo_process("build"), + execs().with_status(0).with_stderr("\ +[UPDATING] git repository `file://[..]` +[COMPILING] foo v0.1.0 (file://[..]) +[COMPILING] bar v0.0.1 (file://[..]) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..] +")); + assert_that(p.cargo("build"), + execs().with_status(0).with_stderr("[FINISHED] [..]")); +} + +#[test] +fn patch_to_git() { + let foo = git::repo(&paths::root().join("override")) + .file("Cargo.toml", r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + "#) + .file("src/lib.rs", "pub fn foo() {}"); + foo.build(); + + Package::new("foo", "0.1.0").publish(); + + let p = project("bar") + .file("Cargo.toml", &format!(r#" + [package] + name = "bar" + version = "0.0.1" + authors = [] + + [dependencies] + foo = "0.1" + + [patch.crates-io] + foo = {{ git = '{}' }} + "#, foo.url())) + .file("src/lib.rs", " + extern crate foo; + pub fn bar() { + foo::foo(); + } + "); + + assert_that(p.cargo_process("build"),//.env("RUST_LOG", "cargo=trace"), + execs().with_status(0).with_stderr("\ +[UPDATING] git repository `file://[..]` +[UPDATING] registry `file://[..]` +[COMPILING] foo v0.1.0 (file://[..]) +[COMPILING] bar v0.0.1 (file://[..]) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..] +")); + assert_that(p.cargo("build"), + execs().with_status(0).with_stderr("[FINISHED] [..]")); +} + +#[test] +fn unused() { + Package::new("foo", "0.1.0").publish(); + + let p = project("bar") + .file("Cargo.toml", r#" + [package] + name = "bar" + version = "0.0.1" + authors = [] + + [dependencies] + foo = "0.1.0" + + [patch.crates-io] + foo = { path = "foo" } + "#) + .file("src/lib.rs", "") + .file("foo/Cargo.toml", r#" + [package] + name = "foo" + version = "0.2.0" + authors = [] + "#) + .file("foo/src/lib.rs", r#" + not rust code + "#); + + assert_that(p.cargo_process("build"), + execs().with_status(0).with_stderr("\ +[UPDATING] registry `file://[..]` +[DOWNLOADING] foo v0.1.0 [..] +[COMPILING] foo v0.1.0 +[COMPILING] bar v0.0.1 (file://[..]) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..] +")); + assert_that(p.cargo("build"), + execs().with_status(0).with_stderr("[FINISHED] [..]")); + + // unused patch should be in the lock file + let mut lock = String::new(); + File::open(p.root().join("Cargo.lock")).unwrap() + .read_to_string(&mut lock).unwrap(); + let toml: toml::Value = toml::from_str(&lock).unwrap(); + assert_eq!(toml["patch"]["unused"].as_array().unwrap().len(), 1); + assert_eq!(toml["patch"]["unused"][0]["name"].as_str(), Some("foo")); + assert_eq!(toml["patch"]["unused"][0]["version"].as_str(), Some("0.2.0")); +} + +#[test] +fn unused_git() { + Package::new("foo", "0.1.0").publish(); + + let foo = git::repo(&paths::root().join("override")) + .file("Cargo.toml", r#" + [package] + name = "foo" + version = "0.2.0" + authors = [] + "#) + .file("src/lib.rs", ""); + foo.build(); + + let p = project("bar") + .file("Cargo.toml", &format!(r#" + [package] + name = "bar" + version = "0.0.1" + authors = [] + + [dependencies] + foo = "0.1" + + [patch.crates-io] + foo = {{ git = '{}' }} + "#, foo.url())) + .file("src/lib.rs", ""); + + assert_that(p.cargo_process("build"), + execs().with_status(0).with_stderr("\ +[UPDATING] git repository `file://[..]` +[UPDATING] registry `file://[..]` +[DOWNLOADING] foo v0.1.0 [..] +[COMPILING] foo v0.1.0 +[COMPILING] bar v0.0.1 (file://[..]) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..] +")); + assert_that(p.cargo("build"), + execs().with_status(0).with_stderr("[FINISHED] [..]")); +} + +#[test] +fn add_patch() { + Package::new("foo", "0.1.0").publish(); + + let p = project("bar") + .file("Cargo.toml", r#" + [package] + name = "bar" + version = "0.0.1" + authors = [] + + [dependencies] + foo = "0.1.0" + "#) + .file("src/lib.rs", "") + .file("foo/Cargo.toml", r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + "#) + .file("foo/src/lib.rs", r#""#); + + assert_that(p.cargo_process("build"), + execs().with_status(0).with_stderr("\ +[UPDATING] registry `file://[..]` +[DOWNLOADING] foo v0.1.0 [..] +[COMPILING] foo v0.1.0 +[COMPILING] bar v0.0.1 (file://[..]) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..] +")); + assert_that(p.cargo("build"), + execs().with_status(0).with_stderr("[FINISHED] [..]")); + + t!(t!(File::create(p.root().join("Cargo.toml"))).write_all(br#" + [package] + name = "bar" + version = "0.0.1" + authors = [] + + [dependencies] + foo = "0.1.0" + + [patch.crates-io] + foo = { path = 'foo' } + "#)); + + assert_that(p.cargo("build"), + execs().with_status(0).with_stderr("\ +[COMPILING] foo v0.1.0 (file://[..]) +[COMPILING] bar v0.0.1 (file://[..]) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..] +")); + assert_that(p.cargo("build"), + execs().with_status(0).with_stderr("[FINISHED] [..]")); +} + +#[test] +fn add_ignored_patch() { + Package::new("foo", "0.1.0").publish(); + + let p = project("bar") + .file("Cargo.toml", r#" + [package] + name = "bar" + version = "0.0.1" + authors = [] + + [dependencies] + foo = "0.1.0" + "#) + .file("src/lib.rs", "") + .file("foo/Cargo.toml", r#" + [package] + name = "foo" + version = "0.1.1" + authors = [] + "#) + .file("foo/src/lib.rs", r#""#); + + assert_that(p.cargo_process("build"), + execs().with_status(0).with_stderr("\ +[UPDATING] registry `file://[..]` +[DOWNLOADING] foo v0.1.0 [..] +[COMPILING] foo v0.1.0 +[COMPILING] bar v0.0.1 (file://[..]) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..] +")); + assert_that(p.cargo("build"), + execs().with_status(0).with_stderr("[FINISHED] [..]")); + + t!(t!(File::create(p.root().join("Cargo.toml"))).write_all(br#" + [package] + name = "bar" + version = "0.0.1" + authors = [] + + [dependencies] + foo = "0.1.0" + + [patch.crates-io] + foo = { path = 'foo' } + "#)); + + assert_that(p.cargo("build"), + execs().with_status(0).with_stderr("\ +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..] +")); + assert_that(p.cargo("build"), + execs().with_status(0).with_stderr("[FINISHED] [..]")); +} + +#[test] +fn new_minor() { + Package::new("foo", "0.1.0").publish(); + + let p = project("bar") + .file("Cargo.toml", r#" + [package] + name = "bar" + version = "0.0.1" + authors = [] + + [dependencies] + foo = "0.1.0" + + [patch.crates-io] + foo = { path = 'foo' } + "#) + .file("src/lib.rs", "") + .file("foo/Cargo.toml", r#" + [package] + name = "foo" + version = "0.1.1" + authors = [] + "#) + .file("foo/src/lib.rs", r#""#); + + assert_that(p.cargo_process("build"), + execs().with_status(0).with_stderr("\ +[UPDATING] registry `file://[..]` +[COMPILING] foo v0.1.1 [..] +[COMPILING] bar v0.0.1 (file://[..]) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..] +")); +} + +#[test] +fn transitive_new_minor() { + Package::new("foo", "0.1.0").publish(); + + let p = project("bar") + .file("Cargo.toml", r#" + [package] + name = "bar" + version = "0.0.1" + authors = [] + + [dependencies] + subdir = { path = 'subdir' } + + [patch.crates-io] + foo = { path = 'foo' } + "#) + .file("src/lib.rs", "") + .file("subdir/Cargo.toml", r#" + [package] + name = "subdir" + version = "0.1.0" + authors = [] + + [dependencies] + foo = '0.1.0' + "#) + .file("subdir/src/lib.rs", r#""#) + .file("foo/Cargo.toml", r#" + [package] + name = "foo" + version = "0.1.1" + authors = [] + "#) + .file("foo/src/lib.rs", r#""#); + + assert_that(p.cargo_process("build"), + execs().with_status(0).with_stderr("\ +[UPDATING] registry `file://[..]` +[COMPILING] foo v0.1.1 [..] +[COMPILING] subdir v0.1.0 [..] +[COMPILING] bar v0.0.1 (file://[..]) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..] +")); +} + +#[test] +fn new_major() { + Package::new("foo", "0.1.0").publish(); + + let p = project("bar") + .file("Cargo.toml", r#" + [package] + name = "bar" + version = "0.0.1" + authors = [] + + [dependencies] + foo = "0.2.0" + + [patch.crates-io] + foo = { path = 'foo' } + "#) + .file("src/lib.rs", "") + .file("foo/Cargo.toml", r#" + [package] + name = "foo" + version = "0.2.0" + authors = [] + "#) + .file("foo/src/lib.rs", r#""#); + + assert_that(p.cargo_process("build"), + execs().with_status(0).with_stderr("\ +[UPDATING] registry `file://[..]` +[COMPILING] foo v0.2.0 [..] +[COMPILING] bar v0.0.1 (file://[..]) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..] +")); + + Package::new("foo", "0.2.0").publish(); + assert_that(p.cargo("update"), + execs().with_status(0)); + assert_that(p.cargo("build"), + execs().with_status(0).with_stderr("\ +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..] +")); + + t!(t!(File::create(p.root().join("Cargo.toml"))).write_all(br#" + [package] + name = "bar" + version = "0.0.1" + authors = [] + + [dependencies] + foo = "0.2.0" + "#)); + assert_that(p.cargo("build"), + execs().with_status(0).with_stderr("\ +[UPDATING] registry `file://[..]` +[DOWNLOADING] foo v0.2.0 [..] +[COMPILING] foo v0.2.0 +[COMPILING] bar v0.0.1 (file://[..]) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..] +")); +} + +#[test] +fn transitive_new_major() { + Package::new("foo", "0.1.0").publish(); + + let p = project("bar") + .file("Cargo.toml", r#" + [package] + name = "bar" + version = "0.0.1" + authors = [] + + [dependencies] + subdir = { path = 'subdir' } + + [patch.crates-io] + foo = { path = 'foo' } + "#) + .file("src/lib.rs", "") + .file("subdir/Cargo.toml", r#" + [package] + name = "subdir" + version = "0.1.0" + authors = [] + + [dependencies] + foo = '0.2.0' + "#) + .file("subdir/src/lib.rs", r#""#) + .file("foo/Cargo.toml", r#" + [package] + name = "foo" + version = "0.2.0" + authors = [] + "#) + .file("foo/src/lib.rs", r#""#); + + assert_that(p.cargo_process("build"), + execs().with_status(0).with_stderr("\ +[UPDATING] registry `file://[..]` +[COMPILING] foo v0.2.0 [..] +[COMPILING] subdir v0.1.0 [..] +[COMPILING] bar v0.0.1 (file://[..]) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..] +")); +} + +#[test] +fn remove_patch() { + Package::new("foo", "0.1.0").publish(); + Package::new("bar", "0.1.0").publish(); + + let p = project("bar") + .file("Cargo.toml", r#" + [package] + name = "bar" + version = "0.0.1" + authors = [] + + [dependencies] + foo = "0.1" + + [patch.crates-io] + foo = { path = 'foo' } + bar = { path = 'bar' } + "#) + .file("src/lib.rs", "") + .file("foo/Cargo.toml", r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + "#) + .file("foo/src/lib.rs", r#""#) + .file("bar/Cargo.toml", r#" + [package] + name = "bar" + version = "0.1.0" + authors = [] + "#) + .file("bar/src/lib.rs", r#""#); + + // Generate a lock file where `bar` is unused + assert_that(p.cargo_process("build"), execs().with_status(0)); + let mut lock_file1 = String::new(); + File::open(p.root().join("Cargo.lock")).unwrap() + .read_to_string(&mut lock_file1).unwrap(); + + // Remove `bar` and generate a new lock file form the old one + File::create(p.root().join("Cargo.toml")).unwrap().write_all(r#" + [package] + name = "bar" + version = "0.0.1" + authors = [] + + [dependencies] + foo = "0.1" + + [patch.crates-io] + foo = { path = 'foo' } + "#.as_bytes()).unwrap(); + assert_that(p.cargo("build"), execs().with_status(0)); + let mut lock_file2 = String::new(); + File::open(p.root().join("Cargo.lock")).unwrap() + .read_to_string(&mut lock_file2).unwrap(); + + // Remove the lock file and build from scratch + fs::remove_file(p.root().join("Cargo.lock")).unwrap(); + assert_that(p.cargo("build"), execs().with_status(0)); + let mut lock_file3 = String::new(); + File::open(p.root().join("Cargo.lock")).unwrap() + .read_to_string(&mut lock_file3).unwrap(); + + assert!(lock_file1.contains("bar")); + assert_eq!(lock_file2, lock_file3); + assert!(lock_file1 != lock_file2); +} + +#[test] +fn non_crates_io() { + Package::new("foo", "0.1.0").publish(); + + let p = project("bar") + .file("Cargo.toml", r#" + [package] + name = "bar" + version = "0.0.1" + authors = [] + + [patch.some-other-source] + foo = { path = 'foo' } + "#) + .file("src/lib.rs", "") + .file("foo/Cargo.toml", r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + "#) + .file("foo/src/lib.rs", r#""#); + + assert_that(p.cargo_process("build"), + execs().with_status(101) + .with_stderr("\ +error: failed to parse manifest at `[..]` + +Caused by: + invalid url `some-other-source`: relative URL without a base +")); +} + +#[test] +fn replace_with_crates_io() { + Package::new("foo", "0.1.0").publish(); + + let p = project("bar") + .file("Cargo.toml", r#" + [package] + name = "bar" + version = "0.0.1" + authors = [] + + [patch.crates-io] + foo = "0.1" + "#) + .file("src/lib.rs", "") + .file("foo/Cargo.toml", r#" + [package] + name = "foo" + version = "0.1.0" + authors = [] + "#) + .file("foo/src/lib.rs", r#""#); + + assert_that(p.cargo_process("build"), + execs().with_status(101) + .with_stderr("\ +[UPDATING] [..] +error: failed to resolve patches for `[..]` + +Caused by: + patch for `foo` in `[..]` points to the same source, but patches must point \ + to different sources +")); +}