| // Copyright (C) 2025 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| use std::{ |
| path::{Path, PathBuf}, |
| process::{Command, ExitStatus, Output}, |
| str::from_utf8, |
| }; |
| |
| use anyhow::{bail, Result}; |
| use chrono::Datelike; |
| use clap::Parser; |
| use crate_updater::UpdatesTried; |
| use rand::seq::SliceRandom; |
| use rand::thread_rng; |
| use serde::Deserialize; |
| |
| #[derive(Parser)] |
| struct Cli { |
| /// Absolute path to a repo checkout of main-without-vendor. |
| /// It is strongly recommended that you use a source tree dedicated to |
| /// running this updater. |
| android_root: PathBuf, |
| } |
| |
| pub trait SuccessOrError { |
| fn success_or_error(self) -> Result<Self> |
| where |
| Self: std::marker::Sized; |
| } |
| impl SuccessOrError for ExitStatus { |
| fn success_or_error(self) -> Result<Self> { |
| if !self.success() { |
| let exit_code = |
| self.code().map(|code| format!("{code}")).unwrap_or("(unknown)".to_string()); |
| bail!("Process failed with exit code {exit_code}"); |
| } |
| Ok(self) |
| } |
| } |
| impl SuccessOrError for Output { |
| fn success_or_error(self) -> Result<Self> { |
| (&self).success_or_error()?; |
| Ok(self) |
| } |
| } |
| impl SuccessOrError for &Output { |
| fn success_or_error(self) -> Result<Self> { |
| if !self.status.success() { |
| let exit_code = |
| self.status.code().map(|code| format!("{code}")).unwrap_or("(unknown)".to_string()); |
| bail!( |
| "Process failed with exit code {}\nstdout:\n{}\nstderr:\n{}", |
| exit_code, |
| from_utf8(&self.stdout)?, |
| from_utf8(&self.stderr)? |
| ); |
| } |
| Ok(self) |
| } |
| } |
| |
| pub trait RunAndStreamOutput { |
| fn run_and_stream_output(&mut self) -> Result<ExitStatus>; |
| } |
| impl RunAndStreamOutput for Command { |
| fn run_and_stream_output(&mut self) -> Result<ExitStatus> { |
| self.spawn()?.wait()?.success_or_error() |
| } |
| } |
| |
| fn cleanup_and_sync_monorepo(monorepo_path: &Path) -> Result<()> { |
| Command::new("git") |
| .args(["restore", "--staged", "."]) |
| .current_dir(monorepo_path) |
| .run_and_stream_output()?; |
| Command::new("git") |
| .args(["restore", "."]) |
| .current_dir(monorepo_path) |
| .run_and_stream_output()?; |
| |
| Command::new("git") |
| .args(["clean", "-f", "-d"]) |
| .current_dir(monorepo_path) |
| .run_and_stream_output()?; |
| |
| Command::new("git") |
| .args(["checkout", "goog/main"]) |
| .current_dir(monorepo_path) |
| .run_and_stream_output()?; |
| |
| let output = Command::new("git") |
| .args(["status", "--porcelain", "."]) |
| .current_dir(monorepo_path) |
| .output()? |
| .success_or_error()?; |
| if !output.stdout.is_empty() { |
| bail!("Monorepo {} has uncommitted changes", monorepo_path.display()); |
| } |
| |
| Command::new("repo").args(["sync", "."]).current_dir(monorepo_path).run_and_stream_output()?; |
| |
| Ok(()) |
| } |
| |
| #[derive(Deserialize, Default, Debug)] |
| struct UpdateSuggestions { |
| updates: Vec<UpdateSuggestion>, |
| } |
| |
| #[derive(Deserialize, Default, Debug)] |
| struct UpdateSuggestion { |
| name: String, |
| version: String, |
| } |
| |
| fn sync_to_green(monorepo_path: &Path) -> Result<()> { |
| Command::new("prodcertstatus").run_and_stream_output()?; |
| Command::new("/google/data/ro/projects/android/smartsync_login").run_and_stream_output()?; |
| |
| let output = Command::new("/google/data/ro/projects/android/ab") |
| .args([ |
| "lkgb", |
| "--branch=git_main-without-vendor", |
| "--target=aosp_arm64-trunk_staging-userdebug", |
| "--raw", |
| "--custom_raw_format={o[buildId]}", |
| ]) |
| .output()? |
| .success_or_error()?; |
| let bid = from_utf8(&output.stdout)?.trim(); |
| println!("bid = {bid}"); |
| |
| Command::new("/google/data/ro/projects/android/smartsync_repo") |
| .args(["sync", "-j99", "-t", bid]) |
| .current_dir(monorepo_path) |
| .run_and_stream_output()?; |
| |
| // Even though we sync the rest of the repository to a green build, |
| // we sync the monorepo to tip-of-tree, which reduces merge conflicts |
| // and duplicate update CLs. |
| Command::new("repo").args(["sync", "."]).current_dir(monorepo_path).run_and_stream_output()?; |
| |
| Ok(()) |
| } |
| |
| fn get_suggestions(monorepo_path: &Path) -> Result<Vec<UpdateSuggestion>> { |
| // TODO: Improve update suggestion algorithm. |
| let mut suggestions = Vec::new(); |
| for compatibility in ["ignore", "loose", "strict"] { |
| let output = Command::new(monorepo_path.join("crate_tool")) |
| .args([ |
| "suggest-updates", |
| "--json", |
| "--patches", |
| "--semver-compatibility", |
| compatibility, |
| ]) |
| .current_dir(monorepo_path) |
| .output()? |
| .success_or_error()?; |
| let json: UpdateSuggestions = serde_json::from_slice(&output.stdout)?; |
| suggestions.extend(json.updates); |
| } |
| |
| // Return suggestions in random order. This reduces merge conflicts and ensures |
| // all crates eventually get tried, even if something goes wrong and the program |
| // terminates prematurely. |
| let mut rng = thread_rng(); |
| suggestions.shuffle(&mut rng); |
| |
| Ok(suggestions) |
| } |
| |
| fn try_update( |
| android_root: &Path, |
| monorepo_path: &Path, |
| crate_name: &str, |
| version: &str, |
| ) -> Result<()> { |
| println!("Trying to update {crate_name} to {version}"); |
| |
| Command::new(monorepo_path.join("crate_tool")) |
| .args(["update", crate_name, version]) |
| .current_dir(monorepo_path) |
| .run_and_stream_output()?; |
| |
| if Command::new("git") |
| .args(["diff", "--exit-code", "pseudo_crate/Cargo.toml"]) |
| .current_dir(monorepo_path) |
| .run_and_stream_output() |
| .is_ok() |
| { |
| bail!("Crate {crate_name} was already updated"); |
| } |
| |
| Command::new("/usr/bin/bash") |
| .args([ |
| "-c", |
| format!( |
| "source {}/build/envsetup.sh && lunch aosp_husky-trunk_staging-eng && mm && m rust", |
| android_root.display() |
| ) |
| .as_str(), |
| ]) |
| .env_remove("OUT_DIR") |
| .current_dir(monorepo_path) |
| .run_and_stream_output()?; |
| |
| let now = chrono::Utc::now(); |
| Command::new("repo") |
| .args([ |
| "start", |
| format!( |
| "automatic-crate-update-{}-{}-{}-{}-{}", |
| crate_name, |
| version, |
| now.year(), |
| now.month(), |
| now.day() |
| ) |
| .as_str(), |
| ]) |
| .current_dir(monorepo_path) |
| .run_and_stream_output()?; |
| Command::new("git").args(["add", "."]).current_dir(monorepo_path).run_and_stream_output()?; |
| Command::new("git") |
| .args([ |
| "commit", |
| "-m", |
| format!("Update {crate_name} to {version}\n\nTest: m rust").as_str(), |
| ]) |
| .current_dir(monorepo_path) |
| .run_and_stream_output()?; |
| Command::new("repo") |
| .args([ |
| "upload", |
| "-c", |
| ".", |
| "-y", |
| "-o", |
| "banned-words~skip", |
| "-o", |
| "nokeycheck", |
| "--label", |
| "Presubmit-Ready+1", |
| "--label", |
| "Autosubmit+1", |
| "-o", |
| "t=automatic-crate-updates", |
| ]) |
| .current_dir(monorepo_path) |
| .run_and_stream_output()?; |
| |
| Ok(()) |
| } |
| |
| fn main() -> Result<()> { |
| let args = Cli::parse(); |
| if !args.android_root.is_absolute() { |
| bail!("Must be an absolute path: {}", args.android_root.display()); |
| } |
| if !args.android_root.is_dir() { |
| bail!("Does not exist, or is not a directory: {}", args.android_root.display()); |
| } |
| let monorepo_path = args.android_root.join("external/rust/android-crates-io"); |
| |
| cleanup_and_sync_monorepo(&monorepo_path)?; |
| |
| sync_to_green(&monorepo_path)?; |
| |
| Command::new("/usr/bin/bash") |
| .args([ |
| "-c", |
| "source build/envsetup.sh && lunch aosp_husky-trunk_staging-eng && m cargo_embargo", |
| ]) |
| .env_remove("OUT_DIR") |
| .current_dir(&args.android_root) |
| .run_and_stream_output()?; |
| |
| let mut updates_tried = UpdatesTried::read()?; |
| for suggestion in get_suggestions(&monorepo_path)? { |
| let crate_name = suggestion.name.as_str(); |
| let version = suggestion.version.as_str(); |
| if updates_tried.contains(crate_name, version) { |
| println!("Skipping {crate_name} (already attempted recently)"); |
| continue; |
| } |
| cleanup_and_sync_monorepo(&monorepo_path)?; |
| let res = try_update(&args.android_root, &monorepo_path, crate_name, version) |
| .inspect_err(|e| println!("Update failed: {}", e)); |
| updates_tried.record(suggestion.name, suggestion.version, res.is_ok())?; |
| } |
| cleanup_and_sync_monorepo(&monorepo_path)?; |
| |
| Ok(()) |
| } |