xref: /aosp_15_r20/external/toolchain-utils/llvm_tools/patch_sync/src/main.rs (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
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 mod android_utils;
6 mod patch_parsing;
7 mod version_control;
8 
9 use std::borrow::ToOwned;
10 use std::collections::BTreeSet;
11 use std::path::{Path, PathBuf};
12 
13 use anyhow::{Context, Result};
14 use structopt::StructOpt;
15 
16 use patch_parsing::{filter_patches_by_platform, PatchCollection, PatchDictSchema, VersionRange};
17 use version_control::RepoSetupContext;
18 
main() -> Result<()>19 fn main() -> Result<()> {
20     match Opt::from_args() {
21         Opt::Show {
22             cros_checkout_path,
23             android_checkout_path,
24             sync,
25             keep_unmerged,
26         } => show_subcmd(ShowOpt {
27             cros_checkout_path,
28             android_checkout_path,
29             sync,
30             keep_unmerged,
31         }),
32         Opt::Transpose {
33             cros_checkout_path,
34             cros_reviewers,
35             old_cros_ref,
36             android_checkout_path,
37             android_reviewers,
38             old_android_ref,
39             sync,
40             verbose,
41             dry_run,
42             no_commit,
43             wip,
44             disable_cq,
45             uprev,
46         } => transpose_subcmd(TransposeOpt {
47             cros_checkout_path,
48             cros_reviewers: cros_reviewers
49                 .map(|r| r.split(',').map(ToOwned::to_owned).collect())
50                 .unwrap_or_default(),
51             old_cros_ref,
52             android_checkout_path,
53             android_reviewers: android_reviewers
54                 .map(|r| r.split(',').map(ToOwned::to_owned).collect())
55                 .unwrap_or_default(),
56             old_android_ref,
57             sync,
58             verbose,
59             dry_run,
60             no_commit,
61             wip,
62             disable_cq,
63             uprev_ebuilds: uprev,
64         }),
65     }
66 }
67 
68 struct ShowOpt {
69     cros_checkout_path: PathBuf,
70     android_checkout_path: PathBuf,
71     keep_unmerged: bool,
72     sync: bool,
73 }
74 
show_subcmd(args: ShowOpt) -> Result<()>75 fn show_subcmd(args: ShowOpt) -> Result<()> {
76     let ShowOpt {
77         cros_checkout_path,
78         android_checkout_path,
79         keep_unmerged,
80         sync,
81     } = args;
82     let ctx = RepoSetupContext {
83         cros_checkout: cros_checkout_path,
84         android_checkout: android_checkout_path,
85         sync_before: sync,
86         wip_mode: true,   // Has no effect, as we're not making changes
87         enable_cq: false, // Has no effect, as we're not uploading anything
88         uprev_ebuilds: false,
89     };
90     ctx.setup()?;
91     let make_collection = |platform: &str, patches_fp: &Path| -> Result<PatchCollection> {
92         let parsed_collection = PatchCollection::parse_from_file(patches_fp)
93             .with_context(|| format!("could not parse {} PATCHES.json", platform))?;
94         Ok(if keep_unmerged {
95             parsed_collection
96         } else {
97             filter_patches_by_platform(&parsed_collection, platform).map_patches(|p| {
98                 // Need to do this platforms creation as Rust 1.55 cannot use "from".
99                 let mut platforms = BTreeSet::new();
100                 platforms.insert(platform.to_string());
101                 PatchDictSchema {
102                     platforms,
103                     ..p.clone()
104                 }
105             })
106         })
107     };
108     let cur_cros_collection = make_collection("chromiumos", &ctx.cros_patches_path())?;
109     let cur_android_collection = make_collection("android", &ctx.android_patches_path())?;
110     let merged = cur_cros_collection.union(&cur_android_collection)?;
111     println!("{}", merged.serialize_patches()?);
112     Ok(())
113 }
114 
115 struct TransposeOpt {
116     cros_checkout_path: PathBuf,
117     old_cros_ref: String,
118     android_checkout_path: PathBuf,
119     old_android_ref: String,
120     sync: bool,
121     verbose: bool,
122     dry_run: bool,
123     no_commit: bool,
124     cros_reviewers: Vec<String>,
125     android_reviewers: Vec<String>,
126     wip: bool,
127     disable_cq: bool,
128     uprev_ebuilds: bool,
129 }
130 
transpose_subcmd(args: TransposeOpt) -> Result<()>131 fn transpose_subcmd(args: TransposeOpt) -> Result<()> {
132     let ctx = RepoSetupContext {
133         cros_checkout: args.cros_checkout_path,
134         android_checkout: args.android_checkout_path,
135         sync_before: args.sync,
136         wip_mode: args.wip,
137         enable_cq: !args.disable_cq,
138         uprev_ebuilds: args.uprev_ebuilds,
139     };
140     ctx.setup()?;
141     let cros_patches_path = ctx.cros_patches_path();
142     let android_patches_path = ctx.android_patches_path();
143 
144     // Get new Patches -------------------------------------------------------
145     let patch_parsing::PatchTemporalDiff {
146         cur_collection: cur_cros_collection,
147         new_patches: new_cros_patches,
148         version_updates: cros_version_updates,
149     } = patch_parsing::new_patches(
150         &cros_patches_path,
151         &ctx.old_cros_patch_contents(&args.old_cros_ref)?,
152         "chromiumos",
153     )
154     .context("finding new patches for chromiumos")?;
155     let patch_parsing::PatchTemporalDiff {
156         cur_collection: cur_android_collection,
157         new_patches: new_android_patches,
158         version_updates: android_version_updates,
159     } = patch_parsing::new_patches(
160         &android_patches_path,
161         &ctx.old_android_patch_contents(&args.old_android_ref)?,
162         "android",
163     )
164     .context("finding new patches for android")?;
165 
166     // Have to ignore patches that are already at the destination, even if
167     // the patches are new.
168     let new_cros_patches = new_cros_patches.subtract(&cur_android_collection)?;
169     let new_android_patches = new_android_patches.subtract(&cur_cros_collection)?;
170 
171     // Need to do an extra filtering step for Android, as AOSP doesn't
172     // want patches outside of the start/end bounds.
173     let android_llvm_version: u64 = {
174         let android_llvm_version_str =
175             android_utils::get_android_llvm_version(&ctx.android_checkout)?;
176         android_llvm_version_str.parse::<u64>().with_context(|| {
177             format!(
178                 "converting llvm version to u64: '{}'",
179                 android_llvm_version_str
180             )
181         })?
182     };
183     if args.verbose {
184         println!("Android LLVM version: r{}", android_llvm_version);
185     }
186     let new_cros_patches =
187         new_cros_patches.filter_patches(|p| match (p.get_from_version(), p.get_until_version()) {
188             (Some(start), Some(end)) => start <= android_llvm_version && android_llvm_version < end,
189             (Some(start), None) => start <= android_llvm_version,
190             (None, Some(end)) => android_llvm_version < end,
191             (None, None) => true,
192         });
193 
194     // Need to filter version updates to only existing patches to the other platform.
195     let cros_version_updates =
196         filter_version_changes(cros_version_updates, &cur_android_collection);
197     let android_version_updates =
198         filter_version_changes(android_version_updates, &cur_cros_collection);
199 
200     if args.verbose {
201         display_patches("New patches from ChromiumOS", &new_cros_patches);
202         display_version_updates("Version updates from ChromiumOS", &cros_version_updates);
203         display_patches("New patches from Android", &new_android_patches);
204         display_version_updates("Version updates from Android", &android_version_updates);
205     }
206 
207     if args.dry_run {
208         println!("--dry-run specified; skipping modifications");
209         return Ok(());
210     }
211 
212     modify_repos(
213         &ctx,
214         args.no_commit,
215         ModifyOpt {
216             new_cros_patches,
217             cur_cros_collection,
218             cros_version_updates,
219             cros_reviewers: args.cros_reviewers,
220             new_android_patches,
221             cur_android_collection,
222             android_version_updates,
223             android_reviewers: args.android_reviewers,
224         },
225     )
226 }
227 
228 struct ModifyOpt {
229     new_cros_patches: PatchCollection,
230     cur_cros_collection: PatchCollection,
231     cros_version_updates: Vec<(String, Option<VersionRange>)>,
232     cros_reviewers: Vec<String>,
233     new_android_patches: PatchCollection,
234     cur_android_collection: PatchCollection,
235     android_version_updates: Vec<(String, Option<VersionRange>)>,
236     android_reviewers: Vec<String>,
237 }
238 
modify_repos(ctx: &RepoSetupContext, no_commit: bool, opt: ModifyOpt) -> Result<()>239 fn modify_repos(ctx: &RepoSetupContext, no_commit: bool, opt: ModifyOpt) -> Result<()> {
240     // Cleanup on scope exit.
241     scopeguard::defer! {
242         ctx.cleanup();
243     }
244     // Transpose Patches -----------------------------------------------------
245     let mut cur_android_collection = opt.cur_android_collection;
246     let mut cur_cros_collection = opt.cur_cros_collection;
247     // Apply any version ranges and new patches, then write out.
248     if !opt.new_cros_patches.is_empty() || !opt.cros_version_updates.is_empty() {
249         cur_android_collection =
250             cur_android_collection.update_version_ranges(&opt.cros_version_updates);
251         opt.new_cros_patches
252             .transpose_write(&mut cur_android_collection)?;
253     }
254     if !opt.new_android_patches.is_empty() || !opt.android_version_updates.is_empty() {
255         cur_cros_collection =
256             cur_cros_collection.update_version_ranges(&opt.android_version_updates);
257         opt.new_android_patches
258             .transpose_write(&mut cur_cros_collection)?;
259     }
260 
261     if no_commit {
262         println!("--no-commit specified; not committing or uploading");
263         return Ok(());
264     }
265     // Commit and upload for review ------------------------------------------
266     // Note we want to check if the android patches are empty for CrOS, and
267     // vice versa. This is a little counterintuitive.
268     if !opt.new_android_patches.is_empty() {
269         ctx.cros_repo_upload(&opt.cros_reviewers)
270             .context("uploading chromiumos changes")?;
271     }
272     if !opt.new_cros_patches.is_empty() {
273         if let Err(e) = android_utils::sort_android_patches(&ctx.android_checkout) {
274             eprintln!(
275                 "Couldn't sort Android patches; continuing. Caused by: {}",
276                 e
277             );
278         }
279         ctx.android_repo_upload(&opt.android_reviewers)
280             .context("uploading android changes")?;
281     }
282     Ok(())
283 }
284 
285 /// Filter version changes that can't apply to a given collection.
filter_version_changes<T>( version_updates: T, other_platform_collection: &PatchCollection, ) -> Vec<(String, Option<VersionRange>)> where T: IntoIterator<Item = (String, Option<VersionRange>)>,286 fn filter_version_changes<T>(
287     version_updates: T,
288     other_platform_collection: &PatchCollection,
289 ) -> Vec<(String, Option<VersionRange>)>
290 where
291     T: IntoIterator<Item = (String, Option<VersionRange>)>,
292 {
293     version_updates
294         .into_iter()
295         .filter(|(rel_patch_path, _)| {
296             other_platform_collection
297                 .patches
298                 .iter()
299                 .any(|p| &p.rel_patch_path == rel_patch_path)
300         })
301         .collect()
302 }
303 
display_patches(prelude: &str, collection: &PatchCollection)304 fn display_patches(prelude: &str, collection: &PatchCollection) {
305     println!("{}", prelude);
306     if collection.patches.is_empty() {
307         println!("  [No Patches]");
308         return;
309     }
310     println!("{}", collection);
311 }
312 
display_version_updates(prelude: &str, version_updates: &[(String, Option<VersionRange>)])313 fn display_version_updates(prelude: &str, version_updates: &[(String, Option<VersionRange>)]) {
314     println!("{}", prelude);
315     if version_updates.is_empty() {
316         println!("  [No Version Changes]");
317         return;
318     }
319     for (rel_patch_path, _) in version_updates {
320         println!("*  {}", rel_patch_path);
321     }
322 }
323 
324 #[derive(Debug, structopt::StructOpt)]
325 #[structopt(name = "patch_sync", about = "A pipeline for syncing the patch code")]
326 enum Opt {
327     /// Show a combined view of the PATCHES.json file, without making any changes.
328     #[allow(dead_code)]
329     Show {
330         #[structopt(parse(from_os_str))]
331         cros_checkout_path: PathBuf,
332         #[structopt(parse(from_os_str))]
333         android_checkout_path: PathBuf,
334 
335         /// Keep a patch's platform field even if it's not merged at that platform.
336         #[structopt(long)]
337         keep_unmerged: bool,
338 
339         /// Run repo sync before transposing.
340         #[structopt(short, long)]
341         sync: bool,
342     },
343     /// Transpose patches from two PATCHES.json files
344     /// to each other.
345     Transpose {
346         /// Path to the ChromiumOS source repo checkout.
347         #[structopt(long = "cros-checkout", parse(from_os_str))]
348         cros_checkout_path: PathBuf,
349 
350         /// Emails to send review requests to during ChromiumOS upload.
351         /// Comma separated.
352         #[structopt(long = "cros-rev")]
353         cros_reviewers: Option<String>,
354 
355         /// Git ref (e.g. hash) for the ChromiumOS overlay to use as the base.
356         #[structopt(long = "overlay-base-ref")]
357         old_cros_ref: String,
358 
359         /// Path to the Android Open Source Project source repo checkout.
360         #[structopt(long = "aosp-checkout", parse(from_os_str))]
361         android_checkout_path: PathBuf,
362 
363         /// Emails to send review requests to during Android upload.
364         /// Comma separated.
365         #[structopt(long = "aosp-rev")]
366         android_reviewers: Option<String>,
367 
368         /// Git ref (e.g. hash) for the llvm_android repo to use as the base.
369         #[structopt(long = "aosp-base-ref")]
370         old_android_ref: String,
371 
372         /// Run repo sync before transposing.
373         #[structopt(short, long)]
374         sync: bool,
375 
376         /// Revbump/uprev ebuilds during transposing.
377         #[structopt(long)]
378         uprev: bool,
379 
380         /// Print information to stdout
381         #[structopt(short, long)]
382         verbose: bool,
383 
384         /// Do not change any files. Useful in combination with `--verbose`
385         /// Implies `--no-commit`.
386         #[structopt(long)]
387         dry_run: bool,
388 
389         /// Do not commit or upload any changes made.
390         #[structopt(long)]
391         no_commit: bool,
392 
393         /// Upload and send things for review, but mark as WIP and send no
394         /// emails.
395         #[structopt(long)]
396         wip: bool,
397 
398         /// Don't run CQ if set. Only has an effect if uploading.
399         #[structopt(long)]
400         disable_cq: bool,
401     },
402 }
403