1use anyhow::{anyhow, Ok, Result};
54use cargo::{
55 core::{
56 package::{Package, PackageSet},
57 registry::PackageRegistry,
58 resolver::{features::CliFeatures, HasDevUnits},
59 Resolve, Workspace,
60 },
61 ops::{get_resolved_packages, load_pkg_lockfile, resolve_with_previous},
62 sources::SourceConfigMap,
63 util::{cache_lock::CacheLockMode, important_paths::find_root_manifest_for_wd, GlobalContext},
64};
65use clap::Parser;
66use fs_extra::dir::{copy, CopyOptions};
67use log::*;
68use std::{
69 collections::HashSet,
70 ffi::OsStr,
71 fs,
72 path::{Path, PathBuf},
73};
74
75const PATCH_EXT: &str = "patch";
76
77#[derive(Parser, Debug)]
78#[command(author, version, about, long_about = None)]
79struct Cli {
80 crates: Vec<String>,
81 #[arg(short, long)]
82 force: bool,
83}
84
85trait PackageExt {
86 fn slug(&self) -> Result<&str>;
87 fn patch_target_path(&self, workspace: &Workspace<'_>) -> Result<PathBuf>;
88}
89
90impl PackageExt for Package {
91 fn slug(&self) -> Result<&str> {
92 if let Some(name) = self.root().file_name().and_then(|s| s.to_str()) {
93 Ok(name)
94 } else {
95 Err(anyhow!("Dependency Folder does not have a name"))
96 }
97 }
98
99 fn patch_target_path(&self, workspace: &Workspace<'_>) -> Result<PathBuf> {
100 let slug = self.slug()?;
101 let patch_target_path = workspace.patch_target_folder().join(slug);
102 Ok(patch_target_path)
103 }
104}
105
106trait WorkspaceExt {
107 fn patches_folder(&self) -> PathBuf;
108 fn patch_target_folder(&self) -> PathBuf;
109 fn patch_target_tmp_folder(&self) -> PathBuf;
110 fn clean_patch_folder(&self) -> Result<()>;
111}
112
113impl WorkspaceExt for Workspace<'_> {
114 fn patches_folder(&self) -> PathBuf {
115 self.root().join("patches/")
116 }
117 fn patch_target_folder(&self) -> PathBuf {
118 self.root().join("target/patch/")
119 }
120 fn patch_target_tmp_folder(&self) -> PathBuf {
121 self.root().join("target/patch-tmp/")
122 }
123
124 fn clean_patch_folder(&self) -> Result<()> {
125 let path = self.patch_target_folder();
126 if path.exists() {
127 fs::remove_dir_all(self.patch_target_folder())?;
128 }
129 Ok(())
130 }
131}
132
133fn resolve_ws<'a>(ws: &Workspace<'a>) -> Result<(PackageSet<'a>, Resolve)> {
134 let mut registry =
135 PackageRegistry::new_with_source_config(ws.gctx(), SourceConfigMap::new(ws.gctx())?)?;
136 registry.lock_patches();
137 let resolve = {
138 let prev = load_pkg_lockfile(ws)?;
139 let resolve: Resolve = resolve_with_previous(
140 &mut registry,
141 ws,
142 &CliFeatures::new_all(true),
143 HasDevUnits::No,
144 prev.as_ref(),
145 None,
146 &[],
147 false,
148 )?;
149 resolve
150 };
151 let packages = get_resolved_packages(&resolve, registry)?;
152 Ok((packages, resolve))
153}
154
155fn copy_package(pkg: &Package, patch_target_folder: &Path, overwrite: bool) -> Result<PathBuf> {
156 fs::create_dir_all(patch_target_folder)?;
157 let options = CopyOptions::new();
158 let patch_target_path = patch_target_folder.join(pkg.slug()?);
159 if patch_target_path.exists() {
160 if overwrite {
161 info!("crate: {}, copy to {:?}", pkg.name(), &patch_target_folder);
162 fs::remove_dir_all(&patch_target_path)?;
163 } else {
164 info!(
165 "crate: {}, skip, {:?} already exists.",
166 pkg.name(),
167 &patch_target_path
168 );
169 return Ok(patch_target_path);
170 }
171 }
172 let _ = copy(pkg.root(), patch_target_folder, &options)?;
173 Ok(patch_target_path)
174}
175
176fn find_cargo_toml(path: &Path) -> Result<PathBuf> {
177 let path = fs::canonicalize(path)?;
178 find_root_manifest_for_wd(&path)
179}
180
181pub fn run() -> anyhow::Result<()> {
182 let args = {
183 let mut args = Cli::parse();
184 if let Some(idx) = args.crates.iter().position(|c| c == "patch-crate") {
185 args.crates.remove(idx);
186 }
187 args
188 };
189
190 let gctx = GlobalContext::default()?;
191 let _lock = gctx.acquire_package_cache_lock(CacheLockMode::Shared)?;
192
193 let cargo_toml_path = find_cargo_toml(&PathBuf::from("."))?;
194
195 let workspace = Workspace::new(&cargo_toml_path, &gctx)?;
196
197 let patches_folder = workspace.patches_folder();
198
199 let patch_target_folder = workspace.patch_target_folder();
200 let patch_target_tmp_folder = workspace.patch_target_tmp_folder();
201
202 let (pkg_set, resolve) = resolve_ws(&workspace)?;
203
204 if !args.crates.is_empty() {
205 info!("starting patch creation.");
206 if !patches_folder.exists() {
207 fs::create_dir_all(&patches_folder)?;
208 }
209 for n in args.crates.iter() {
210 info!("crate: {}, starting patch creation.", n);
212 let pkg_id = resolve.query(n)?;
213 let pkg = pkg_set.get_one(pkg_id)?;
214 let patched_crate_path = pkg.patch_target_path(&workspace)?;
215
216 let original_crate_path = copy_package(pkg, &patch_target_tmp_folder, true)?;
218 git::init(&original_crate_path)?;
219
220 let original_crate_git_path = original_crate_path.join(".git");
221 let patched_crate_git_path = patched_crate_path.join(".git");
222
223 git::destroy(&patched_crate_path)?;
226 copy(
227 &original_crate_git_path,
228 &patched_crate_git_path,
229 &CopyOptions::new().overwrite(true).copy_inside(true),
230 )?;
231
232 let patch_file = patches_folder.join(format!(
233 "{}+{}.{}",
234 pkg_id.name(),
235 pkg_id.version(),
236 PATCH_EXT
237 ));
238 git::create_patch(&patched_crate_path, &patch_file)?;
239 fs::remove_dir_all(&patch_target_tmp_folder)?;
240
241 git::destroy(&patched_crate_path)?;
242 info!("crate: {}, create patch successfully, {:?}", n, &patch_file);
243 }
244 } else {
245 info!("applying patch");
247
248 let custom_metadata = workspace.custom_metadata().into_iter().chain(
249 workspace
250 .members()
251 .flat_map(|member| member.manifest().custom_metadata()),
252 );
253
254 let mut crates_to_patch = custom_metadata
255 .flat_map(|m| {
256 m.as_table()
257 .and_then(|table| table.get("patch"))
258 .into_iter()
259 .flat_map(|patch| patch.as_table())
260 .flat_map(|patch| patch.get("crates"))
261 .filter_map(|crates| crates.as_array())
262 })
263 .flatten()
264 .flat_map(|s| s.as_str())
265 .map(|n| resolve.query(n).and_then(|id| pkg_set.get_one(id)))
266 .collect::<Result<HashSet<_>>>()?;
267
268 if args.force {
269 info!("Cleaning up patch folder.");
270 workspace.clean_patch_folder()?;
271 }
272
273 if patches_folder.exists() {
274 for entry in fs::read_dir(patches_folder)? {
275 let entry = entry?;
276 if entry.metadata()?.is_file()
277 && entry.path().extension() == Some(OsStr::new(PATCH_EXT))
278 {
279 let patch_file = entry.path();
280 let filename = patch_file
281 .file_stem()
282 .and_then(|s| s.to_str())
283 .ok_or(anyhow!("Patch file does not have a name"))?;
284
285 if let Some((pkg_name, version)) = filename.split_once('+') {
286 let pkg_id = resolve.query(format!("{}@{}", pkg_name, version).as_str())?;
287 let pkg = pkg_set.get_one(pkg_id)?;
288 if !crates_to_patch.contains(&pkg) {
289 warn!(
290 "crate: {}, {} is not in the [package.metadata.patch] or [workspace.metadata.patch] section of Cargo.toml. Did you forget to add it?",
291 pkg_name, pkg_name
292 );
293 continue;
294 }
295
296 let patch_target_path = pkg.patch_target_path(&workspace)?;
297 if !patch_target_path.exists() {
298 copy_package(pkg, &patch_target_folder, args.force)?;
299 info!("crate: {}, applying patch started.", pkg_name);
300 git::init(&patch_target_path)?;
301 git::apply(&patch_target_path, &patch_file)?;
302 git::destroy(&patch_target_path)?;
303 info!(
304 "crate: {}, successfully applied patch {:?}.",
305 pkg_name, patch_file
306 );
307 } else {
308 info!("crate: {}, skip applying patch, {:?} already exists. Did you forget to add `--force`?", pkg_name, patch_target_path);
309 }
310 crates_to_patch.remove(pkg);
311 }
312 }
313 }
314 }
315 for pkg in crates_to_patch {
316 copy_package(pkg, &patch_target_folder, args.force)?;
317 }
318 }
319
320 info!("Done");
321 Ok(())
322}
323
324mod log {
325 pub use paris::*;
326}
327
328mod git {
329 use std::{ffi::OsStr, fs, path::Path, process::Command};
330
331 pub fn init(repo_dir: &Path) -> anyhow::Result<()> {
332 Command::new("git")
333 .current_dir(repo_dir)
334 .args(["init"])
335 .output()?;
336 Command::new("git")
337 .current_dir(repo_dir)
338 .args(["add", "."])
339 .output()?;
340 Command::new("git")
341 .current_dir(repo_dir)
342 .args(["commit", "-m", "zero"])
343 .output()?;
344 Ok(())
345 }
346
347 pub fn apply(repo_dir: &Path, patch_file: &Path) -> anyhow::Result<()> {
348 #[cfg(target_os = "windows")]
349 let patch_file = patch_file
350 .to_string_lossy()
351 .to_string()
352 .trim_start_matches(r#"\\?\"#)
353 .to_string();
354 #[cfg(not(target_os = "windows"))]
355 let patch_file = patch_file.to_string_lossy().to_string();
356
357 let out = Command::new("git")
358 .current_dir(repo_dir)
359 .args([
360 "apply",
361 "--ignore-space-change",
362 "--ignore-whitespace",
363 "--whitespace=nowarn",
364 &patch_file,
365 ])
366 .output()?;
367
368 if !out.status.success() {
369 anyhow::bail!(String::from_utf8(out.stderr)?)
370 }
371 Ok(())
372 }
373 pub fn destroy(repo_dir: &Path) -> anyhow::Result<()> {
374 let git_dir = repo_dir.join(".git");
375 if git_dir.exists() {
376 fs::remove_dir_all(git_dir)?;
377 }
378 Ok(())
379 }
380 pub fn create_patch(repo_dir: &Path, patch_file: &Path) -> anyhow::Result<()> {
381 Command::new("git")
382 .current_dir(repo_dir)
383 .args(["add", "."])
384 .output()?;
385
386 let out = Command::new("git")
387 .current_dir(repo_dir)
388 .args([OsStr::new("diff"), OsStr::new("--staged")])
389 .output()?;
390
391 if out.status.success() {
392 fs::write(patch_file, out.stdout)?;
393 }
394 Ok(())
395 }
396}