Preupload check for managed crates.

Currently just stages every crate and verifies that it is the same as
what we are uploading.

This won't scale, but is OK for now.

Bug: 339424309
Test: Ran locally on a test migration of the atty crate
Change-Id: I41d7d8e270daae30d7e9c08b1b6cd129dad1d1c2
diff --git a/tools/external_crates/crate_health/src/main.rs b/tools/external_crates/crate_health/src/main.rs
index 75644bd..0d6e4ec 100644
--- a/tools/external_crates/crate_health/src/main.rs
+++ b/tools/external_crates/crate_health/src/main.rs
@@ -23,7 +23,8 @@
 use clap::{Parser, Subcommand};
 use crate_health::{
     copy_dir, default_repo_root, maybe_build_cargo_embargo, CrateCollection, Migratable,
-    NameAndVersionMap, NameAndVersionRef, NamedAndVersioned, PseudoCrate, RepoPath, VersionMatch,
+    NameAndVersion, NameAndVersionMap, NameAndVersionRef, NamedAndVersioned, PseudoCrate, RepoPath,
+    VersionMatch,
 };
 use glob::glob;
 use semver::Version;
@@ -33,6 +34,7 @@
     #[command(subcommand)]
     command: Cmd,
 
+    // The path to the Android source repo.
     #[arg(long, default_value_os_t=default_repo_root().unwrap_or(PathBuf::from(".")))]
     repo_root: PathBuf,
 
@@ -57,10 +59,13 @@
         /// The crate name. Also the directory name in external/rust/crates
         crate_name: String,
     },
+    /// Regenerate a crate directory.
     Regenerate {
         /// The crate name.
         crate_name: String,
     },
+    /// Regenerate all crates
+    RegenerateAll {},
     /// Run pre-upload checks.
     PreuploadCheck {
         /// List of changed files
@@ -161,7 +166,7 @@
             ));
             pseudo_crate.add(&NameAndVersionRef::new(&crate_name, &version))?;
 
-            regenerate(&args.repo_root, &crate_name)?;
+            regenerate(&args.repo_root, [crate_name.as_str()].into_iter())?;
 
             for entry in glob(
                 src_dir
@@ -179,8 +184,11 @@
 
             Ok(())
         }
-        Cmd::Regenerate { crate_name } => regenerate(&args.repo_root, &crate_name),
-        Cmd::PreuploadCheck { files: _ } => Ok(()),
+        Cmd::Regenerate { crate_name } => {
+            regenerate(&args.repo_root, [crate_name.as_str()].into_iter())
+        }
+        Cmd::RegenerateAll {} => regenerate_all(&args.repo_root),
+        Cmd::PreuploadCheck { files: _ } => preupload_check(&args.repo_root),
     }
 }
 
@@ -384,33 +392,65 @@
     }
 }
 
-pub fn regenerate(repo_root: &impl AsRef<Path>, crate_name: &str) -> Result<()> {
+pub fn regenerate<'a>(
+    repo_root: &impl AsRef<Path>,
+    crates: impl Iterator<Item = &'a str>,
+) -> Result<()> {
     let repo_root = repo_root.as_ref();
-    let android_crate_dir =
-        repo_root.join("external/rust/android-crates-io/crates").join(&crate_name);
-    if !android_crate_dir.exists() {
-        return Err(anyhow!(
-            "Crate {} not found in external/rust/android-crates-io/crates",
-            crate_name
-        ));
+
+    let version_match = stage(&repo_root, crates)?;
+
+    for pair in version_match.pairs() {
+        let source_version = NameAndVersion::from(&pair.source.key());
+        let pair = pair.to_compatible().ok_or(anyhow!(
+            "No compatible vendored crate found for {} v{}",
+            source_version.name(),
+            source_version.version()
+        ))?;
+
+        let android_crate_dir =
+            repo_root.join("external/rust/android-crates-io/crates").join(pair.source.name());
+        remove_dir_all(&android_crate_dir)?;
+        rename(pair.dest.staging_path().abs(), &android_crate_dir)?;
+    }
+
+    Ok(())
+}
+
+pub fn stage<'a>(
+    repo_root: &impl AsRef<Path>,
+    crates: impl Iterator<Item = &'a str>,
+) -> Result<VersionMatch<CrateCollection>> {
+    let repo_root = repo_root.as_ref();
+
+    let mut cc = CrateCollection::new(repo_root);
+    for crate_name in crates {
+        let android_crate_dir =
+            repo_root.join("external/rust/android-crates-io/crates").join(crate_name);
+        if !android_crate_dir.exists() {
+            return Err(anyhow!(
+                "Crate {} not found in external/rust/android-crates-io/crates",
+                crate_name
+            ));
+        }
+
+        // Source
+        cc.add_from(&android_crate_dir)?;
+        cc.map_field_mut().retain(|_nv, krate| krate.is_crates_io());
+        let num_versions = cc.get_versions(crate_name).count();
+        if num_versions != 1 {
+            return Err(anyhow!(
+                "Expected a single crate version for {}, but found {}. Crates with multiple versions are not supported yet.",
+                crate_name,
+                num_versions
+            ));
+        }
     }
 
     let pseudo_crate =
         PseudoCrate::new(RepoPath::new(repo_root, "external/rust/android-crates-io/pseudo_crate"));
     pseudo_crate.vendor()?;
 
-    // Source
-    let mut cc = CrateCollection::new(repo_root);
-    cc.add_from(&android_crate_dir)?;
-    cc.map_field_mut().retain(|_nv, krate| krate.is_crates_io());
-    if cc.map_field().len() != 1 {
-        return Err(anyhow!(
-            "Expected a single crate version for {}, but found {}. Crates with multiple versions are not supported yet.",
-            crate_name,
-            cc.map_field().len()
-        ));
-    }
-
     // Dest
     let mut dest = CrateCollection::new(repo_root);
     dest.add_from(&pseudo_crate.get_path().join(&"vendor").rel())?;
@@ -423,14 +463,45 @@
     version_match.generate_android_bps()?;
     version_match.diff_android_bps()?;
 
-    let compatible_pairs = version_match.compatible_pairs().collect::<Vec<_>>();
-    if compatible_pairs.len() != 1 {
-        return Err(anyhow!("Couldn't find a compatible version to migrate to"));
-    }
-    let pair = compatible_pairs.first().unwrap();
+    Ok(version_match)
+}
 
