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