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