-    remove_dir_all(&android_crate_dir)?;
-    rename(pair.dest.staging_path().abs(), &android_crate_dir)?;
+pub fn regenerate_all(repo_root: &impl AsRef<Path>) -> Result<()> {
+    let repo_root = repo_root.as_ref();
+    let pseudo_crate =
+        PseudoCrate::new(RepoPath::new(repo_root, "external/rust/android-crates-io/pseudo_crate"));
+    regenerate(&repo_root, pseudo_crate.deps()?.keys().map(|k| k.as_str()))
+}
+
+pub fn preupload_check(repo_root: &impl AsRef<Path>) -> Result<()> {
+    let repo_root = repo_root.as_ref();
+    let pseudo_crate =
+        PseudoCrate::new(RepoPath::new(repo_root, "external/rust/android-crates-io/pseudo_crate"));
+    let version_match = stage(&repo_root, pseudo_crate.deps()?.keys().map(|k| k.as_str()))?;
+
+    for pair in version_match.pairs() {
+        let source_version = NameAndVersion::from(&pair.source.key());
+        let pair = pair.to_compatible().ok_or(anyhow!(
+            "No compatible vendored crate found for {} v{}",
+            source_version.name(),
+            source_version.version()
+        ))?;
+
+        let diff_status = Command::new("diff")
+            .args(["-u", "-r", "-w", "--no-dereference"])
+            .arg(pair.dest.staging_path().rel())
+            .arg(Path::new("external/rust/android-crates-io/crates").join(pair.source.name()))
+            .current_dir(repo_root)
+            .spawn()?
+            .wait()?;
+        if !diff_status.success() {
+            return Err(anyhow!(
+                "Found differences between {} and {}",
+                pair.source.path(),
+                pair.dest.staging_path()
+            ));
+        }
+    }
 
     Ok(())
 }
diff --git a/tools/external_crates/crate_health/src/pseudo_crate.rs b/tools/external_crates/crate_health/src/pseudo_crate.rs
index 844ed02..0757557 100644
--- a/tools/external_crates/crate_health/src/pseudo_crate.rs
+++ b/tools/external_crates/crate_health/src/pseudo_crate.rs
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 use std::{
+    collections::BTreeMap,
     fs::{create_dir, write},
     process::Command,
     str::from_utf8,
@@ -20,6 +21,7 @@
 
 use anyhow::{anyhow, Context, Result};
 use serde::Serialize;
+use serde_json::Value;
 use tinytemplate::TinyTemplate;
 
 use crate::{ensure_exists_and_empty, NamedAndVersioned, RepoPath};
@@ -137,4 +139,32 @@
         }
         Ok(())
     }
+    pub fn deps(&self) -> Result<BTreeMap<String, String>> {
+        let output = Command::new("cargo")
+            .args(["metadata", "--offline", "--format-version=1"])
+            .current_dir(self.path.abs())
+            .output()?;
+        if !output.status.success() {
+            println!("{}", from_utf8(&output.stderr)?);
+            return Err(anyhow!("Failed to run 'cargo metadata'"));
+        }
+        let metadata: Value = serde_json::from_slice(&output.stdout)?;
+        let mut deps = BTreeMap::new();
+        for dep in metadata["packages"][0]["dependencies"]
+            .as_array()
+            .ok_or(anyhow!("Failed to deserialize cargo metadata"))?
+        {
+            deps.insert(
+                dep["name"]
+                    .as_str()
+                    .ok_or(anyhow!("Failed to deserialize cargo metadata"))?
+                    .to_string(),
+                dep["req"]
+                    .as_str()
+                    .ok_or(anyhow!("Failed to deserialize cargo metadata"))?
+                    .to_string(),
+            );
+        }
+        Ok(deps)
+    }
 }