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