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