1 // Copyright (C) 2024 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::{path::PathBuf, process::Command, str::from_utf8};
16 
17 use anyhow::{anyhow, Result};
18 use clap::{Parser, Subcommand};
19 use crate_health::{
20     default_repo_root, maybe_build_cargo_embargo, migrate, CrateCollection, Migratable,
21     NameAndVersionMap, NamedAndVersioned, RepoPath,
22 };
23 
24 #[derive(Parser)]
25 struct Cli {
26     #[command(subcommand)]
27     command: Cmd,
28 
29     #[arg(long, default_value_os_t=default_repo_root().unwrap_or(PathBuf::from(".")))]
30     repo_root: PathBuf,
31 
32     /// Rebuild cargo_embargo and bpfmt, even if they are already present in the out directory.
33     #[arg(long, default_value_t = false)]
34     rebuild_cargo_embargo: bool,
35 }
36 
37 #[derive(Subcommand)]
38 enum Cmd {
39     /// Check the health of a crate, and whether it is safe to migrate.
40     MigrationHealth {
41         /// The crate name. Also the directory name in external/rust/crates
42         crate_name: String,
43     },
44     /// Migrate a crate from external/rust/crates to the monorepo.
45     Migrate {
46         /// The crate name. Also the directory name in external/rust/crates
47         crate_name: String,
48     },
49     Regenerate {
50         /// The crate name.
51         crate_name: String,
52     },
53     /// Run pre-upload checks.
54     PreuploadCheck {
55         /// List of changed files
56         files: Vec<String>,
57     },
58 }
59 
60 static IGNORED_FILES: &'static [&'static str] = &[
61     ".appveyor.yml",
62     ".bazelci",
63     ".bazelignore",
64     ".bazelrc",
65     ".bazelversion",
66     ".buildkite",
67     ".cargo",
68     ".cargo-checksum.json",
69     ".cargo_vcs_info.json",
70     ".circleci",
71     ".cirrus.yml",
72     ".clang-format",
73     ".clang-tidy",
74     ".clippy.toml",
75     ".clog.toml",
76     ".clog.toml",
77     ".codecov.yaml",
78     ".codecov.yml",
79     ".editorconfig",
80     ".gcloudignore",
81     ".gdbinit",
82     ".git",
83     ".git-blame-ignore-revs",
84     ".git-ignore-revs",
85     ".gitallowed",
86     ".gitattributes",
87     ".github",
88     ".gitignore",
89     ".idea",
90     ".ignore",
91     ".istanbul.yml",
92     ".mailmap",
93     ".md-inc.toml",
94     ".mdl-style.rb",
95     ".mdlrc",
96     ".pylintrc",
97     ".pylintrc-examples",
98     ".pylintrc-tests",
99     ".reuse",
100     ".rspec",
101     ".rustfmt.toml",
102     ".shellcheckrc",
103     ".standard-version",
104     ".tarpaulin.toml",
105     ".tokeignore",
106     ".travis.yml",
107     ".versionrc",
108     ".vim",
109     ".vscode",
110     ".yapfignore",
111     ".yardopts",
112     "BUILD",
113     "Cargo.lock",
114     "Cargo.lock.saved",
115     "Cargo.toml.orig",
116     "OWNERS",
117     // rules.mk related files that we won't migrate.
118     "cargo2rulesmk.json",
119     "CleanSpec.mk",
120     "rules.mk",
121     // cargo_embargo intermediates.
122     "Android.bp.orig",
123     "cargo.metadata",
124     "cargo.out",
125     "target.tmp",
126 ];
127 
main() -> Result<()>128 fn main() -> Result<()> {
129     let args = Cli::parse();
130 
131     maybe_build_cargo_embargo(&args.repo_root, args.rebuild_cargo_embargo)?;
132 
133     match args.command {
134         Cmd::MigrationHealth { crate_name } => {
135             if args
136                 .repo_root
137                 .join("external/rust/android-crates-io/crates")
138                 .join(&crate_name)
139                 .exists()
140             {
141                 return Err(anyhow!(
142                     "Crate {} already exists in external/rust/android-crates-io/crates",
143                     crate_name
144                 ));
145             }
146 
147             let mut cc = CrateCollection::new(&args.repo_root);
148             cc.add_from(&PathBuf::from("external/rust/crates").join(&crate_name))?;
149             cc.map_field_mut().retain(|_nv, krate| krate.is_crates_io());
150             if cc.map_field().len() != 1 {
151                 return Err(anyhow!(
152                     "Expected a single crate version for {}, but found {}. Crates with multiple versions are not supported yet.",
153                     crate_name,
154                     cc.map_field().len()
155                 ));
156             }
157 
158             cc.stage_crates()?;
159             cc.generate_android_bps()?;
160             cc.diff_android_bps()?;
161 
162             let krate = cc.map_field().values().next().unwrap();
163             println!("Found {} v{} in {}", krate.name(), krate.version(), krate.path());
164             let migratable;
165             if !krate.is_android_bp_healthy() {
166                 if krate.is_migration_denied() {
167                     println!("This crate is on the migration denylist");
168                 }
169                 if !krate.android_bp().abs().exists() {
170                     println!("There is no Android.bp file in {}", krate.path());
171                 }
172                 if !krate.cargo_embargo_json().abs().exists() {
173                     println!("There is no cargo_embargo.json file in {}", krate.path());
174                 } else if !krate.generate_android_bp_success() {
175                     println!("cargo_embargo execution did not succeed for {}", krate.path());
176                 } else if !krate.android_bp_unchanged() {
177                     println!(
178                         "Running cargo_embargo on {} produced changes to the Android.bp file:",
179                         krate.path()
180                     );
181                     println!(
182                         "{}",
183                         from_utf8(
184                             &krate
185                                 .android_bp_diff()
186                                 .ok_or(anyhow!("No Android.bp diff found"))?
187                                 .stdout
188                         )?
189                     );
190                 }
191                 migratable = false;
192             } else {
193                 let migration = migrate(
194                     RepoPath::new(
195                         args.repo_root.clone(),
196                         PathBuf::from("external/rust/crates").join(&crate_name),
197                     ),
198                     RepoPath::new(args.repo_root.clone(), &"out/rust-crate-migration-report"),
199                 )?;
200                 let compatible_pairs = migration.compatible_pairs().collect::<Vec<_>>();
201                 if compatible_pairs.len() != 1 {
202                     return Err(anyhow!("Couldn't find a compatible version to migrate to",));
203                 }
204                 let pair = compatible_pairs.first().unwrap();
205                 if pair.source.version() != pair.dest.version() {
206                     println!(
207                         "Source and destination versions are different: {} -> {}",
208                         pair.source.version(),
209                         pair.dest.version()
210                     );
211                 }
212                 if !pair.dest.is_migratable() {
213                     if !pair.dest.patch_success() {
214                         println!("Patches did not apply successfully to the migrated crate");
215                         // TODO: Show errors.
216                     }
217                     if !pair.dest.generate_android_bp_success() {
218                         println!("cargo_embargo execution did not succeed for the migrated crate");
219                     } else if pair.dest.android_bp_unchanged() {
220                         println!("Running cargo_embargo for the migrated crate produced changes to the Android.bp file:");
221                         println!(
222                             "{}",
223                             from_utf8(
224                                 &pair
225                                     .dest
226                                     .android_bp_diff()
227                                     .ok_or(anyhow!("No Android.bp diff found"))?
228                                     .stdout
229                             )?
230                         );
231                     }
232                 }
233 
234                 let diff_status = Command::new("diff")
235                     .args(["-u", "-r", "-w", "--no-dereference"])
236                     .args(IGNORED_FILES.iter().map(|ignored| format!("--exclude={}", ignored)))
237                     .arg(pair.source.path().rel())
238                     .arg(pair.dest.staging_path().rel())
239                     .current_dir(&args.repo_root)
240                     .spawn()?
241                     .wait()?;
242                 if !diff_status.success() {
243                     println!(
244                         "Found differences between {} and {}",
245                         pair.source.path(),
246                         pair.dest.staging_path()
247                     );
248                 }
249                 println!("All diffs:");
250                 Command::new("diff")
251                     .args(["-u", "-r", "-w", "-q", "--no-dereference"])
252                     .arg(pair.source.path().rel())
253                     .arg(pair.dest.staging_path().rel())
254                     .current_dir(&args.repo_root)
255                     .spawn()?
256                     .wait()?;
257 
258                 migratable = pair.dest.is_migratable() && diff_status.success()
259             }
260 
261             println!(
262                 "The crate is {}",
263                 if krate.is_android_bp_healthy() && migratable { "healthy" } else { "UNHEALTHY" }
264             );
265         }
266         Cmd::Migrate { crate_name: _ } => todo!(),
267         Cmd::Regenerate { crate_name: _ } => todo!(),
268         Cmd::PreuploadCheck { files: _ } => todo!(),
269     }
270 
271     Ok(())
272 }
273