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