• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright (C) 2025 The Android Open Source Project
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //      http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 use std::{
16     path::{Path, PathBuf},
17     process::{Command, ExitStatus, Output},
18     str::from_utf8,
19 };
20 
21 use anyhow::{bail, Result};
22 use chrono::Datelike;
23 use clap::Parser;
24 use crate_updater::UpdatesTried;
25 use rand::seq::SliceRandom;
26 use rand::thread_rng;
27 use serde::Deserialize;
28 
29 #[derive(Parser)]
30 struct Cli {
31     /// Absolute path to a repo checkout of aosp-main.
32     /// It is strongly recommended that you use a source tree dedicated to
33     /// running this updater.
34     android_root: PathBuf,
35 }
36 
37 pub trait SuccessOrError {
success_or_error(self) -> Result<Self> where Self: std::marker::Sized38     fn success_or_error(self) -> Result<Self>
39     where
40         Self: std::marker::Sized;
41 }
42 impl SuccessOrError for ExitStatus {
success_or_error(self) -> Result<Self>43     fn success_or_error(self) -> Result<Self> {
44         if !self.success() {
45             let exit_code =
46                 self.code().map(|code| format!("{code}")).unwrap_or("(unknown)".to_string());
47             bail!("Process failed with exit code {exit_code}");
48         }
49         Ok(self)
50     }
51 }
52 impl SuccessOrError for Output {
success_or_error(self) -> Result<Self>53     fn success_or_error(self) -> Result<Self> {
54         (&self).success_or_error()?;
55         Ok(self)
56     }
57 }
58 impl SuccessOrError for &Output {
success_or_error(self) -> Result<Self>59     fn success_or_error(self) -> Result<Self> {
60         if !self.status.success() {
61             let exit_code =
62                 self.status.code().map(|code| format!("{code}")).unwrap_or("(unknown)".to_string());
63             bail!(
64                 "Process failed with exit code {}\nstdout:\n{}\nstderr:\n{}",
65                 exit_code,
66                 from_utf8(&self.stdout)?,
67                 from_utf8(&self.stderr)?
68             );
69         }
70         Ok(self)
71     }
72 }
73 
74 pub trait RunAndStreamOutput {
run_and_stream_output(&mut self) -> Result<ExitStatus>75     fn run_and_stream_output(&mut self) -> Result<ExitStatus>;
76 }
77 impl RunAndStreamOutput for Command {
run_and_stream_output(&mut self) -> Result<ExitStatus>78     fn run_and_stream_output(&mut self) -> Result<ExitStatus> {
79         self.spawn()?.wait()?.success_or_error()
80     }
81 }
82 
cleanup_and_sync_monorepo(monorepo_path: &Path) -> Result<()>83 fn cleanup_and_sync_monorepo(monorepo_path: &Path) -> Result<()> {
84     Command::new("git")
85         .args(["restore", "--staged", "."])
86         .current_dir(monorepo_path)
87         .run_and_stream_output()?;
88     Command::new("git")
89         .args(["restore", "."])
90         .current_dir(monorepo_path)
91         .run_and_stream_output()?;
92 
93     Command::new("git")
94         .args(["clean", "-f", "-d"])
95         .current_dir(monorepo_path)
96         .run_and_stream_output()?;
97 
98     Command::new("git")
99         .args(["checkout", "aosp/main"])
100         .current_dir(monorepo_path)
101         .run_and_stream_output()?;
102 
103     let output = Command::new("git")
104         .args(["status", "--porcelain", "."])
105         .current_dir(monorepo_path)
106         .output()?
107         .success_or_error()?;
108     if !output.stdout.is_empty() {
109         bail!("Monorepo {} has uncommitted changes", monorepo_path.display());
110     }
111 
112     Command::new("repo").args(["sync", "."]).current_dir(monorepo_path).run_and_stream_output()?;
113 
114     Ok(())
115 }
116 
117 #[derive(Deserialize, Default, Debug)]
118 struct UpdateSuggestions {
119     updates: Vec<UpdateSuggestion>,
120 }
121 
122 #[derive(Deserialize, Default, Debug)]
123 struct UpdateSuggestion {
124     name: String,
125     version: String,
126 }
127 
sync_to_green(monorepo_path: &Path) -> Result<()>128 fn sync_to_green(monorepo_path: &Path) -> Result<()> {
129     Command::new("prodcertstatus").run_and_stream_output()?;
130     Command::new("/google/data/ro/projects/android/smartsync_login").run_and_stream_output()?;
131 
132     let output = Command::new("/google/data/ro/projects/android/ab")
133         .args([
134             "lkgb",
135             "--branch=aosp-main",
136             "--target=aosp_arm64-trunk_staging-userdebug",
137             "--raw",
138             "--custom_raw_format={o[buildId]}",
139         ])
140         .output()?
141         .success_or_error()?;
142     let bid = from_utf8(&output.stdout)?.trim();
143     println!("bid = {bid}");
144 
145     Command::new("/google/data/ro/projects/android/smartsync_repo")
146         .args(["sync", "-j99", "-t", bid])
147         .current_dir(monorepo_path)
148         .run_and_stream_output()?;
149 
150     // Even though we sync the rest of the repository to a green build,
151     // we sync the monorepo to tip-of-tree, which reduces merge conflicts
152     // and duplicate update CLs.
153     Command::new("repo").args(["sync", "."]).current_dir(monorepo_path).run_and_stream_output()?;
154 
155     Ok(())
156 }
157 
get_suggestions(monorepo_path: &Path) -> Result<Vec<UpdateSuggestion>>158 fn get_suggestions(monorepo_path: &Path) -> Result<Vec<UpdateSuggestion>> {
159     // TODO: Improve update suggestion algorithm.
160     let mut suggestions = Vec::new();
161     for compatibility in ["ignore", "loose", "strict"] {
162         let output = Command::new(monorepo_path.join("crate_tool"))
163             .args([
164                 "suggest-updates",
165                 "--json",
166                 "--patches",
167                 "--semver-compatibility",
168                 compatibility,
169             ])
170             .current_dir(monorepo_path)
171             .output()?
172             .success_or_error()?;
173         let json: UpdateSuggestions = serde_json::from_slice(&output.stdout)?;
174         suggestions.extend(json.updates);
175     }
176 
177     // Return suggestions in random order. This reduces merge conflicts and ensures
178     // all crates eventually get tried, even if something goes wrong and the program
179     // terminates prematurely.
180     let mut rng = thread_rng();
181     suggestions.shuffle(&mut rng);
182 
183     Ok(suggestions)
184 }
185 
try_update( android_root: &Path, monorepo_path: &Path, crate_name: &str, version: &str, ) -> Result<()>186 fn try_update(
187     android_root: &Path,
188     monorepo_path: &Path,
189     crate_name: &str,
190     version: &str,
191 ) -> Result<()> {
192     println!("Trying to update {crate_name} to {version}");
193 
194     Command::new(monorepo_path.join("crate_tool"))
195         .args(["update", crate_name, version])
196         .current_dir(monorepo_path)
197         .run_and_stream_output()?;
198 
199     if Command::new("git")
200         .args(["diff", "--exit-code", "pseudo_crate/Cargo.toml"])
201         .current_dir(monorepo_path)
202         .run_and_stream_output()
203         .is_ok()
204     {
205         bail!("Crate {crate_name} was already updated");
206     }
207 
208     Command::new("/usr/bin/bash")
209         .args([
210             "-c",
211             format!(
212                 "source {}/build/envsetup.sh && lunch aosp_husky-trunk_staging-eng && mm && m rust",
213                 android_root.display()
214             )
215             .as_str(),
216         ])
217         .env_remove("OUT_DIR")
218         .current_dir(monorepo_path)
219         .run_and_stream_output()?;
220 
221     let now = chrono::Utc::now();
222     Command::new("repo")
223         .args([
224             "start",
225             format!(
226                 "automatic-crate-update-{}-{}-{}-{}-{}",
227                 crate_name,
228                 version,
229                 now.year(),
230                 now.month(),
231                 now.day()
232             )
233             .as_str(),
234         ])
235         .current_dir(monorepo_path)
236         .run_and_stream_output()?;
237     Command::new("git").args(["add", "."]).current_dir(monorepo_path).run_and_stream_output()?;
238     Command::new("git")
239         .args([
240             "commit",
241             "-m",
242             format!("Update {crate_name} to {version}\n\nTest: m rust").as_str(),
243         ])
244         .current_dir(monorepo_path)
245         .run_and_stream_output()?;
246     Command::new("repo")
247         .args([
248             "upload",
249             "-c",
250             ".",
251             "-y",
252             "-o",
253             "banned-words~skip",
254             "-o",
255             "nokeycheck",
256             "--label",
257             "Presubmit-Ready+1",
258             "--label",
259             "Autosubmit+1",
260             "-o",
261             "t=automatic-crate-updates",
262         ])
263         .current_dir(monorepo_path)
264         .run_and_stream_output()?;
265 
266     Ok(())
267 }
268 
main() -> Result<()>269 fn main() -> Result<()> {
270     let args = Cli::parse();
271     if !args.android_root.is_absolute() {
272         bail!("Must be an absolute path: {}", args.android_root.display());
273     }
274     if !args.android_root.is_dir() {
275         bail!("Does not exist, or is not a directory: {}", args.android_root.display());
276     }
277     let monorepo_path = args.android_root.join("external/rust/android-crates-io");
278 
279     cleanup_and_sync_monorepo(&monorepo_path)?;
280 
281     sync_to_green(&monorepo_path)?;
282 
283     Command::new("/usr/bin/bash")
284         .args([
285             "-c",
286             "source build/envsetup.sh && lunch aosp_husky-trunk_staging-eng && m cargo_embargo",
287         ])
288         .env_remove("OUT_DIR")
289         .current_dir(&args.android_root)
290         .run_and_stream_output()?;
291 
292     let mut updates_tried = UpdatesTried::read()?;
293     for suggestion in get_suggestions(&monorepo_path)? {
294         let crate_name = suggestion.name.as_str();
295         let version = suggestion.version.as_str();
296         if updates_tried.contains(crate_name, version) {
297             println!("Skipping {crate_name} (already attempted recently)");
298             continue;
299         }
300         cleanup_and_sync_monorepo(&monorepo_path)?;
301         let res = try_update(&args.android_root, &monorepo_path, crate_name, version)
302             .inspect_err(|e| println!("Update failed: {}", e));
303         updates_tried.record(suggestion.name, suggestion.version, res.is_ok())?;
304     }
305     cleanup_and_sync_monorepo(&monorepo_path)?;
306 
307     Ok(())
308 }
309