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