|
| 1 | +//! A source that takes other source and patches it with local patch files. |
| 2 | +//! See [`PatchedSource`] for details. |
| 3 | +
|
| 4 | +use std::path::Path; |
| 5 | +use std::path::PathBuf; |
| 6 | +use std::task::Poll; |
| 7 | + |
| 8 | +use anyhow::Context as _; |
| 9 | +use cargo_util::paths; |
| 10 | +use cargo_util::ProcessBuilder; |
| 11 | +use cargo_util::Sha256; |
| 12 | +use cargo_util_schemas::core::PatchInfo; |
| 13 | +use cargo_util_schemas::core::SourceKind; |
| 14 | +use lazycell::LazyCell; |
| 15 | + |
| 16 | +use crate::core::Dependency; |
| 17 | +use crate::core::Package; |
| 18 | +use crate::core::PackageId; |
| 19 | +use crate::core::SourceId; |
| 20 | +use crate::core::Verbosity; |
| 21 | +use crate::sources::source::MaybePackage; |
| 22 | +use crate::sources::source::QueryKind; |
| 23 | +use crate::sources::source::Source; |
| 24 | +use crate::sources::IndexSummary; |
| 25 | +use crate::sources::PathSource; |
| 26 | +use crate::sources::SourceConfigMap; |
| 27 | +use crate::util::cache_lock::CacheLockMode; |
| 28 | +use crate::util::hex; |
| 29 | +use crate::util::OptVersionReq; |
| 30 | +use crate::CargoResult; |
| 31 | +use crate::GlobalContext; |
| 32 | + |
| 33 | +/// A file indicates that if present, the patched source is ready to use. |
| 34 | +const READY_LOCK: &str = ".cargo-ok"; |
| 35 | + |
| 36 | +/// `PatchedSource` is a source that, when fetching, it patches a paticular |
| 37 | +/// package with given local patch files. |
| 38 | +/// |
| 39 | +/// This could only be created from [the `[patch]` section][patch] with any |
| 40 | +/// entry carrying `{ .., patches = ["..."] }` field. Other kinds of dependency |
| 41 | +/// sections (normal, dev, build) shouldn't allow to create any `PatchedSource`. |
| 42 | +/// |
| 43 | +/// [patch]: https://doc.rust-lang.org/nightly/cargo/reference/overriding-dependencies.html#the-patch-section |
| 44 | +/// |
| 45 | +/// ## Filesystem layout |
| 46 | +/// |
| 47 | +/// When Cargo fetches a package from a `PatchedSource`, it'll copy everything |
| 48 | +/// from the original source to a dedicated patched source directory. That |
| 49 | +/// directory is located under `$CARGO_HOME`. The patched source of each package |
| 50 | +/// would be put under: |
| 51 | +/// |
| 52 | +/// ```text |
| 53 | +/// $CARGO_HOME/patched-src/<hash-of-original-source>/<pkg>-<version>/<cksum-of-patches>/`. |
| 54 | +/// ``` |
| 55 | +/// |
| 56 | +/// The file tree of the patched source directory roughly looks like: |
| 57 | +/// |
| 58 | +/// ```text |
| 59 | +/// $CARGO_HOME/patched-src/github.com-6d038ece37e82ae2 |
| 60 | +/// ├── gimli-0.29.0/ |
| 61 | +/// │ ├── a0d193bd15a5ed96/ # checksum of all patch files from a patch to [email protected] |
| 62 | +/// │ ├── c58e1db3de7c154d/ |
| 63 | +/// └── serde-1.0.197/ |
| 64 | +/// └── deadbeef12345678/ |
| 65 | +/// ``` |
| 66 | +/// |
| 67 | +/// ## `SourceId` for tracking the original package |
| 68 | +/// |
| 69 | +/// Due to the nature that a patched source is actually locked to a specific |
| 70 | +/// version of one package, the SourceId URL of a `PatchedSource` needs to |
| 71 | +/// carry such information. It looks like: |
| 72 | +/// |
| 73 | +/// ```text |
| 74 | +/// patched+registry+https://github.com/rust-lang/crates.io-index?name=foo&version=1.0.0&patch=0001-bugfix.patch |
| 75 | +/// ``` |
| 76 | +/// |
| 77 | +/// where the `patched+` protocol is essential for Cargo to distinguish between |
| 78 | +/// a patched source and the source it patches. The query string contains the |
| 79 | +/// name and version of the package being patched. We want patches to be as |
| 80 | +/// reproducible as it could, so lock to one specific version here. |
| 81 | +/// See [`PatchInfo::from_query`] to learn what are being tracked. |
| 82 | +/// |
| 83 | +/// To achieve it, the version specified in any of the entry in `[patch]` must |
| 84 | +/// be an exact version via the `=` SemVer comparsion operator. For example, |
| 85 | +/// this will fetch source of [email protected] from crates.io, and apply patches to it. |
| 86 | +/// |
| 87 | +/// ```toml |
| 88 | +/// [patch.crates-io] |
| 89 | +/// serde = { version = "=1.2.3", patches = ["patches/0001-serde-bug.patch"] } |
| 90 | +/// ``` |
| 91 | +/// |
| 92 | +/// ## Patch tools |
| 93 | +/// |
| 94 | +/// When patching a package, Cargo will change the working directory to |
| 95 | +/// the root directory of the copied source code, and then execute the tool |
| 96 | +/// specified via the `patchtool.path` config value in the Cargo configuration. |
| 97 | +/// Paths of patch files will be provided as absolute paths to the tool. |
| 98 | +pub struct PatchedSource<'gctx> { |
| 99 | + source_id: SourceId, |
| 100 | + /// The source of the package we're going to patch. |
| 101 | + original_source: Box<dyn Source + 'gctx>, |
| 102 | + /// Checksum from all patch files. |
| 103 | + patches_checksum: LazyCell<String>, |
| 104 | + /// For respecting `[source]` replacement configuration. |
| 105 | + map: SourceConfigMap<'gctx>, |
| 106 | + path_source: Option<PathSource<'gctx>>, |
| 107 | + quiet: bool, |
| 108 | + gctx: &'gctx GlobalContext, |
| 109 | +} |
| 110 | + |
| 111 | +impl<'gctx> PatchedSource<'gctx> { |
| 112 | + pub fn new( |
| 113 | + source_id: SourceId, |
| 114 | + gctx: &'gctx GlobalContext, |
| 115 | + ) -> CargoResult<PatchedSource<'gctx>> { |
| 116 | + let original_id = { |
| 117 | + let mut url = source_id.url().clone(); |
| 118 | + url.set_query(None); |
| 119 | + url.set_fragment(None); |
| 120 | + let url = url.as_str(); |
| 121 | + let Some(url) = url.strip_prefix("patched+") else { |
| 122 | + anyhow::bail!("patched source url requires a `patched` scheme, got `{url}`"); |
| 123 | + }; |
| 124 | + SourceId::from_url(&url)? |
| 125 | + }; |
| 126 | + let map = SourceConfigMap::new(gctx)?; |
| 127 | + let source = PatchedSource { |
| 128 | + source_id, |
| 129 | + original_source: map.load(original_id, &Default::default())?, |
| 130 | + patches_checksum: LazyCell::new(), |
| 131 | + map, |
| 132 | + path_source: None, |
| 133 | + quiet: false, |
| 134 | + gctx, |
| 135 | + }; |
| 136 | + Ok(source) |
| 137 | + } |
| 138 | + |
| 139 | + /// Downloads the package source if needed. |
| 140 | + fn download_pkg(&mut self) -> CargoResult<Package> { |
| 141 | + let patch_info = self.patch_info(); |
| 142 | + let exact_req = &format!("={}", patch_info.version()); |
| 143 | + let original_id = self.original_source.source_id(); |
| 144 | + let dep = Dependency::parse(patch_info.name(), Some(exact_req), original_id)?; |
| 145 | + let pkg_id = loop { |
| 146 | + match self.original_source.query_vec(&dep, QueryKind::Exact) { |
| 147 | + Poll::Ready(deps) => break deps?.remove(0).as_summary().package_id(), |
| 148 | + Poll::Pending => self.original_source.block_until_ready()?, |
| 149 | + } |
| 150 | + }; |
| 151 | + |
| 152 | + let source = self.map.load(original_id, &Default::default())?; |
| 153 | + Box::new(source).download_now(pkg_id, self.gctx) |
| 154 | + } |
| 155 | + |
| 156 | + fn copy_pkg_src(&self, pkg: &Package, dst: &Path) -> CargoResult<()> { |
| 157 | + let src = pkg.root(); |
| 158 | + for entry in walkdir::WalkDir::new(src) { |
| 159 | + let entry = entry?; |
| 160 | + let path = entry.path().strip_prefix(src).unwrap(); |
| 161 | + let src = entry.path(); |
| 162 | + let dst = dst.join(path); |
| 163 | + if entry.file_type().is_dir() { |
| 164 | + paths::create_dir_all(dst)?; |
| 165 | + } else { |
| 166 | + // TODO: handle symlink? |
| 167 | + paths::copy(src, dst)?; |
| 168 | + } |
| 169 | + } |
| 170 | + Ok(()) |
| 171 | + } |
| 172 | + |
| 173 | + fn apply_patches(&self, pkg: &Package, dst: &Path) -> CargoResult<()> { |
| 174 | + let patches = self.patch_info().patches(); |
| 175 | + let n = patches.len(); |
| 176 | + assert!(n > 0, "must have at least one patch, got {n}"); |
| 177 | + |
| 178 | + self.gctx.shell().status("Patching", pkg)?; |
| 179 | + |
| 180 | + let patchtool_config = self.gctx.patchtool_config()?; |
| 181 | + let Some(tool) = patchtool_config.path.as_ref() else { |
| 182 | + anyhow::bail!("missing `[patchtool]` for patching dependencies"); |
| 183 | + }; |
| 184 | + |
| 185 | + let program = tool.path.resolve_program(self.gctx); |
| 186 | + let mut cmd = ProcessBuilder::new(program); |
| 187 | + cmd.cwd(dst).args(&tool.args); |
| 188 | + |
| 189 | + for patch_path in patches { |
| 190 | + let patch_path = self.gctx.cwd().join(patch_path); |
| 191 | + let mut cmd = cmd.clone(); |
| 192 | + cmd.arg(patch_path); |
| 193 | + if matches!(self.gctx.shell().verbosity(), Verbosity::Verbose) { |
| 194 | + self.gctx.shell().status("Running", &cmd)?; |
| 195 | + cmd.exec()?; |
| 196 | + } else { |
| 197 | + cmd.exec_with_output()?; |
| 198 | + } |
| 199 | + } |
| 200 | + |
| 201 | + Ok(()) |
| 202 | + } |
| 203 | + |
| 204 | + /// Gets the destination directory we put the patched source at. |
| 205 | + fn dest_src_dir(&self, pkg: &Package) -> CargoResult<PathBuf> { |
| 206 | + let patched_src_root = self.gctx.patched_source_path(); |
| 207 | + let patched_src_root = self |
| 208 | + .gctx |
| 209 | + .assert_package_cache_locked(CacheLockMode::DownloadExclusive, &patched_src_root); |
| 210 | + let pkg_id = pkg.package_id(); |
| 211 | + let source_id = pkg_id.source_id(); |
| 212 | + let ident = source_id.url().host_str().unwrap_or_default(); |
| 213 | + let hash = hex::short_hash(&source_id); |
| 214 | + let name = pkg_id.name(); |
| 215 | + let version = pkg_id.version(); |
| 216 | + let mut dst = patched_src_root.join(format!("{ident}-{hash}")); |
| 217 | + dst.push(format!("{name}-{version}")); |
| 218 | + dst.push(self.patches_checksum()?); |
| 219 | + Ok(dst) |
| 220 | + } |
| 221 | + |
| 222 | + fn patches_checksum(&self) -> CargoResult<&String> { |
| 223 | + self.patches_checksum.try_borrow_with(|| { |
| 224 | + let mut cksum = Sha256::new(); |
| 225 | + for patch in self.patch_info().patches() { |
| 226 | + cksum.update_path(patch)?; |
| 227 | + } |
| 228 | + let mut cksum = cksum.finish_hex(); |
| 229 | + // TODO: is it safe to truncate sha256? |
| 230 | + cksum.truncate(16); |
| 231 | + Ok(cksum) |
| 232 | + }) |
| 233 | + } |
| 234 | + |
| 235 | + fn patch_info(&self) -> &PatchInfo { |
| 236 | + let SourceKind::Patched(info) = self.source_id.kind() else { |
| 237 | + panic!("patched source must be SourceKind::Patched"); |
| 238 | + }; |
| 239 | + info |
| 240 | + } |
| 241 | +} |
| 242 | + |
| 243 | +impl<'gctx> Source for PatchedSource<'gctx> { |
| 244 | + fn source_id(&self) -> SourceId { |
| 245 | + self.source_id |
| 246 | + } |
| 247 | + |
| 248 | + fn supports_checksums(&self) -> bool { |
| 249 | + false |
| 250 | + } |
| 251 | + |
| 252 | + fn requires_precise(&self) -> bool { |
| 253 | + false |
| 254 | + } |
| 255 | + |
| 256 | + fn query( |
| 257 | + &mut self, |
| 258 | + dep: &Dependency, |
| 259 | + kind: QueryKind, |
| 260 | + f: &mut dyn FnMut(IndexSummary), |
| 261 | + ) -> Poll<CargoResult<()>> { |
| 262 | + // Version requirement here is still the `=` exact one for fetching |
| 263 | + // the source to patch, so switch it to a wildchard requirement. |
| 264 | + // It is safe because this source contains one and the only package. |
| 265 | + let mut dep = dep.clone(); |
| 266 | + dep.set_version_req(OptVersionReq::Any); |
| 267 | + if let Some(src) = self.path_source.as_mut() { |
| 268 | + src.query(&dep, kind, f) |
| 269 | + } else { |
| 270 | + Poll::Pending |
| 271 | + } |
| 272 | + } |
| 273 | + |
| 274 | + fn invalidate_cache(&mut self) { |
| 275 | + // No cache for a patched source |
| 276 | + } |
| 277 | + |
| 278 | + fn set_quiet(&mut self, quiet: bool) { |
| 279 | + self.quiet = quiet; |
| 280 | + } |
| 281 | + |
| 282 | + fn download(&mut self, id: PackageId) -> CargoResult<MaybePackage> { |
| 283 | + self.path_source |
| 284 | + .as_mut() |
| 285 | + .expect("path source must exist") |
| 286 | + .download(id) |
| 287 | + } |
| 288 | + |
| 289 | + fn finish_download(&mut self, _pkg_id: PackageId, _contents: Vec<u8>) -> CargoResult<Package> { |
| 290 | + panic!("no download should have started") |
| 291 | + } |
| 292 | + |
| 293 | + fn fingerprint(&self, pkg: &Package) -> CargoResult<String> { |
| 294 | + let fingerprint = self.original_source.fingerprint(pkg)?; |
| 295 | + let cksum = self.patches_checksum()?; |
| 296 | + Ok(format!("{fingerprint}/{cksum}")) |
| 297 | + } |
| 298 | + |
| 299 | + fn describe(&self) -> String { |
| 300 | + use std::fmt::Write as _; |
| 301 | + let mut desc = self.original_source.describe(); |
| 302 | + let n_patches = self.patch_info().patches().len(); |
| 303 | + write!(desc, "with {n_patches} patch file").unwrap(); |
| 304 | + if n_patches > 1 { |
| 305 | + write!(desc, "s").unwrap(); |
| 306 | + } |
| 307 | + desc |
| 308 | + } |
| 309 | + |
| 310 | + fn add_to_yanked_whitelist(&mut self, _pkgs: &[PackageId]) { |
| 311 | + // There is no yanked package for a patched source |
| 312 | + } |
| 313 | + |
| 314 | + fn is_yanked(&mut self, _pkg: PackageId) -> Poll<CargoResult<bool>> { |
| 315 | + // There is no yanked package for a patched source |
| 316 | + Poll::Ready(Ok(false)) |
| 317 | + } |
| 318 | + |
| 319 | + fn block_until_ready(&mut self) -> CargoResult<()> { |
| 320 | + if self.path_source.is_some() { |
| 321 | + return Ok(()); |
| 322 | + } |
| 323 | + |
| 324 | + let pkg = self.download_pkg().context("failed to download source")?; |
| 325 | + let dst = self.dest_src_dir(&pkg)?; |
| 326 | + |
| 327 | + let ready_lock = dst.join(READY_LOCK); |
| 328 | + let cksum = self.patches_checksum()?; |
| 329 | + match paths::read(&ready_lock) { |
| 330 | + Ok(prev_cksum) if &prev_cksum == cksum => { |
| 331 | + // We've applied patches. Assume they never change. |
| 332 | + } |
| 333 | + _ => { |
| 334 | + // Either we were interrupted, or never get started. |
| 335 | + // We just start over here. |
| 336 | + if let Err(e) = paths::remove_dir_all(&dst) { |
| 337 | + tracing::trace!("failed to remove `{}`: {e}", dst.display()); |
| 338 | + } |
| 339 | + self.copy_pkg_src(&pkg, &dst) |
| 340 | + .context("failed to copy source")?; |
| 341 | + self.apply_patches(&pkg, &dst) |
| 342 | + .context("failed to apply patches")?; |
| 343 | + paths::write(&ready_lock, cksum)?; |
| 344 | + } |
| 345 | + } |
| 346 | + |
| 347 | + self.path_source = Some(PathSource::new(&dst, self.source_id, self.gctx)); |
| 348 | + |
| 349 | + Ok(()) |
| 350 | + } |
| 351 | +} |
0 commit comments