Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[experiment] patch with patch files #13779

Closed
wants to merge 12 commits into from
Closed
1 change: 1 addition & 0 deletions crates/cargo-test-support/src/compare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
2 changes: 2 additions & 0 deletions crates/cargo-util-schemas/src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
50 changes: 46 additions & 4 deletions crates/cargo-util-schemas/src/core/package_id_spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -599,6 +617,18 @@ 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::new("bar".into(), "1.2.0".into(), 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]
Expand Down Expand Up @@ -651,5 +681,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(_)
);
}
}
107 changes: 107 additions & 0 deletions crates/cargo-util-schemas/src/core/source_kind.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::cmp::Ordering;
use std::path::PathBuf;

/// The possible kinds of code source.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
Expand All @@ -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 {
Expand All @@ -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,
}
}
}
Expand Down Expand Up @@ -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),
}
}
Expand Down Expand Up @@ -199,3 +208,101 @@ 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 struct PatchInfo {
/// Name of the package to be patched.
name: String,
/// Verision 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<PathBuf>,
}

impl PatchInfo {
pub fn new(name: String, version: String, patches: Vec<PathBuf>) -> PatchInfo {
PatchInfo {
name,
version,
patches,
}
}

/// 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<Item = (impl AsRef<str>, impl AsRef<str>)>,
) -> Result<PatchInfo, PatchInfoError> {
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::new(name, version, patches))
}

/// As a URL query string.
pub fn as_query(&self) -> PatchInfoQuery<'_> {
PatchInfoQuery(self)
}

pub fn name(&self) -> &str {
self.name.as_str()
}

pub fn version(&self) -> &str {
self.version.as_str()
}

pub fn patches(&self) -> &[PathBuf] {
self.patches.as_slice()
}
}

/// 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 {
write!(f, "name=")?;
for value in url::form_urlencoded::byte_serialize(self.0.name.as_bytes()) {
write!(f, "{value}")?;
}
write!(f, "&version=")?;
for value in url::form_urlencoded::byte_serialize(self.0.version.as_bytes()) {
write!(f, "{value}")?;
}
for path in &self.0.patches {
write!(f, "&patch=")?;
let path = path.to_str().expect("utf8 patch").replace("\\", "/");
for value in url::form_urlencoded::byte_serialize(path.as_bytes()) {
write!(f, "{value}")?;
}
}

Ok(())
}
}

/// Error parsing patch info from URL query string.
#[derive(Debug, thiserror::Error)]
#[error("missing query string `{0}`")]
pub struct PatchInfoError(&'static str);
8 changes: 8 additions & 0 deletions crates/cargo-util-schemas/src/manifest/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,13 @@ pub struct TomlDetailedDependency<P: Clone = String> {
#[serde(rename = "default_features")]
pub default_features2: Option<bool>,
pub package: Option<PackageName>,
/// `patches = [<path>, ...]` 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<Vec<P>>,
pub public: Option<bool>,

/// One or more of `bin`, `cdylib`, `staticlib`, `bin:<name>`.
Expand Down Expand Up @@ -815,6 +822,7 @@ impl<P: Clone> Default for TomlDetailedDependency<P> {
default_features: Default::default(),
default_features2: Default::default(),
package: Default::default(),
patches: Default::default(),
public: Default::default(),
artifact: Default::default(),
lib: Default::default(),
Expand Down
5 changes: 5 additions & 0 deletions src/cargo/core/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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)?,
Expand Down
Loading
Loading