diff --git a/crates/cargo-test-support/src/compare.rs b/crates/cargo-test-support/src/compare.rs index e822bb3822c..1d1cfa9bd17 100644 --- a/crates/cargo-test-support/src/compare.rs +++ b/crates/cargo-test-support/src/compare.rs @@ -300,6 +300,7 @@ static E2E_LITERAL_REDACTIONS: &[(&str, &str)] = &[ ("[DOCTEST]", " Doc-tests"), ("[PACKAGING]", " Packaging"), ("[PACKAGED]", " Packaged"), + ("[PATCHING]", " Patching"), ("[DOWNLOADING]", " Downloading"), ("[DOWNLOADED]", " Downloaded"), ("[UPLOADING]", " Uploading"), diff --git a/crates/cargo-util-schemas/src/core/mod.rs b/crates/cargo-util-schemas/src/core/mod.rs index e8a878aa77c..d8e209111d7 100644 --- a/crates/cargo-util-schemas/src/core/mod.rs +++ b/crates/cargo-util-schemas/src/core/mod.rs @@ -7,4 +7,6 @@ pub use package_id_spec::PackageIdSpecError; pub use partial_version::PartialVersion; pub use partial_version::PartialVersionError; pub use source_kind::GitReference; +pub use source_kind::PatchInfo; +pub use source_kind::PatchInfoError; pub use source_kind::SourceKind; diff --git a/crates/cargo-util-schemas/src/core/package_id_spec.rs b/crates/cargo-util-schemas/src/core/package_id_spec.rs index 72d72149e2a..8a015bd598b 100644 --- a/crates/cargo-util-schemas/src/core/package_id_spec.rs +++ b/crates/cargo-util-schemas/src/core/package_id_spec.rs @@ -7,6 +7,7 @@ use url::Url; use crate::core::GitReference; use crate::core::PartialVersion; use crate::core::PartialVersionError; +use crate::core::PatchInfo; use crate::core::SourceKind; use crate::manifest::PackageName; use crate::restricted_names::NameValidationError; @@ -145,6 +146,14 @@ impl PackageIdSpec { kind = Some(SourceKind::Path); url = strip_url_protocol(&url); } + "patched" => { + let patch_info = + PatchInfo::from_query(url.query_pairs()).map_err(ErrorKind::PatchInfo)?; + url.set_query(None); + kind = Some(SourceKind::Patched(patch_info)); + // We don't strip protocol and leave `patch` as part of URL + // in order to distinguish them. + } kind => return Err(ErrorKind::UnsupportedProtocol(kind.into()).into()), } } else { @@ -232,10 +241,16 @@ impl fmt::Display for PackageIdSpec { write!(f, "{protocol}+")?; } write!(f, "{}", url)?; - if let Some(SourceKind::Git(git_ref)) = self.kind.as_ref() { - if let Some(pretty) = git_ref.pretty_ref(true) { - write!(f, "?{}", pretty)?; + match self.kind.as_ref() { + Some(SourceKind::Git(git_ref)) => { + if let Some(pretty) = git_ref.pretty_ref(true) { + write!(f, "?{pretty}")?; + } } + Some(SourceKind::Patched(patch_info)) => { + write!(f, "?{}", patch_info.as_query())?; + } + _ => {} } if url.path_segments().unwrap().next_back().unwrap() != &*self.name { printed_name = true; @@ -314,13 +329,16 @@ enum ErrorKind { #[error(transparent)] PartialVersion(#[from] crate::core::PartialVersionError), + + #[error(transparent)] + PatchInfo(#[from] crate::core::PatchInfoError), } #[cfg(test)] mod tests { use super::ErrorKind; use super::PackageIdSpec; - use crate::core::{GitReference, SourceKind}; + use crate::core::{GitReference, PatchInfo, SourceKind}; use url::Url; #[test] @@ -599,6 +617,22 @@ mod tests { }, "path+file:///path/to/my/project/foo#1.1.8", ); + + // Unstable + ok( + "patched+https://crates.io/foo?name=bar&version=1.2.0&patch=%2Fto%2Fa.patch&patch=%2Fb.patch#bar@1.2.0", + PackageIdSpec { + name: String::from("bar"), + version: Some("1.2.0".parse().unwrap()), + url: Some(Url::parse("patched+https://crates.io/foo").unwrap()), + kind: Some(SourceKind::Patched(PatchInfo::Resolved { + name: "bar".into(), + version: "1.2.0".into(), + patches: vec!["/to/a.patch".into(), "/b.patch".into()], + })), + }, + "patched+https://crates.io/foo?name=bar&version=1.2.0&patch=%2Fto%2Fa.patch&patch=%2Fb.patch#bar@1.2.0", + ); } #[test] @@ -651,5 +685,17 @@ mod tests { err!("@1.2.3", ErrorKind::NameValidation(_)); err!("registry+https://github.com", ErrorKind::NameValidation(_)); err!("https://crates.io/1foo#1.2.3", ErrorKind::NameValidation(_)); + err!( + "patched+https://crates.io/foo?version=1.2.0&patch=%2Fb.patch#bar@1.2.0", + ErrorKind::PatchInfo(_) + ); + err!( + "patched+https://crates.io/foo?name=bar&patch=%2Fb.patch#bar@1.2.0", + ErrorKind::PatchInfo(_) + ); + err!( + "patched+https://crates.io/foo?name=bar&version=1.2.0&#bar@1.2.0", + ErrorKind::PatchInfo(_) + ); } } diff --git a/crates/cargo-util-schemas/src/core/source_kind.rs b/crates/cargo-util-schemas/src/core/source_kind.rs index 7b2ecaeec8c..53609636857 100644 --- a/crates/cargo-util-schemas/src/core/source_kind.rs +++ b/crates/cargo-util-schemas/src/core/source_kind.rs @@ -1,4 +1,5 @@ use std::cmp::Ordering; +use std::path::PathBuf; /// The possible kinds of code source. #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -15,6 +16,8 @@ pub enum SourceKind { LocalRegistry, /// A directory-based registry. Directory, + /// A source with paths to patch files (unstable). + Patched(PatchInfo), } impl SourceKind { @@ -27,6 +30,8 @@ impl SourceKind { SourceKind::SparseRegistry => None, SourceKind::LocalRegistry => Some("local-registry"), SourceKind::Directory => Some("directory"), + // Patched source URL already includes the `patched+` prefix, see `SourceId::new` + SourceKind::Patched(_) => None, } } } @@ -107,6 +112,10 @@ impl Ord for SourceKind { (SourceKind::Directory, _) => Ordering::Less, (_, SourceKind::Directory) => Ordering::Greater, + (SourceKind::Patched(a), SourceKind::Patched(b)) => a.cmp(b), + (SourceKind::Patched(_), _) => Ordering::Less, + (_, SourceKind::Patched(_)) => Ordering::Greater, + (SourceKind::Git(a), SourceKind::Git(b)) => a.cmp(b), } } @@ -199,3 +208,134 @@ impl<'a> std::fmt::Display for PrettyRef<'a> { Ok(()) } } + +/// Information to find the source package and patch files. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum PatchInfo { + /// The package to be patched is known and finalized. + Resolved { + /// Name and version of the package to be patched. + name: String, + /// Version of the package to be patched. + version: String, + /// Absolute paths to patch files. + /// + /// These are absolute to ensure Cargo can locate them in the patching phase. + patches: Vec, + }, + /// The package to be patched hasn't yet be resolved. Usually after a few + /// times of dependecy resolution, this will become [`PatchInfo::Resolved`]. + Deferred { + /// Absolute paths to patch files. + /// + /// These are absolute to ensure Cargo can locate them in the patching phase. + patches: Vec, + }, +} + +impl PatchInfo { + /// Collects patch information from query string. + /// + /// * `name` --- Package name + /// * `version` --- Package exact version + /// * `patch` --- Paths to patch files. Mutiple occurrences allowed. + pub fn from_query( + query_pairs: impl Iterator, impl AsRef)>, + ) -> Result { + let mut name = None; + let mut version = None; + let mut patches = Vec::new(); + for (k, v) in query_pairs { + let v = v.as_ref(); + match k.as_ref() { + "name" => name = Some(v.to_owned()), + "version" => version = Some(v.to_owned()), + "patch" => patches.push(PathBuf::from(v)), + _ => {} + } + } + let name = name.ok_or_else(|| PatchInfoError("name"))?; + let version = version.ok_or_else(|| PatchInfoError("version"))?; + if patches.is_empty() { + return Err(PatchInfoError("path")); + } + Ok(PatchInfo::Resolved { + name, + version, + patches, + }) + } + + /// As a URL query string. + pub fn as_query(&self) -> PatchInfoQuery<'_> { + PatchInfoQuery(self) + } + + pub fn finalize(self, name: String, version: String) -> Self { + match self { + PatchInfo::Deferred { patches } => PatchInfo::Resolved { + name, + version, + patches, + }, + _ => panic!("patch info has already finalized: {self:?}"), + } + } + + pub fn patches(&self) -> &[PathBuf] { + match self { + PatchInfo::Resolved { patches, .. } => patches.as_slice(), + PatchInfo::Deferred { patches } => patches.as_slice(), + } + } + + pub fn is_resolved(&self) -> bool { + matches!(self, PatchInfo::Resolved { .. }) + } +} + +/// A [`PatchInfo`] that can be `Display`ed as URL query string. +pub struct PatchInfoQuery<'a>(&'a PatchInfo); + +impl<'a> std::fmt::Display for PatchInfoQuery<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.0 { + PatchInfo::Resolved { + name, + version, + patches, + } => { + f.write_str("name=")?; + for value in url::form_urlencoded::byte_serialize(name.as_bytes()) { + write!(f, "{value}")?; + } + f.write_str("&version=")?; + for value in url::form_urlencoded::byte_serialize(version.as_bytes()) { + write!(f, "{value}")?; + } + if !patches.is_empty() { + f.write_str("&")?; + } + } + _ => {} + } + + let mut patches = self.0.patches().iter().peekable(); + while let Some(path) = patches.next() { + f.write_str("patch=")?; + let path = path.to_str().expect("utf8 patch").replace("\\", "/"); + for value in url::form_urlencoded::byte_serialize(path.as_bytes()) { + write!(f, "{value}")?; + } + if patches.peek().is_some() { + f.write_str("&")? + } + } + Ok(()) + } +} + +/// Error parsing patch info from URL query string. +#[derive(Debug, thiserror::Error)] +#[error("missing query string `{0}`")] +pub struct PatchInfoError(&'static str); diff --git a/crates/cargo-util-schemas/src/manifest/mod.rs b/crates/cargo-util-schemas/src/manifest/mod.rs index fe954f0f4ca..e9bcc003126 100644 --- a/crates/cargo-util-schemas/src/manifest/mod.rs +++ b/crates/cargo-util-schemas/src/manifest/mod.rs @@ -777,6 +777,13 @@ pub struct TomlDetailedDependency { #[serde(rename = "default_features")] pub default_features2: Option, pub package: Option, + /// `patches = [, ...]` for specifying patch files (unstable). + /// + /// Paths of patches are relative to the file it appears in. + /// If that's a `Cargo.toml`, they'll be relative to that TOML file, + /// and if it's a `.cargo/config.toml` file, they'll be relative to the + /// parent directory of that file. + pub patches: Option>, pub public: Option, /// One or more of `bin`, `cdylib`, `staticlib`, `bin:`. @@ -815,6 +822,7 @@ impl Default for TomlDetailedDependency

{ default_features: Default::default(), default_features2: Default::default(), package: Default::default(), + patches: Default::default(), public: Default::default(), artifact: Default::default(), lib: Default::default(), diff --git a/src/cargo/core/features.rs b/src/cargo/core/features.rs index f613bdf9094..9176ed542b3 100644 --- a/src/cargo/core/features.rs +++ b/src/cargo/core/features.rs @@ -513,6 +513,9 @@ features! { /// Allow multiple packages to participate in the same API namespace (unstable, open_namespaces, "", "reference/unstable.html#open-namespaces"), + + /// Allow patching dependencies with patch files. + (unstable, patch_files, "", "reference/unstable.html#patch-files"), } /// Status and metadata for a single unstable feature. @@ -775,6 +778,7 @@ unstable_cli_options!( next_lockfile_bump: bool, no_index_update: bool = ("Do not update the registry index even if the cache is outdated"), panic_abort_tests: bool = ("Enable support to run tests with -Cpanic=abort"), + patch_files: bool = ("Allow patching dependencies with patch files"), profile_rustflags: bool = ("Enable the `rustflags` option in profiles in .cargo/config.toml file"), public_dependency: bool = ("Respect a dependency's `public` field in Cargo.toml to control public/private dependencies"), publish_timeout: bool = ("Enable the `publish.timeout` key in .cargo/config.toml file"), @@ -1277,6 +1281,7 @@ impl CliUnstable { "mtime-on-use" => self.mtime_on_use = parse_empty(k, v)?, "no-index-update" => self.no_index_update = parse_empty(k, v)?, "panic-abort-tests" => self.panic_abort_tests = parse_empty(k, v)?, + "patch-files" => self.patch_files = parse_empty(k, v)?, "public-dependency" => self.public_dependency = parse_empty(k, v)?, "profile-rustflags" => self.profile_rustflags = parse_empty(k, v)?, "trim-paths" => self.trim_paths = parse_empty(k, v)?, diff --git a/src/cargo/core/registry.rs b/src/cargo/core/registry.rs index 780e0c4fe71..d97768fe2a0 100644 --- a/src/cargo/core/registry.rs +++ b/src/cargo/core/registry.rs @@ -180,6 +180,7 @@ pub type PatchDependency<'a> = (&'a Dependency, Option); /// Argument to [`PackageRegistry::patch`] which is information about a `[patch]` /// directive that we found in a lockfile, if present. +#[derive(Debug)] pub struct LockedPatchDependency { /// The original `Dependency` directive, except "locked" so it's version /// requirement is Locked to `foo` and its `SourceId` has a "precise" listed. @@ -517,6 +518,13 @@ impl<'gctx> PackageRegistry<'gctx> { &self.patches } + /// Removes all residual state of patches. + pub fn clear_patch(&mut self) { + self.patches = Default::default(); + self.patches_locked = false; + self.patches_available = Default::default(); + } + /// Loads the [`Source`] for a given [`SourceId`] to this registry, making /// them available to resolution. fn load(&mut self, source_id: SourceId, kind: Kind) -> CargoResult<()> { diff --git a/src/cargo/core/source_id.rs b/src/cargo/core/source_id.rs index d03a0a5769c..1eba458a53e 100644 --- a/src/cargo/core/source_id.rs +++ b/src/cargo/core/source_id.rs @@ -1,6 +1,7 @@ use crate::core::GitReference; use crate::core::PackageId; use crate::core::SourceKind; +use crate::sources::patched::PatchedSource; use crate::sources::registry::CRATES_IO_HTTP_INDEX; use crate::sources::source::Source; use crate::sources::{DirectorySource, CRATES_IO_DOMAIN, CRATES_IO_INDEX, CRATES_IO_REGISTRY}; @@ -8,6 +9,7 @@ use crate::sources::{GitSource, PathSource, RegistrySource}; use crate::util::interning::InternedString; use crate::util::{context, CanonicalUrl, CargoResult, GlobalContext, IntoUrl}; use anyhow::Context as _; +use cargo_util_schemas::core::PatchInfo; use serde::de; use serde::ser; use std::cmp::{self, Ordering}; @@ -176,6 +178,14 @@ impl SourceId { let url = url.into_url()?; SourceId::new(SourceKind::Path, url, None) } + "patched" => { + let mut url = url.into_url()?; + let patch_info = PatchInfo::from_query(url.query_pairs()) + .with_context(|| format!("parse `{url}`"))?; + url.set_fragment(None); + url.set_query(None); + SourceId::for_patches(SourceId::from_url(url.as_str())?, patch_info) + } kind => Err(anyhow::format_err!("unsupported source protocol: {}", kind)), } } @@ -245,6 +255,16 @@ impl SourceId { SourceId::new(SourceKind::Directory, url, None) } + pub fn for_patches(orig_source_id: SourceId, patch_info: PatchInfo) -> CargoResult { + let url = orig_source_id.as_encoded_url(); + // `Url::set_scheme` disallow conversions between non-special and speicial schemes, + // so parse the url from string again. + let url = format!("patched+{url}") + .parse() + .with_context(|| format!("cannot set patched scheme on `{url}`"))?; + SourceId::new(SourceKind::Patched(patch_info), url, None) + } + /// Returns the `SourceId` corresponding to the main repository. /// /// This is the main cargo registry by default, but it can be overridden in @@ -376,6 +396,11 @@ impl SourceId { matches!(self.inner.kind, SourceKind::Git(_)) } + /// Returns `true` if this source is patched by patch files. + pub fn is_patched(self) -> bool { + matches!(self.inner.kind, SourceKind::Patched(_)) + } + /// Creates an implementation of `Source` corresponding to this ID. /// /// * `yanked_whitelist` --- Packages allowed to be used, even if they are yanked. @@ -419,6 +444,7 @@ impl SourceId { .expect("path sources cannot be remote"); Ok(Box::new(DirectorySource::new(&path, self, gctx))) } + SourceKind::Patched(_) => Ok(Box::new(PatchedSource::new(self, gctx)?)), } } @@ -430,6 +456,14 @@ impl SourceId { } } + /// Gets the patch information if this is a patched source, otherwise `None`. + pub fn patch_info(self) -> Option<&'static PatchInfo> { + match &self.inner.kind { + SourceKind::Patched(i) => Some(i), + _ => None, + } + } + /// Check if the precise data field has bean set pub fn has_precise(self) -> bool { self.inner.precise.is_some() @@ -665,6 +699,14 @@ impl fmt::Display for SourceId { } SourceKind::LocalRegistry => write!(f, "registry `{}`", url_display(&self.inner.url)), SourceKind::Directory => write!(f, "dir {}", url_display(&self.inner.url)), + SourceKind::Patched(ref patch_info) => { + let n = patch_info.patches().len(); + let plural = if n == 1 { "" } else { "s" }; + if let PatchInfo::Resolved { name, version, .. } = &patch_info { + write!(f, "{name}@{version} ")?; + } + write!(f, "with {n} patch file{plural}") + } } } } @@ -730,6 +772,14 @@ impl<'a> fmt::Display for SourceIdAsUrl<'a> { write!(f, "#{}", precise)?; } } + + if let SourceIdInner { + kind: SourceKind::Patched(patch_info), + .. + } = &self.inner + { + write!(f, "?{}", patch_info.as_query())?; + } Ok(()) } } @@ -806,6 +856,8 @@ mod tests { use std::hash::Hasher; use std::path::Path; + use cargo_util_schemas::core::PatchInfo; + let gen_hash = |source_id: SourceId| { let mut hasher = std::collections::hash_map::DefaultHasher::new(); source_id.stable_hash(Path::new("/tmp/ws"), &mut hasher); @@ -850,6 +902,16 @@ mod tests { let source_id = SourceId::for_directory(path).unwrap(); assert_eq!(gen_hash(source_id), 17459999773908528552); assert_eq!(crate::util::hex::short_hash(&source_id), "6568fe2c2fab5bfe"); + + let patch_info = PatchInfo::Resolved { + name: "foo".into(), + version: "1.0.0".into(), + patches: vec![path.into()], + }; + let registry_source_id = SourceId::for_registry(&url).unwrap(); + let source_id = SourceId::for_patches(registry_source_id, patch_info).unwrap(); + assert_eq!(gen_hash(source_id), 15459318675065232737); + assert_eq!(crate::util::hex::short_hash(&source_id), "87ca345b36470e4d"); } #[test] diff --git a/src/cargo/core/workspace.rs b/src/cargo/core/workspace.rs index 4ac8777bd62..16baf6df16a 100644 --- a/src/cargo/core/workspace.rs +++ b/src/cargo/core/workspace.rs @@ -484,7 +484,7 @@ impl<'gctx> Workspace<'gctx> { })?, }; patch.insert( - url, + url.clone(), deps.iter() .map(|(name, dep)| { crate::util::toml::to_dependency( @@ -498,6 +498,7 @@ impl<'gctx> Workspace<'gctx> { // any relative paths are resolved before they'd be joined with root. Path::new("unused-relative-path"), /* kind */ None, + &url, ) }) .collect::>>()?, diff --git a/src/cargo/ops/resolve.rs b/src/cargo/ops/resolve.rs index 65b3f2b6814..381acdd14a3 100644 --- a/src/cargo/ops/resolve.rs +++ b/src/cargo/ops/resolve.rs @@ -74,6 +74,7 @@ use crate::core::SourceId; use crate::core::Workspace; use crate::ops; use crate::sources::RecursivePathSource; +use crate::sources::CRATES_IO_INDEX; use crate::util::cache_lock::CacheLockMode; use crate::util::errors::CargoResult; use crate::util::CanonicalUrl; @@ -81,6 +82,7 @@ use anyhow::Context as _; use cargo_util::paths; use std::collections::{HashMap, HashSet}; use tracing::{debug, trace}; +use url::Url; /// Filter for keep using Package ID from previous lockfile. type Keep<'a> = &'a dyn Fn(&PackageId) -> bool; @@ -330,6 +332,53 @@ pub fn resolve_with_previous<'gctx>( keep_previous: Option>, specs: &[PackageIdSpec], register_patches: bool, +) -> CargoResult { + let mut patches = if register_patches { + Some(PatchEntries::from_workspace(ws, previous)?) + } else { + None + }; + + loop { + let resolved = resolve_with_previous_inner( + registry, + ws, + cli_features, + has_dev_units, + previous, + keep_previous, + specs, + patches.as_ref(), + )?; + + if let Some(patches) = patches.as_mut() { + if patches.requires_reresolve(&resolved)? { + let times = patches.resolve_times; + debug!(times, "re-resolve due to new patches ready"); + continue; + } + } + + if let Some(patches) = patches.as_ref() { + emit_warnings_of_unused_patches(ws, &resolved, registry)?; + emit_warnings_of_unresolved_patches(ws, &resolved, patches)?; + } + + return Ok(resolved); + } +} + +/// Helper for re-resolve for [`resolve_with_previous`]. +#[tracing::instrument(skip_all)] +fn resolve_with_previous_inner<'gctx>( + registry: &mut PackageRegistry<'gctx>, + ws: &Workspace<'gctx>, + cli_features: &CliFeatures, + has_dev_units: HasDevUnits, + previous: Option<&Resolve>, + keep_previous: Option<&dyn Fn(&PackageId) -> bool>, + specs: &[PackageIdSpec], + patches: Option<&PatchEntries>, ) -> CargoResult { // We only want one Cargo at a time resolving a crate graph since this can // involve a lot of frobbing of the global caches. @@ -367,11 +416,13 @@ pub fn resolve_with_previous<'gctx>( version_prefs.max_rust_version(Some(rust_version)); } - let avoid_patch_ids = if register_patches { - register_patch_entries(registry, ws, previous, &mut version_prefs, keep_previous)? - } else { - HashSet::new() - }; + let avoid_patch_ids = register_patch_entries( + registry, + patches, + previous, + &mut version_prefs, + keep_previous, + )?; // Refine `keep` with patches that should avoid locking. let keep = |p: &PackageId| keep_previous(p) && !avoid_patch_ids.contains(p); @@ -394,7 +445,7 @@ pub fn resolve_with_previous<'gctx>( } } - if register_patches { + if patches.is_some() { registry.lock_patches(); } @@ -426,12 +477,7 @@ pub fn resolve_with_previous<'gctx>( Some(ws.gctx()), )?; - let patches = registry.patches().values().flat_map(|v| v.iter()); - resolved.register_used_patches(patches); - - if register_patches && !resolved.unused_patches().is_empty() { - emit_warnings_of_unused_patches(ws, &resolved, registry)?; - } + resolved.register_used_patches(registry.patches().values().flat_map(|v| v.iter())); if let Some(previous) = previous { resolved.merge_from(previous)?; @@ -797,6 +843,58 @@ fn emit_warnings_of_unused_patches( return Ok(()); } +/// Emits warnings of unresolved patches case by case. +/// +/// Patches from [`PatchEntries::unresolved`] have not yet get a "resolved", +/// e.g. don't know which versions of packages, hence we can't use +/// [`emit_warnings_of_unused_patches`] which requires summaries. +fn emit_warnings_of_unresolved_patches( + ws: &Workspace<'_>, + resolve: &Resolve, + patches: &PatchEntries, +) -> CargoResult<()> { + let resolved_patched_pkg_ids: Vec<_> = resolve + .iter() + .filter(|pkg_id| pkg_id.source_id().is_patched()) + .collect(); + for (url, patches) in &patches.unresolved { + let canonical_url = CanonicalUrl::new(url)?; + for patch in patches { + let maybe_already_used = resolved_patched_pkg_ids + .iter() + .filter(|id| { + let source_id = id.source_id(); + let url = source_id.canonical_url().raw_canonicalized_url(); + // To check if the patch is for the same source, + // we need strip `patched` prefix here, + // as CanonicalUrl preserves that. + let url = url.as_str().strip_prefix("patched+").unwrap(); + url == canonical_url.raw_canonicalized_url().as_str() + }) + .any(|id| patch.matches_ignoring_source(*id)); + + if maybe_already_used { + continue; + } + + let url = if url.as_str() == CRATES_IO_INDEX { + "crates-io" + } else { + url.as_str() + }; + + let msg = format!( + "unused patch `{}@{}` {} in `{url}`", + patch.name_in_toml(), + patch.version_req(), + patch.source_id(), + ); + ws.gctx().shell().warn(msg)?; + } + } + Ok(()) +} + /// Informs `registry` and `version_pref` that `[patch]` entries are available /// and preferable for the dependency resolution. /// @@ -806,14 +904,20 @@ fn emit_warnings_of_unused_patches( #[tracing::instrument(level = "debug", skip_all, ret)] fn register_patch_entries( registry: &mut PackageRegistry<'_>, - ws: &Workspace<'_>, + patches: Option<&PatchEntries>, previous: Option<&Resolve>, version_prefs: &mut VersionPreferences, keep_previous: Keep<'_>, ) -> CargoResult> { let mut avoid_patch_ids = HashSet::new(); - for (url, patches) in ws.root_patch()?.iter() { - for patch in patches { + let Some(patches) = patches else { + return Ok(avoid_patch_ids); + }; + + registry.clear_patch(); + + for (url, patches) in patches.iter() { + for patch in patches.iter() { version_prefs.prefer_dependency(patch.clone()); } let Some(previous) = previous else { @@ -832,7 +936,7 @@ fn register_patch_entries( // previous resolve graph, which is primarily what's done here to // build the `registrations` list. let mut registrations = Vec::new(); - for dep in patches { + for dep in patches.iter() { let candidates = || { previous .iter() @@ -941,3 +1045,205 @@ fn lock_replacements( }; replace } + +/// All `[patch]` from a workspace, including patches from mainfests and configurations. +/// +/// This keeps track of patches that are fully resolved and ready to be applied, +/// as well as patches that are pending resolution. +#[derive(Debug, Default)] +struct PatchEntries { + /// Patches that have been resolved and are ready to be applied. + /// + /// All patches fall into this, except file `[patch]`, which need delay appalications. + ready_patches: HashMap>, + /// Patches that are pending resolution. + /// + /// Only after a few dependency resolution happen, can a `[patch]` entry + /// with patch files know which version it is going to patch. Hence we need + /// to "delay" the patch process until at least one resolution is done, and + /// re-resolves the graph is any patch become applicable with the newly + /// resolved dependency graph. + unresolved: Vec<(Url, Vec)>, + + from_previous: HashMap>, + + /// How many times [`PatchEntries::requires_reresolve`] has been called. + resolve_times: u32, +} + +impl PatchEntries { + const RESOLVE_TIMES_LIMIT: u32 = 20; + + fn from_workspace(ws: &Workspace<'_>, previous: Option<&Resolve>) -> CargoResult { + let mut ready_patches = HashMap::new(); + let mut unresolved = Vec::new(); + for (url, patches) in ws.root_patch()?.into_iter() { + let (source_patches, file_patches): (Vec<_>, Vec<_>) = patches + .into_iter() + .partition(|p| !p.source_id().is_patched()); + if !source_patches.is_empty() { + ready_patches.insert(url.clone(), source_patches); + } + if !file_patches.is_empty() { + unresolved.push((url, file_patches)); + } + } + + let mut from_previous = HashMap::>::new(); + let resolved_patched_pkg_ids: Vec<_> = previous + .iter() + .flat_map(|previous| { + previous + .iter() + .chain(previous.unused_patches().iter().cloned()) + }) + .filter(|pkg_id| { + pkg_id + .source_id() + .patch_info() + .map(|info| info.is_resolved()) + .unwrap_or_default() + }) + .collect(); + for (url, patches) in &unresolved { + let mut ready_patches = Vec::new(); + let canonical_url = CanonicalUrl::new(url)?; + for patch in patches { + for pkg_id in resolved_patched_pkg_ids.iter().filter(|id| { + let source_id = id.source_id(); + let url = source_id.canonical_url().raw_canonicalized_url(); + // To check if the patch is for the same source, + // we need strip `patched` prefix here, + // as CanonicalUrl preserves that. + let url = url.as_str().strip_prefix("patched+").unwrap(); + url == canonical_url.raw_canonicalized_url().as_str() + }) { + if patch.matches_ignoring_source(*pkg_id) { + let mut patch = patch.clone(); + patch.set_source_id(pkg_id.source_id()); + patch.lock_to(*pkg_id); + ready_patches.push(LockedPatchDependency { + dependency: patch, + package_id: *pkg_id, + alt_package_id: None, + }); + } + } + } + if !ready_patches.is_empty() { + from_previous + .entry(url.clone()) + .or_default() + .extend(ready_patches); + } + } + + Ok(PatchEntries { + ready_patches, + unresolved, + from_previous, + resolve_times: 0, + }) + } + + /// Returns true when there are new patches ready to apply. + fn requires_reresolve(&mut self, resolve: &Resolve) -> CargoResult { + if self.resolve_times >= PatchEntries::RESOLVE_TIMES_LIMIT { + anyhow::bail!("patching recursion appears to never reach a fixed point"); + } + + let mut has_new_patches = false; + for (url, unresolved) in self.unresolved.iter_mut() { + let canonical_url = CanonicalUrl::new(url)?; + let mut ready_patches = Vec::new(); + let mut pending_patches = Vec::new(); + for patch in unresolved.iter() { + // TODO: Performance + let mut matched = false; + for pkg_id in resolve + .iter() + .filter(|id| id.source_id().canonical_url() == &canonical_url) + { + if patch.matches_ignoring_source(pkg_id) { + has_new_patches |= true; + let mut patch = patch.clone(); + let name = pkg_id.name(); + let version = pkg_id.version(); + let patch_info = patch + .source_id() + .patch_info() + .expect("patched source must have patch info") + .clone() + .finalize(name.to_string(), version.to_string()); + let new_source_id = SourceId::for_patches(pkg_id.source_id(), patch_info)?; + patch.set_source_id(new_source_id); + patch.lock_version(version); + ready_patches.push(patch); + } + matched |= true; + } + // Match nothing. Still pending. + if !matched { + pending_patches.push(patch.clone()); + } + } + // Keep pending patches as unresolved for the next iteration, if any + *unresolved = pending_patches; + + if !ready_patches.is_empty() { + if let Some(from_previous) = self.from_previous.get_mut(url) { + for patch in &ready_patches { + if let Some(index) = from_previous + .iter() + .position(|locked| patch.matches_ignoring_source(locked.package_id)) + { + from_previous.swap_remove(index); + } + } + } + self.ready_patches + .entry(url.clone()) + .or_default() + .extend(ready_patches); + } + } + + self.resolve_times += 1; + Ok(has_new_patches) + } + + fn iter(&self) -> Box)> + '_> { + if self.ready_patches.is_empty() { + Box::new(self.from_previous.iter().map(|(url, deps)| { + let deps = Deps { + deps: None, + locked_deps: Some(deps), + }; + (url, deps) + })) + } else { + Box::new(self.ready_patches.iter().map(|(url, deps)| { + let deps = Deps { + deps: Some(deps), + locked_deps: self.from_previous.get(url), + }; + (url, deps) + })) + } + } +} + +struct Deps<'a> { + deps: Option<&'a Vec>, + locked_deps: Option<&'a Vec>, +} + +impl<'a> Deps<'a> { + fn iter(&self) -> impl Iterator { + let locked_deps = self + .locked_deps + .into_iter() + .flat_map(|deps| deps.into_iter().map(|locked| &locked.dependency)); + self.deps.into_iter().flatten().chain(locked_deps) + } +} diff --git a/src/cargo/sources/mod.rs b/src/cargo/sources/mod.rs index 9c98cc49eaa..7a181f83aec 100644 --- a/src/cargo/sources/mod.rs +++ b/src/cargo/sources/mod.rs @@ -40,6 +40,7 @@ pub mod config; pub mod directory; pub mod git; pub mod overlay; +pub mod patched; pub mod path; pub mod registry; pub mod replaced; diff --git a/src/cargo/sources/patched.rs b/src/cargo/sources/patched.rs new file mode 100644 index 00000000000..3ff7dd795c8 --- /dev/null +++ b/src/cargo/sources/patched.rs @@ -0,0 +1,351 @@ +//! A source that takes other source and patches it with local patch files. +//! See [`PatchedSource`] for details. + +use std::path::Path; +use std::path::PathBuf; +use std::task::Poll; + +use anyhow::Context as _; +use cargo_util::paths; +use cargo_util::ProcessBuilder; +use cargo_util::Sha256; +use cargo_util_schemas::core::PatchInfo; +use cargo_util_schemas::core::SourceKind; +use lazycell::LazyCell; + +use crate::core::Dependency; +use crate::core::Package; +use crate::core::PackageId; +use crate::core::SourceId; +use crate::core::Verbosity; +use crate::sources::source::MaybePackage; +use crate::sources::source::QueryKind; +use crate::sources::source::Source; +use crate::sources::IndexSummary; +use crate::sources::PathSource; +use crate::sources::SourceConfigMap; +use crate::util::cache_lock::CacheLockMode; +use crate::util::hex; +use crate::util::OptVersionReq; +use crate::CargoResult; +use crate::GlobalContext; + +/// A file indicates that if present, the patched source is ready to use. +const READY_LOCK: &str = ".cargo-ok"; + +/// `PatchedSource` is a source that, when fetching, it patches a paticular +/// package with given local patch files. +/// +/// This could only be created from [the `[patch]` section][patch] with any +/// entry carrying `{ .., patches = ["..."] }` field. Other kinds of dependency +/// sections (normal, dev, build) shouldn't allow to create any `PatchedSource`. +/// +/// [patch]: https://doc.rust-lang.org/nightly/cargo/reference/overriding-dependencies.html#the-patch-section +/// +/// ## Filesystem layout +/// +/// When Cargo fetches a package from a `PatchedSource`, it'll copy everything +/// from the original source to a dedicated patched source directory. That +/// directory is located under `$CARGO_HOME`. The patched source of each package +/// would be put under: +/// +/// ```text +/// $CARGO_HOME/patched-src//-//`. +/// ``` +/// +/// The file tree of the patched source directory roughly looks like: +/// +/// ```text +/// $CARGO_HOME/patched-src/github.com-6d038ece37e82ae2 +/// ├── gimli-0.29.0/ +/// │ ├── a0d193bd15a5ed96/ # checksum of all patch files from a patch to gimli@0.29.0 +/// │ ├── c58e1db3de7c154d/ +/// └── serde-1.0.197/ +/// └── deadbeef12345678/ +/// ``` +/// +/// ## `SourceId` for tracking the original package +/// +/// Due to the nature that a patched source is actually locked to a specific +/// version of one package, the SourceId URL of a `PatchedSource` needs to +/// carry such information. It looks like: +/// +/// ```text +/// patched+registry+https://github.com/rust-lang/crates.io-index?name=foo&version=1.0.0&patch=0001-bugfix.patch +/// ``` +/// +/// where the `patched+` protocol is essential for Cargo to distinguish between +/// a patched source and the source it patches. The query string contains the +/// name and version of the package being patched. We want patches to be as +/// reproducible as it could, so lock to one specific version here. +/// See [`PatchInfo::from_query`] to learn what are being tracked. +/// +/// To achieve it, the version specified in any of the entry in `[patch]` must +/// be an exact version via the `=` SemVer comparsion operator. For example, +/// this will fetch source of serde@1.2.3 from crates.io, and apply patches to it. +/// +/// ```toml +/// [patch.crates-io] +/// serde = { version = "=1.2.3", patches = ["patches/0001-serde-bug.patch"] } +/// ``` +/// +/// ## Patch tools +/// +/// When patching a package, Cargo will change the working directory to +/// the root directory of the copied source code, and then execute the tool +/// specified via the `patchtool.path` config value in the Cargo configuration. +/// Paths of patch files will be provided as absolute paths to the tool. +pub struct PatchedSource<'gctx> { + source_id: SourceId, + /// The source of the package we're going to patch. + original_source: Box, + /// Checksum from all patch files. + patches_checksum: LazyCell, + /// For respecting `[source]` replacement configuration. + map: SourceConfigMap<'gctx>, + path_source: Option>, + quiet: bool, + gctx: &'gctx GlobalContext, +} + +impl<'gctx> PatchedSource<'gctx> { + pub fn new( + source_id: SourceId, + gctx: &'gctx GlobalContext, + ) -> CargoResult> { + let original_id = { + let mut url = source_id.url().clone(); + url.set_query(None); + url.set_fragment(None); + let url = url.as_str(); + let Some(url) = url.strip_prefix("patched+") else { + anyhow::bail!("patched source url requires a `patched` scheme, got `{url}`"); + }; + SourceId::from_url(&url)? + }; + let map = SourceConfigMap::new(gctx)?; + let source = PatchedSource { + source_id, + original_source: map.load(original_id, &Default::default())?, + patches_checksum: LazyCell::new(), + map, + path_source: None, + quiet: false, + gctx, + }; + Ok(source) + } + + /// Downloads the package source if needed. + fn download_pkg(&mut self) -> CargoResult { + let PatchInfo::Resolved { name, version, .. } = self.patch_info() else { + panic!("patched source `{}` must be resolved", self.describe()); + }; + let exact_req = &format!("={version}"); + let original_id = self.original_source.source_id(); + let dep = Dependency::parse(name, Some(exact_req), original_id)?; + let pkg_id = loop { + match self.original_source.query_vec(&dep, QueryKind::Exact) { + Poll::Ready(deps) => break deps?.remove(0).as_summary().package_id(), + Poll::Pending => self.original_source.block_until_ready()?, + } + }; + + let source = self.map.load(original_id, &Default::default())?; + Box::new(source).download_now(pkg_id, self.gctx) + } + + fn copy_pkg_src(&self, pkg: &Package, dst: &Path) -> CargoResult<()> { + let src = pkg.root(); + for entry in walkdir::WalkDir::new(src) { + let entry = entry?; + let path = entry.path().strip_prefix(src).unwrap(); + let src = entry.path(); + let dst = dst.join(path); + if entry.file_type().is_dir() { + paths::create_dir_all(dst)?; + } else { + // TODO: handle symlink? + paths::copy(src, dst)?; + } + } + Ok(()) + } + + fn apply_patches(&self, pkg: &Package, dst: &Path) -> CargoResult<()> { + let patches = self.patch_info().patches(); + let n = patches.len(); + assert!(n > 0, "must have at least one patch, got {n}"); + + self.gctx.shell().status("Patching", pkg)?; + + let patchtool_config = self.gctx.patchtool_config()?; + let Some(tool) = patchtool_config.path.as_ref() else { + anyhow::bail!("missing `[patchtool]` for patching dependencies"); + }; + + let program = tool.path.resolve_program(self.gctx); + let mut cmd = ProcessBuilder::new(program); + cmd.cwd(dst).args(&tool.args); + + for patch_path in patches { + let patch_path = self.gctx.cwd().join(patch_path); + let mut cmd = cmd.clone(); + cmd.arg(patch_path); + if matches!(self.gctx.shell().verbosity(), Verbosity::Verbose) { + self.gctx.shell().status("Running", &cmd)?; + cmd.exec()?; + } else { + cmd.exec_with_output()?; + } + } + + Ok(()) + } + + /// Gets the destination directory we put the patched source at. + fn dest_src_dir(&self, pkg: &Package) -> CargoResult { + let patched_src_root = self.gctx.patched_source_path(); + let patched_src_root = self + .gctx + .assert_package_cache_locked(CacheLockMode::DownloadExclusive, &patched_src_root); + let pkg_id = pkg.package_id(); + let source_id = pkg_id.source_id(); + let ident = source_id.url().host_str().unwrap_or_default(); + let hash = hex::short_hash(&source_id); + let name = pkg_id.name(); + let version = pkg_id.version(); + let mut dst = patched_src_root.join(format!("{ident}-{hash}")); + dst.push(format!("{name}-{version}")); + dst.push(self.patches_checksum()?); + Ok(dst) + } + + fn patches_checksum(&self) -> CargoResult<&String> { + self.patches_checksum.try_borrow_with(|| { + let mut cksum = Sha256::new(); + for patch in self.patch_info().patches() { + cksum.update_path(patch)?; + } + let mut cksum = cksum.finish_hex(); + // TODO: is it safe to truncate sha256? + cksum.truncate(16); + Ok(cksum) + }) + } + + fn patch_info(&self) -> &PatchInfo { + let SourceKind::Patched(info) = self.source_id.kind() else { + panic!("patched source must be SourceKind::Patched"); + }; + info + } +} + +impl<'gctx> Source for PatchedSource<'gctx> { + fn source_id(&self) -> SourceId { + self.source_id + } + + fn supports_checksums(&self) -> bool { + false + } + + fn requires_precise(&self) -> bool { + false + } + + fn query( + &mut self, + dep: &Dependency, + kind: QueryKind, + f: &mut dyn FnMut(IndexSummary), + ) -> Poll> { + // Version requirement here is still the `=` exact one for fetching + // the source to patch, so switch it to a wildchard requirement. + // It is safe because this source contains one and the only package. + let mut dep = dep.clone(); + dep.set_version_req(OptVersionReq::Any); + if let Some(src) = self.path_source.as_mut() { + src.query(&dep, kind, f) + } else { + Poll::Pending + } + } + + fn invalidate_cache(&mut self) { + // No cache for a patched source + } + + fn set_quiet(&mut self, quiet: bool) { + self.quiet = quiet; + } + + fn download(&mut self, id: PackageId) -> CargoResult { + self.path_source + .as_mut() + .expect("path source must exist") + .download(id) + } + + fn finish_download(&mut self, _pkg_id: PackageId, _contents: Vec) -> CargoResult { + panic!("no download should have started") + } + + fn fingerprint(&self, pkg: &Package) -> CargoResult { + let fingerprint = self.original_source.fingerprint(pkg)?; + let cksum = self.patches_checksum()?; + Ok(format!("{fingerprint}/{cksum}")) + } + + fn describe(&self) -> String { + use std::fmt::Write as _; + let mut desc = self.original_source.describe(); + let n = self.patch_info().patches().len(); + let plural = if n == 1 { "" } else { "s" }; + write!(desc, " with {n} patch file{plural}").unwrap(); + desc + } + + fn add_to_yanked_whitelist(&mut self, _pkgs: &[PackageId]) { + // There is no yanked package for a patched source + } + + fn is_yanked(&mut self, _pkg: PackageId) -> Poll> { + // There is no yanked package for a patched source + Poll::Ready(Ok(false)) + } + + fn block_until_ready(&mut self) -> CargoResult<()> { + if self.path_source.is_some() { + return Ok(()); + } + + let pkg = self.download_pkg().context("failed to download source")?; + let dst = self.dest_src_dir(&pkg)?; + + let ready_lock = dst.join(READY_LOCK); + let cksum = self.patches_checksum()?; + match paths::read(&ready_lock) { + Ok(prev_cksum) if &prev_cksum == cksum => { + // We've applied patches. Assume they never change. + } + _ => { + // Either we were interrupted, or never get started. + // We just start over here. + if let Err(e) = paths::remove_dir_all(&dst) { + tracing::trace!("failed to remove `{}`: {e}", dst.display()); + } + self.copy_pkg_src(&pkg, &dst) + .context("failed to copy source")?; + self.apply_patches(&pkg, &dst) + .context("failed to apply patches")?; + paths::write(&ready_lock, cksum)?; + } + } + + self.path_source = Some(PathSource::new(&dst, self.source_id, self.gctx)); + + Ok(()) + } +} diff --git a/src/cargo/util/canonical_url.rs b/src/cargo/util/canonical_url.rs index 7516e035691..4981ab2eadd 100644 --- a/src/cargo/util/canonical_url.rs +++ b/src/cargo/util/canonical_url.rs @@ -39,7 +39,12 @@ impl CanonicalUrl { // almost certainly not using the same case conversion rules that GitHub // does. (See issue #84) if url.host_str() == Some("github.com") { - url = format!("https{}", &url[url::Position::AfterScheme..]) + let proto = if url.scheme().starts_with("patched+") { + "patched+https" + } else { + "https" + }; + url = format!("{proto}{}", &url[url::Position::AfterScheme..]) .parse() .unwrap(); let path = url.path().to_lowercase(); diff --git a/src/cargo/util/context/mod.rs b/src/cargo/util/context/mod.rs index c4fa1a5947d..ec096f24e75 100644 --- a/src/cargo/util/context/mod.rs +++ b/src/cargo/util/context/mod.rs @@ -228,6 +228,7 @@ pub struct GlobalContext { doc_extern_map: LazyCell, progress_config: ProgressConfig, env_config: LazyCell, + patchtool_config: LazyCell, /// This should be false if: /// - this is an artifact of the rustc distribution process for "stable" or for "beta" /// - this is an `#[test]` that does not opt in with `enable_nightly_features` @@ -322,6 +323,7 @@ impl GlobalContext { doc_extern_map: LazyCell::new(), progress_config: ProgressConfig::default(), env_config: LazyCell::new(), + patchtool_config: LazyCell::new(), nightly_features_allowed: matches!(&*features::channel(), "nightly" | "dev"), ws_roots: RefCell::new(HashMap::new()), global_cache_tracker: LazyCell::new(), @@ -400,6 +402,11 @@ impl GlobalContext { self.registry_base_path().join("src") } + /// Gets the directory containg patched package sources (`/patched-src`). + pub fn patched_source_path(&self) -> Filesystem { + self.home_path.join("patched-src") + } + /// Gets the default Cargo registry. pub fn default_registry(&self) -> CargoResult> { Ok(self @@ -1860,6 +1867,11 @@ impl GlobalContext { Ok(env_config) } + pub fn patchtool_config(&self) -> CargoResult<&PatchtoolConfig> { + self.patchtool_config + .try_borrow_with(|| self.get::("patchtool")) + } + /// This is used to validate the `term` table has valid syntax. /// /// This is necessary because loading the term settings happens very @@ -2778,6 +2790,12 @@ where deserializer.deserialize_option(ProgressVisitor) } +#[derive(Debug, Default, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct PatchtoolConfig { + pub path: Option, +} + #[derive(Debug)] enum EnvConfigValueInner { Simple(String), diff --git a/src/cargo/util/toml/mod.rs b/src/cargo/util/toml/mod.rs index 2fa704a8b87..7a6c6429393 100644 --- a/src/cargo/util/toml/mod.rs +++ b/src/cargo/util/toml/mod.rs @@ -1,4 +1,5 @@ use annotate_snippets::{Level, Snippet}; +use cargo_util_schemas::core::PatchInfo; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::ffi::OsStr; use std::path::{Path, PathBuf}; @@ -31,6 +32,7 @@ use crate::sources::{CRATES_IO_INDEX, CRATES_IO_REGISTRY}; use crate::util::errors::{CargoResult, ManifestError}; use crate::util::interning::InternedString; use crate::util::lints::{get_span, rel_cwd_manifest_path}; +use crate::util::CanonicalUrl; use crate::util::{self, context::ConfigRelativePath, GlobalContext, IntoUrl, OptVersionReq}; mod embedded; @@ -1344,7 +1346,7 @@ fn to_real_manifest( )?; } let replace = replace(&resolved_toml, &mut manifest_ctx)?; - let patch = patch(&resolved_toml, &mut manifest_ctx)?; + let patch = patch(&resolved_toml, &mut manifest_ctx, &features)?; { let mut names_sources = BTreeMap::new(); @@ -1700,7 +1702,7 @@ fn to_virtual_manifest( }; ( replace(&original_toml, &mut manifest_ctx)?, - patch(&original_toml, &mut manifest_ctx)?, + patch(&original_toml, &mut manifest_ctx, &features)?, ) }; if let Some(profiles) = &original_toml.profile { @@ -1779,7 +1781,7 @@ fn gather_dependencies( for (n, v) in dependencies.iter() { let resolved = v.resolved().expect("previously resolved"); - let dep = dep_to_dependency(&resolved, n, manifest_ctx, kind)?; + let dep = dep_to_dependency(&resolved, n, manifest_ctx, kind, None)?; manifest_ctx.deps.push(dep); } Ok(()) @@ -1813,7 +1815,7 @@ fn replace( ); } - let mut dep = dep_to_dependency(replacement, spec.name(), manifest_ctx, None)?; + let mut dep = dep_to_dependency(replacement, spec.name(), manifest_ctx, None, None)?; let version = spec.version().ok_or_else(|| { anyhow!( "replacements must specify a version \ @@ -1836,7 +1838,9 @@ fn replace( fn patch( me: &manifest::TomlManifest, manifest_ctx: &mut ManifestContext<'_, '_>, + features: &Features, ) -> CargoResult>> { + let patch_files_enabled = features.require(Feature::patch_files()).is_ok(); let mut patch = HashMap::new(); for (toml_url, deps) in me.patch.iter().flatten() { let url = match &toml_url[..] { @@ -1853,7 +1857,7 @@ fn patch( })?, }; patch.insert( - url, + url.clone(), deps.iter() .map(|(name, dep)| { unused_dep_keys( @@ -1862,7 +1866,13 @@ fn patch( dep.unused_keys(), &mut manifest_ctx.warnings, ); - dep_to_dependency(dep, name, manifest_ctx, None) + dep_to_dependency( + dep, + name, + manifest_ctx, + None, + Some((&url, patch_files_enabled)), + ) }) .collect::>>()?, ); @@ -1870,6 +1880,7 @@ fn patch( Ok(patch) } +/// Transforms a `patch` entry to a [`Dependency`]. pub(crate) fn to_dependency( dep: &manifest::TomlDependency

, name: &str, @@ -1879,20 +1890,18 @@ pub(crate) fn to_dependency( platform: Option, root: &Path, kind: Option, + patch_source_url: &Url, ) -> CargoResult { - dep_to_dependency( - dep, - name, - &mut ManifestContext { - deps: &mut Vec::new(), - source_id, - gctx, - warnings, - platform, - root, - }, - kind, - ) + let manifest_ctx = &mut ManifestContext { + deps: &mut Vec::new(), + source_id, + gctx, + warnings, + platform, + root, + }; + let patch_source_url = Some((patch_source_url, gctx.cli_unstable().patch_files)); + dep_to_dependency(dep, name, manifest_ctx, kind, patch_source_url) } fn dep_to_dependency( @@ -1900,6 +1909,7 @@ fn dep_to_dependency( name: &str, manifest_ctx: &mut ManifestContext<'_, '_>, kind: Option, + patch_source_url: Option<(&Url, bool)>, ) -> CargoResult { match *orig { manifest::TomlDependency::Simple(ref version) => detailed_dep_to_dependency( @@ -1910,9 +1920,10 @@ fn dep_to_dependency( name, manifest_ctx, kind, + patch_source_url, ), manifest::TomlDependency::Detailed(ref details) => { - detailed_dep_to_dependency(details, name, manifest_ctx, kind) + detailed_dep_to_dependency(details, name, manifest_ctx, kind, patch_source_url) } } } @@ -1922,8 +1933,11 @@ fn detailed_dep_to_dependency( name_in_toml: &str, manifest_ctx: &mut ManifestContext<'_, '_>, kind: Option, + patch_source_url: Option<(&Url, bool)>, ) -> CargoResult { - if orig.version.is_none() && orig.path.is_none() && orig.git.is_none() { + let no_source_specified = orig.version.is_none() && orig.path.is_none() && orig.git.is_none(); + let contains_file_patches = patch_source_url.is_some() && orig.patches.is_some(); + if no_source_specified && !contains_file_patches { anyhow::bail!( "dependency ({name_in_toml}) specified without \ providing a local path, Git repository, version, or \ @@ -2057,6 +2071,11 @@ fn detailed_dep_to_dependency( ) } } + + if let Some(source_id) = patched_source_id(orig, manifest_ctx, &dep, patch_source_url)? { + dep.set_source_id(source_id); + } + Ok(dep) } @@ -2145,6 +2164,70 @@ fn to_dependency_source_id( } } +// Handle `patches` field for `[patch]` table, if any. +fn patched_source_id( + orig: &manifest::TomlDetailedDependency

, + manifest_ctx: &mut ManifestContext<'_, '_>, + dep: &Dependency, + patch_source_url: Option<(&Url, bool)>, +) -> CargoResult> { + let name_in_toml = dep.name_in_toml().as_str(); + let message = "see https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#patch-files about the status of this feature."; + match (patch_source_url, orig.patches.as_ref()) { + (_, None) => { + // not a SourceKind::Patched dep. + Ok(None) + } + (None, Some(_)) => { + let kind = dep.kind().kind_table(); + manifest_ctx.warnings.push(format!( + "unused manifest key: {kind}.{name_in_toml}.patches; {message}" + )); + Ok(None) + } + (Some((url, false)), Some(_)) => { + manifest_ctx.warnings.push(format!( + "ignoring `patches` on patch for `{name_in_toml}` in `{url}`; {message}" + )); + Ok(None) + } + (Some((url, true)), Some(patches)) => { + let source_id = dep.source_id(); + if !source_id.is_registry() { + bail!( + "patch for `{name_in_toml}` in `{url}` requires a registry source \ + when patching with patch files" + ); + } + if &CanonicalUrl::new(url)? != source_id.canonical_url() { + bail!( + "patch for `{name_in_toml}` in `{url}` must refer to the same source \ + when patching with patch files" + ) + } + let patches: Vec<_> = patches + .iter() + .map(|path| { + let path = path.resolve(manifest_ctx.gctx); + let path = manifest_ctx.root.join(path); + // keep paths inside workspace relative to workspace, otherwise absolute. + path.strip_prefix(manifest_ctx.gctx.cwd()) + .map(Into::into) + .unwrap_or_else(|_| paths::normalize_path(&path)) + }) + .collect(); + if patches.is_empty() { + bail!( + "patch for `{name_in_toml}` in `{url}` requires at least one patch file \ + when patching with patch files" + ); + } + let patch_info = PatchInfo::Deferred { patches }; + SourceId::for_patches(source_id, patch_info).map(Some) + } + } +} + pub trait ResolveToPath { fn resolve(&self, gctx: &GlobalContext) -> PathBuf; } diff --git a/tests/testsuite/cargo/z_help/stdout.term.svg b/tests/testsuite/cargo/z_help/stdout.term.svg index a4c8e579b4d..038ad1bd265 100644 --- a/tests/testsuite/cargo/z_help/stdout.term.svg +++ b/tests/testsuite/cargo/z_help/stdout.term.svg @@ -1,4 +1,4 @@ - +