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