xref: /aosp_15_r20/external/toolchain-utils/llvm_tools/patch_sync/src/version_control.rs (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
1 // Copyright 2022 The ChromiumOS Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 use anyhow::{anyhow, bail, ensure, Context, Result};
6 use regex::Regex;
7 use std::ffi::OsStr;
8 use std::fs;
9 use std::path::{Path, PathBuf};
10 use std::process::{Command, Output};
11 
12 const CHROMIUMOS_OVERLAY_REL_PATH: &str = "src/third_party/chromiumos-overlay";
13 const ANDROID_LLVM_REL_PATH: &str = "toolchain/llvm_android";
14 
15 // Need to checkout the upstream, rather than the local clone.
16 const CROS_MAIN_BRANCH: &str = "cros/main";
17 const ANDROID_MAIN_BRANCH: &str = "aosp/main";
18 const WORK_BRANCH_NAME: &str = "__patch_sync_tmp";
19 
20 /// Context struct to keep track of both ChromiumOS and Android checkouts.
21 #[derive(Debug)]
22 pub struct RepoSetupContext {
23     pub cros_checkout: PathBuf,
24     pub android_checkout: PathBuf,
25     /// Run `repo sync` before doing any comparisons.
26     pub sync_before: bool,
27     pub wip_mode: bool,
28     pub enable_cq: bool,
29     /// Generally LLVM ebuilds are now 9999 LIVE ebuilds, so only uprev if this is set.
30     pub uprev_ebuilds: bool,
31 }
32 
33 impl RepoSetupContext {
setup(&self) -> Result<()>34     pub fn setup(&self) -> Result<()> {
35         if self.sync_before {
36             {
37                 let crpp = self.cros_patches_path();
38                 let cros_git = crpp.parent().unwrap();
39                 git_cd_cmd(cros_git, ["checkout", CROS_MAIN_BRANCH])?;
40             }
41             {
42                 let anpp = self.android_patches_path();
43                 let android_git = anpp.parent().unwrap();
44                 git_cd_cmd(android_git, ["checkout", ANDROID_MAIN_BRANCH])?;
45             }
46             repo_cd_cmd(&self.cros_checkout, ["sync", CHROMIUMOS_OVERLAY_REL_PATH])?;
47             repo_cd_cmd(&self.android_checkout, ["sync", ANDROID_LLVM_REL_PATH])?;
48         }
49         Ok(())
50     }
51 
cros_repo_upload<S: AsRef<str>>(&self, reviewers: &[S]) -> Result<()>52     pub fn cros_repo_upload<S: AsRef<str>>(&self, reviewers: &[S]) -> Result<()> {
53         let llvm_dir = self
54             .cros_checkout
55             .join(CHROMIUMOS_OVERLAY_REL_PATH)
56             .join("sys-devel/llvm");
57         ensure!(
58             llvm_dir.is_dir(),
59             "CrOS LLVM dir {} is not a directory",
60             llvm_dir.display()
61         );
62 
63         if self.uprev_ebuilds {
64             Self::rev_bump_llvm(&llvm_dir)?;
65         }
66 
67         let mut extra_args = Vec::new();
68         for reviewer in reviewers {
69             extra_args.push("--re");
70             extra_args.push(reviewer.as_ref());
71         }
72         if self.wip_mode {
73             extra_args.push("--wip");
74             extra_args.push("--no-emails");
75         }
76         if self.enable_cq {
77             extra_args.push("--label=Commit-Queue+1");
78         }
79         Self::repo_upload(
80             &self.cros_checkout,
81             CHROMIUMOS_OVERLAY_REL_PATH,
82             &Self::build_commit_msg(
83                 "llvm: Synchronize patches from android",
84                 "android",
85                 "chromiumos",
86                 "BUG=None\nTEST=CQ",
87             ),
88             extra_args,
89         )
90     }
91 
android_repo_upload<S: AsRef<str>>(&self, reviewers: &[S]) -> Result<()>92     pub fn android_repo_upload<S: AsRef<str>>(&self, reviewers: &[S]) -> Result<()> {
93         let mut extra_args = Vec::new();
94         for reviewer in reviewers {
95             extra_args.push("--re");
96             extra_args.push(reviewer.as_ref());
97         }
98         if self.wip_mode {
99             extra_args.push("--wip");
100             extra_args.push("--no-emails");
101         }
102         if self.enable_cq {
103             extra_args.push("--label=Presubmit-Ready+1");
104         }
105         Self::repo_upload(
106             &self.android_checkout,
107             ANDROID_LLVM_REL_PATH,
108             &Self::build_commit_msg(
109                 "Synchronize patches from chromiumos",
110                 "chromiumos",
111                 "android",
112                 "Test: N/A",
113             ),
114             extra_args,
115         )
116     }
117 
cros_cleanup(&self) -> Result<()>118     fn cros_cleanup(&self) -> Result<()> {
119         let git_path = self.cros_checkout.join(CHROMIUMOS_OVERLAY_REL_PATH);
120         Self::cleanup_branch(&git_path, CROS_MAIN_BRANCH, WORK_BRANCH_NAME)
121             .with_context(|| format!("cleaning up branch {}", WORK_BRANCH_NAME))?;
122         Ok(())
123     }
124 
android_cleanup(&self) -> Result<()>125     fn android_cleanup(&self) -> Result<()> {
126         let git_path = self.android_checkout.join(ANDROID_LLVM_REL_PATH);
127         Self::cleanup_branch(&git_path, ANDROID_MAIN_BRANCH, WORK_BRANCH_NAME)
128             .with_context(|| format!("cleaning up branch {}", WORK_BRANCH_NAME))?;
129         Ok(())
130     }
131 
132     /// Wrapper around cleanups to ensure both get run, even if errors appear.
cleanup(&self)133     pub fn cleanup(&self) {
134         if let Err(e) = self.cros_cleanup() {
135             eprintln!("Failed to clean up chromiumos, continuing: {}", e);
136         }
137         if let Err(e) = self.android_cleanup() {
138             eprintln!("Failed to clean up android, continuing: {}", e);
139         }
140     }
141 
142     /// Get the Android path to the PATCHES.json file
android_patches_path(&self) -> PathBuf143     pub fn android_patches_path(&self) -> PathBuf {
144         self.android_checkout
145             .join(ANDROID_LLVM_REL_PATH)
146             .join("patches/PATCHES.json")
147     }
148 
149     /// Get the ChromiumOS path to the PATCHES.json file
cros_patches_path(&self) -> PathBuf150     pub fn cros_patches_path(&self) -> PathBuf {
151         self.cros_checkout
152             .join(CHROMIUMOS_OVERLAY_REL_PATH)
153             .join("sys-devel/llvm/files/PATCHES.json")
154     }
155 
156     /// Return the contents of the old PATCHES.json from ChromiumOS
old_cros_patch_contents(&self, hash: &str) -> Result<String>157     pub fn old_cros_patch_contents(&self, hash: &str) -> Result<String> {
158         Self::old_file_contents(
159             hash,
160             &self.cros_checkout.join(CHROMIUMOS_OVERLAY_REL_PATH),
161             Path::new("sys-devel/llvm/files/PATCHES.json"),
162         )
163     }
164 
165     /// Return the contents of the old PATCHES.json from android
old_android_patch_contents(&self, hash: &str) -> Result<String>166     pub fn old_android_patch_contents(&self, hash: &str) -> Result<String> {
167         Self::old_file_contents(
168             hash,
169             &self.android_checkout.join(ANDROID_LLVM_REL_PATH),
170             Path::new("patches/PATCHES.json"),
171         )
172     }
173 
repo_upload<'a, I: IntoIterator<Item = &'a str>>( checkout_path: &Path, subproject_git_wd: &'a str, commit_msg: &str, extra_flags: I, ) -> Result<()>174     fn repo_upload<'a, I: IntoIterator<Item = &'a str>>(
175         checkout_path: &Path,
176         subproject_git_wd: &'a str,
177         commit_msg: &str,
178         extra_flags: I,
179     ) -> Result<()> {
180         let git_path = &checkout_path.join(subproject_git_wd);
181         ensure!(
182             git_path.is_dir(),
183             "git_path {} is not a directory",
184             git_path.display()
185         );
186         repo_cd_cmd(
187             checkout_path,
188             ["start", WORK_BRANCH_NAME, subproject_git_wd],
189         )?;
190         let base_args = ["upload", "--br", WORK_BRANCH_NAME, "-y", "--verify"];
191         let new_args = base_args
192             .iter()
193             .copied()
194             .chain(extra_flags)
195             .chain(["--", subproject_git_wd]);
196         git_cd_cmd(git_path, ["add", "."])
197             .and_then(|_| git_cd_cmd(git_path, ["commit", "-m", commit_msg]))
198             .and_then(|_| repo_cd_cmd(checkout_path, new_args))?;
199         Ok(())
200     }
201 
202     /// Clean up the git repo after we're done with it.
cleanup_branch(git_path: &Path, base_branch: &str, rm_branch: &str) -> Result<()>203     fn cleanup_branch(git_path: &Path, base_branch: &str, rm_branch: &str) -> Result<()> {
204         git_cd_cmd(git_path, ["restore", "."])?;
205         git_cd_cmd(git_path, ["clean", "-fd"])?;
206         git_cd_cmd(git_path, ["checkout", base_branch])?;
207         // It's acceptable to be able to not delete the branch. This may be
208         // because the branch does not exist, which is an expected result.
209         // Since this is a very common case, we won't report any failures related
210         // to this command failure as it'll pollute the stderr logs.
211         let _ = git_cd_cmd(git_path, ["branch", "-D", rm_branch]);
212         Ok(())
213     }
214 
215     /// Increment LLVM's revision number
rev_bump_llvm(llvm_dir: &Path) -> Result<PathBuf>216     fn rev_bump_llvm(llvm_dir: &Path) -> Result<PathBuf> {
217         let ebuild = find_ebuild(llvm_dir)
218             .with_context(|| format!("finding ebuild in {} to rev bump", llvm_dir.display()))?;
219         let ebuild_dir = ebuild.parent().unwrap();
220         let suffix_matcher = Regex::new(r"-r([0-9]+)\.ebuild").unwrap();
221         let ebuild_name = ebuild
222             .file_name()
223             .unwrap()
224             .to_str()
225             .ok_or_else(|| anyhow!("converting ebuild filename to utf-8"))?;
226         let new_path = if let Some(captures) = suffix_matcher.captures(ebuild_name) {
227             let full_suffix = captures.get(0).unwrap().as_str();
228             let cur_version = captures.get(1).unwrap().as_str().parse::<u32>().unwrap();
229             let new_filename =
230                 ebuild_name.replace(full_suffix, &format!("-r{}.ebuild", cur_version + 1_u32));
231             let new_path = ebuild_dir.join(new_filename);
232             fs::rename(&ebuild, &new_path)?;
233             new_path
234         } else {
235             // File did not end in a revision. We should append -r1 to the end.
236             let new_filename = ebuild.file_stem().unwrap().to_string_lossy() + "-r1.ebuild";
237             let new_path = ebuild_dir.join(new_filename.as_ref());
238             fs::rename(&ebuild, &new_path)?;
239             new_path
240         };
241         Ok(new_path)
242     }
243 
244     /// Return the contents of an old file in git
old_file_contents(hash: &str, pwd: &Path, file: &Path) -> Result<String>245     fn old_file_contents(hash: &str, pwd: &Path, file: &Path) -> Result<String> {
246         let git_ref = format!(
247             "{}:{}",
248             hash,
249             file.to_str()
250                 .ok_or_else(|| anyhow!("failed to convert filepath to str"))?
251         );
252         let output = git_cd_cmd(pwd, ["show", &git_ref])?;
253         if !output.status.success() {
254             bail!("could not get old file contents for {}", &git_ref)
255         }
256         String::from_utf8(output.stdout)
257             .with_context(|| format!("converting {} file contents to UTF-8", &git_ref))
258     }
259 
260     /// Create the commit message
build_commit_msg(subj: &str, from: &str, to: &str, footer: &str) -> String261     fn build_commit_msg(subj: &str, from: &str, to: &str, footer: &str) -> String {
262         format!(
263             "[patch_sync] {subj}\n\n\
264 Copies new PATCHES.json changes from {from} to {to}.\n
265 For questions about this job, contact [email protected]\n
266 This change is generated automatically by the script go/llvm-patch-sync\n\n
267 {footer}",
268         )
269     }
270 }
271 
272 /// Return the path of an ebuild located within the given directory.
find_ebuild(dir: &Path) -> Result<PathBuf>273 fn find_ebuild(dir: &Path) -> Result<PathBuf> {
274     // The logic here is that we create an iterator over all file paths to ebuilds
275     // with _pre in the name. Then we sort those ebuilds based on their revision numbers.
276     // Then we return the highest revisioned one.
277 
278     let ebuild_rev_matcher = Regex::new(r"-r([0-9]+)\.ebuild").unwrap();
279     // For LLVM ebuilds, we only want to check for ebuilds that have this in their file name.
280     let per_heuristic = "_pre";
281     // Get an iterator over all ebuilds with a _per in the file name.
282     let ebuild_candidates = fs::read_dir(dir)?.filter_map(|entry| {
283         let entry = entry.ok()?;
284         let path = entry.path();
285         if path.extension()? != "ebuild" {
286             // Not an ebuild, ignore.
287             return None;
288         }
289         let stem = path.file_stem()?.to_str()?;
290         if stem.contains(per_heuristic) {
291             return Some(path);
292         }
293         None
294     });
295     let try_parse_ebuild_rev = |path: PathBuf| -> Option<(u64, PathBuf)> {
296         let name = path.file_name()?;
297         if let Some(rev_match) = ebuild_rev_matcher.captures(name.to_str()?) {
298             let rev_str = rev_match.get(1)?;
299             let rev_num = rev_str.as_str().parse::<u64>().ok()?;
300             return Some((rev_num, path));
301         }
302         // If it doesn't have a revision, then it's revision 0.
303         Some((0, path))
304     };
305     let mut sorted_candidates: Vec<_> =
306         ebuild_candidates.filter_map(try_parse_ebuild_rev).collect();
307     sorted_candidates.sort_unstable_by_key(|x| x.0);
308     let highest_rev_ebuild = sorted_candidates
309         .pop()
310         .ok_or_else(|| anyhow!("could not find ebuild"))?;
311     Ok(highest_rev_ebuild.1)
312 }
313 
314 /// Run a given git command from inside a specified git dir.
git_cd_cmd<I, S>(pwd: &Path, args: I) -> Result<Output> where I: IntoIterator<Item = S>, S: AsRef<OsStr>,315 pub fn git_cd_cmd<I, S>(pwd: &Path, args: I) -> Result<Output>
316 where
317     I: IntoIterator<Item = S>,
318     S: AsRef<OsStr>,
319 {
320     let mut command = Command::new("git");
321     command.current_dir(pwd).args(args);
322     let output = command.output()?;
323     if !output.status.success() {
324         bail!(
325             "git command failed:\n  {:?}\nstdout --\n{}\nstderr --\n{}",
326             command,
327             String::from_utf8_lossy(&output.stdout),
328             String::from_utf8_lossy(&output.stderr),
329         );
330     }
331     Ok(output)
332 }
333 
repo_cd_cmd<I, S>(pwd: &Path, args: I) -> Result<()> where I: IntoIterator<Item = S>, S: AsRef<OsStr>,334 pub fn repo_cd_cmd<I, S>(pwd: &Path, args: I) -> Result<()>
335 where
336     I: IntoIterator<Item = S>,
337     S: AsRef<OsStr>,
338 {
339     let mut command = Command::new("repo");
340     command.current_dir(pwd).args(args);
341     let status = command.status()?;
342     if !status.success() {
343         bail!("repo command failed:\n  {:?}  \n", command)
344     }
345     Ok(())
346 }
347 
348 #[cfg(test)]
349 mod test {
350     use super::*;
351     use rand::prelude::Rng;
352     use std::env;
353     use std::fs::File;
354 
355     #[test]
test_revbump_ebuild()356     fn test_revbump_ebuild() {
357         // Random number to append at the end of the test folder to prevent conflicts.
358         let rng: u32 = rand::thread_rng().gen();
359         let llvm_dir = env::temp_dir().join(format!("patch_sync_test_{}", rng));
360         fs::create_dir(&llvm_dir).expect("creating llvm dir in temp directory");
361 
362         {
363             // With revision
364             let ebuild_name = "llvm-13.0_pre433403_p20211019-r10.ebuild";
365             let ebuild_path = llvm_dir.join(ebuild_name);
366             File::create(ebuild_path).expect("creating test ebuild file");
367             let new_ebuild_path =
368                 RepoSetupContext::rev_bump_llvm(&llvm_dir).expect("rev bumping the ebuild");
369             assert!(
370                 new_ebuild_path.ends_with("llvm-13.0_pre433403_p20211019-r11.ebuild"),
371                 "{}",
372                 new_ebuild_path.display()
373             );
374             fs::remove_file(new_ebuild_path).expect("removing renamed ebuild file");
375         }
376         {
377             // Without revision
378             let ebuild_name = "llvm-13.0_pre433403_p20211019.ebuild";
379             let ebuild_path = llvm_dir.join(ebuild_name);
380             File::create(ebuild_path).expect("creating test ebuild file");
381             let new_ebuild_path =
382                 RepoSetupContext::rev_bump_llvm(&llvm_dir).expect("rev bumping the ebuild");
383             assert!(
384                 new_ebuild_path.ends_with("llvm-13.0_pre433403_p20211019-r1.ebuild"),
385                 "{}",
386                 new_ebuild_path.display()
387             );
388             fs::remove_file(new_ebuild_path).expect("removing renamed ebuild file");
389         }
390         {
391             // With both
392             let ebuild_name = "llvm-13.0_pre433403_p20211019.ebuild";
393             let ebuild_path = llvm_dir.join(ebuild_name);
394             File::create(&ebuild_path).expect("creating test ebuild file");
395             let ebuild_link_name = "llvm-13.0_pre433403_p20211019-r2.ebuild";
396             let ebuild_link_path = llvm_dir.join(ebuild_link_name);
397             File::create(ebuild_link_path).expect("creating test ebuild link file");
398             let new_ebuild_path =
399                 RepoSetupContext::rev_bump_llvm(&llvm_dir).expect("rev bumping the ebuild");
400             assert!(
401                 new_ebuild_path.ends_with("llvm-13.0_pre433403_p20211019-r3.ebuild"),
402                 "{}",
403                 new_ebuild_path.display()
404             );
405             fs::remove_file(new_ebuild_path).expect("removing renamed ebuild link file");
406             fs::remove_file(ebuild_path).expect("removing renamed ebuild file");
407         }
408 
409         fs::remove_dir(&llvm_dir).expect("removing temp test dir");
410     }
411 }
412