Merge changes Ic1e88d3c,I86805b5f into main

* changes:
  Get rid of pseudo_crate from crate struct.
  Add a RepoPath struct.
diff --git a/tools/external_crates/crate_health/src/android_bp.rs b/tools/external_crates/crate_health/src/android_bp.rs
index 5b400d7..ec5b937 100644
--- a/tools/external_crates/crate_health/src/android_bp.rs
+++ b/tools/external_crates/crate_health/src/android_bp.rs
@@ -24,7 +24,7 @@
 use anyhow::{anyhow, Context, Result};
 use threadpool::ThreadPool;
 
-use crate::{Crate, NameAndVersion, NameAndVersionMap, NamedAndVersioned};
+use crate::{Crate, NameAndVersion, NameAndVersionMap, NamedAndVersioned, RepoPath};
 
 pub fn generate_android_bps<'a, T: Iterator<Item = &'a Crate>>(
     crates: T,
@@ -38,10 +38,9 @@
         let tx = tx.clone();
         let crate_name = krate.name().to_string();
         let crate_version = krate.version().clone();
-        let repo_root = krate.root().to_path_buf();
-        let test_path = krate.staging_path();
+        let staging_path = krate.staging_path();
         pool.execute(move || {
-            tx.send((crate_name, crate_version, generate_android_bp(&repo_root, &test_path)))
+            tx.send((crate_name, crate_version, generate_android_bp(&staging_path)))
                 .expect("Failed to send");
         });
     }
@@ -52,15 +51,12 @@
     Ok(results)
 }
 
-pub(crate) fn generate_android_bp(
-    repo_root: &impl AsRef<Path>,
-    staging_path: &impl AsRef<Path>,
-) -> Result<Output> {
-    let generate_android_bp_output = run_cargo_embargo(repo_root, staging_path)?;
+pub(crate) fn generate_android_bp(staging_path: &RepoPath) -> Result<Output> {
+    let generate_android_bp_output = run_cargo_embargo(staging_path)?;
     if !generate_android_bp_output.status.success() {
         println!(
             "cargo_embargo failed for {}\nstdout:\n{}\nstderr:\n{}",
-            staging_path.as_ref().display(),
+            staging_path,
             from_utf8(&generate_android_bp_output.stdout)?,
             from_utf8(&generate_android_bp_output.stderr)?
         );
@@ -68,12 +64,9 @@
     Ok(generate_android_bp_output)
 }
 
-fn run_cargo_embargo(
-    repo_root: &impl AsRef<Path>,
-    staging_path: &impl AsRef<Path>,
-) -> Result<Output> {
+fn run_cargo_embargo(staging_path: &RepoPath) -> Result<Output> {
     // Make sure we can find bpfmt.
-    let host_bin = repo_root.as_ref().join("out/host/linux-x86/bin");
+    let host_bin = staging_path.with_same_root(&"out/host/linux-x86/bin").abs();
     let new_path = match env::var_os("PATH") {
         Some(p) => {
             let mut paths = vec![host_bin];
@@ -83,12 +76,12 @@
         None => host_bin.as_os_str().into(),
     };
 
-    let staging_path_absolute = repo_root.as_ref().join(staging_path);
-    let mut cmd = Command::new(repo_root.as_ref().join("out/host/linux-x86/bin/cargo_embargo"));
+    let mut cmd =
+        Command::new(staging_path.with_same_root(&"out/host/linux-x86/bin/cargo_embargo").abs());
     cmd.args(["generate", "cargo_embargo.json"])
         .env("PATH", new_path)
-        .env("ANDROID_BUILD_TOP", repo_root.as_ref())
-        .current_dir(&staging_path_absolute)
+        .env("ANDROID_BUILD_TOP", staging_path.root())
+        .current_dir(staging_path.abs())
         .output()
         .context(format!("Failed to execute {:?}", cmd.get_program()))
 }
diff --git a/tools/external_crates/crate_health/src/bin/health_report.rs b/tools/external_crates/crate_health/src/bin/health_report.rs
index 7a4ffdb..bb78b33 100644
--- a/tools/external_crates/crate_health/src/bin/health_report.rs
+++ b/tools/external_crates/crate_health/src/bin/health_report.rs
@@ -40,7 +40,7 @@
     maybe_build_cargo_embargo(&args.repo_root, false)?;
 
     let mut cc = CrateCollection::new(args.repo_root);
-    cc.add_from(&"external/rust/crates", None::<&&str>)?;
+    cc.add_from(&"external/rust/crates")?;
     cc.map_field_mut().retain(|_nv, krate| krate.is_crates_io());
 
     cc.stage_crates()?;
diff --git a/tools/external_crates/crate_health/src/bin/migration_report.rs b/tools/external_crates/crate_health/src/bin/migration_report.rs
index 900d9aa..0901f53 100644
--- a/tools/external_crates/crate_health/src/bin/migration_report.rs
+++ b/tools/external_crates/crate_health/src/bin/migration_report.rs
@@ -17,7 +17,8 @@
 use anyhow::Result;
 use clap::Parser;
 use crate_health::{
-    default_output_dir, default_repo_root, maybe_build_cargo_embargo, migrate, ReportEngine,
+    default_output_dir, default_repo_root, maybe_build_cargo_embargo, migrate, RepoPath,
+    ReportEngine,
 };
 
 /// Generate a health report for crates in external/rust/crates
@@ -38,8 +39,10 @@
 
     maybe_build_cargo_embargo(&args.repo_root, false)?;
 
-    let migration =
-        migrate(args.repo_root, &"external/rust/crates", &"out/rust-crate-migration-report")?;
+    let migration = migrate(
+        RepoPath::new(args.repo_root.clone(), &"external/rust/crates"),
+        RepoPath::new(args.repo_root.clone(), &"out/rust-crate-migration-report"),
+    )?;
 
     let re = ReportEngine::new()?;
 
diff --git a/tools/external_crates/crate_health/src/crate_collection.rs b/tools/external_crates/crate_health/src/crate_collection.rs
index 413b5bc..7aef6ce 100644
--- a/tools/external_crates/crate_health/src/crate_collection.rs
+++ b/tools/external_crates/crate_health/src/crate_collection.rs
@@ -40,19 +40,11 @@
     pub fn new<P: Into<PathBuf>>(repo_root: P) -> CrateCollection {
         CrateCollection { crates: BTreeMap::new(), repo_root: repo_root.into() }
     }
-    pub fn add_from(
-        &mut self,
-        path: &impl AsRef<Path>,
-        pseudo_crate: Option<&impl AsRef<Path>>,
-    ) -> Result<()> {
+    pub fn add_from(&mut self, path: &impl AsRef<Path>) -> Result<()> {
         for entry_or_err in WalkDir::new(self.repo_root.join(path)) {
             let entry = entry_or_err?;
             if entry.file_name() == "Cargo.toml" {
-                match Crate::from(
-                    &entry.path(),
-                    &self.repo_root.as_path(),
-                    pseudo_crate.map(|p| p.as_ref()),
-                ) {
+                match Crate::from(&entry.path(), &self.repo_root.as_path()) {
                     Ok(krate) => self.crates.insert_or_error(
                         NameAndVersion::new(krate.name().to_string(), krate.version().clone()),
                         krate,
diff --git a/tools/external_crates/crate_health/src/crate_type.rs b/tools/external_crates/crate_health/src/crate_type.rs
index c2d93a9..bfe7d91 100644
--- a/tools/external_crates/crate_health/src/crate_type.rs
+++ b/tools/external_crates/crate_health/src/crate_type.rs
@@ -29,19 +29,15 @@
 
 use crate::{
     copy_dir, ensure_exists_and_empty, name_and_version::IsUpgradableTo, CrateError,
-    NameAndVersionRef, NamedAndVersioned,
+    NameAndVersionRef, NamedAndVersioned, RepoPath,
 };
 
 #[derive(Debug)]
 pub struct Crate {
     manifest: Manifest,
 
-    // root is absolute. All other paths are relative to it.
-    root: PathBuf,
-    relpath: PathBuf,
-    pseudo_crate: Option<PathBuf>,
+    path: RepoPath,
 
-    // compatible_dest_version: Option<Version>,
     patch_output: Vec<Output>,
     generate_android_bp_output: Option<Output>,
     android_bp_diff: Option<Output>,
@@ -62,28 +58,21 @@
 impl IsUpgradableTo for Crate {}
 
 impl Crate {
-    pub fn new<P: Into<PathBuf>, Q: Into<PathBuf>, R: Into<PathBuf>>(
+    pub fn new<P: Into<PathBuf>, Q: Into<PathBuf>>(
         manifest: Manifest,
         root: P,
         relpath: Q,
-        pseudo_crate: Option<R>,
     ) -> Crate {
+        let root: PathBuf = root.into();
         Crate {
             manifest,
-            root: root.into(),
-            relpath: relpath.into(),
-            pseudo_crate: pseudo_crate.map(|p| p.into()),
-            // compatible_dest_version: None,
+            path: RepoPath::new(root.clone(), relpath),
             patch_output: Vec::new(),
             generate_android_bp_output: None,
             android_bp_diff: None,
         }
     }
-    pub fn from<P: Into<PathBuf>, Q: Into<PathBuf>>(
-        cargo_toml: &impl AsRef<Path>,
-        root: P,
-        pseudo_crate: Option<Q>,
-    ) -> Result<Crate> {
+    pub fn from<P: Into<PathBuf>>(cargo_toml: &impl AsRef<Path>, root: P) -> Result<Crate> {
         let root: PathBuf = root.into();
         let manifest_dir = cargo_toml.as_ref().parent().ok_or(anyhow!(
             "Failed to get parent directory of manifest at {}",
@@ -94,36 +83,32 @@
         let (manifest, _nested) =
             read_manifest(cargo_toml.as_ref(), source_id, &Config::default()?)?;
         match manifest {
-            cargo::core::EitherManifest::Real(r) => Ok(Crate::new(r, root, relpath, pseudo_crate)),
+            cargo::core::EitherManifest::Real(r) => Ok(Crate::new(r, root, relpath)),
             cargo::core::EitherManifest::Virtual(_) => {
                 Err(anyhow!(CrateError::VirtualCrate(cargo_toml.as_ref().to_path_buf())))
             }
         }
     }
 
-    pub fn root(&self) -> &Path {
-        self.root.as_path()
+    pub fn path(&self) -> &RepoPath {
+        &self.path
     }
-    pub fn relpath(&self) -> &Path {
-        &self.relpath.as_path()
+    pub fn android_bp(&self) -> RepoPath {
+        self.path.join(&"Android.bp")
     }
-    pub fn path(&self) -> PathBuf {
-        self.root.join(&self.relpath)
+    pub fn cargo_embargo_json(&self) -> RepoPath {
+        self.path.join(&"cargo_embargo.json")
     }
-    pub fn android_bp(&self) -> PathBuf {
-        self.relpath().join("Android.bp")
+    pub fn staging_path(&self) -> RepoPath {
+        self.path.with_same_root(
+            Path::new("out/rust-crate-temporary-build").join(self.staging_dir_name()),
+        )
     }
-    pub fn cargo_embargo_json(&self) -> PathBuf {
-        self.path().join("cargo_embargo.json")
-    }
-    pub fn staging_path(&self) -> PathBuf {
-        Path::new("out/rust-crate-temporary-build").join(self.staging_dir_name())
-    }
-    pub fn patch_dir(&self) -> PathBuf {
-        self.staging_path().join("patches")
+    pub fn patch_dir(&self) -> RepoPath {
+        self.staging_path().join(&"patches")
     }
     pub fn staging_dir_name(&self) -> String {
-        if let Some(dirname) = self.relpath.file_name().and_then(|x| x.to_str()) {
+        if let Some(dirname) = self.path.rel().file_name().and_then(|x| x.to_str()) {
             if dirname == self.name() {
                 return dirname.to_string();
             }
@@ -132,17 +117,17 @@
     }
 
     pub fn aosp_url(&self) -> Option<String> {
-        if self.relpath.starts_with("external/rust/crates") {
-            if self.relpath.ends_with(self.name()) {
+        if self.path.rel().starts_with("external/rust/crates") {
+            if self.path.rel().ends_with(self.name()) {
                 Some(format!(
                     "https://android.googlesource.com/platform/{}/+/refs/heads/main",
-                    self.relpath().display()
+                    self.path()
                 ))
-            } else if self.relpath.parent()?.ends_with(self.name()) {
+            } else if self.path.rel().parent()?.ends_with(self.name()) {
                 Some(format!(
                     "https://android.googlesource.com/platform/{}/+/refs/heads/main/{}",
-                    self.relpath().parent()?.display(),
-                    self.relpath().file_name()?.to_str()?
+                    self.path().rel().parent()?.display(),
+                    self.path().rel().file_name()?.to_str()?
                 ))
             } else {
                 None
@@ -155,9 +140,6 @@
         format!("https://crates.io/crates/{}", self.name())
     }
 
-    pub fn is_vendored(&self) -> bool {
-        self.pseudo_crate.is_some()
-    }
     pub fn is_crates_io(&self) -> bool {
         const NOT_CRATES_IO: &'static [&'static str] = &[
             "external/rust/beto-rust/",                 // Google crates
@@ -166,19 +148,19 @@
             "external/rust/cxx/third-party/",           // Internal/example code
             "external/rust/cxx/demo/",                  // Internal/example code
         ];
-        !NOT_CRATES_IO.iter().any(|prefix| self.relpath.starts_with(prefix))
+        !NOT_CRATES_IO.iter().any(|prefix| self.path().rel().starts_with(prefix))
     }
     pub fn is_migration_denied(&self) -> bool {
         const MIGRATION_DENYLIST: &'static [&'static str] = &[
             "external/rust/crates/openssl/", // It's complicated.
             "external/rust/cxx/",            // It's REALLY complicated.
         ];
-        MIGRATION_DENYLIST.iter().any(|prefix| self.relpath.starts_with(prefix))
+        MIGRATION_DENYLIST.iter().any(|prefix| self.path().rel().starts_with(prefix))
     }
     pub fn is_android_bp_healthy(&self) -> bool {
         !self.is_migration_denied()
-            && self.root().join(self.android_bp()).exists()
-            && self.cargo_embargo_json().exists()
+            && self.android_bp().abs().exists()
+            && self.cargo_embargo_json().abs().exists()
             && self.generate_android_bp_success()
             && self.android_bp_unchanged()
     }
@@ -193,7 +175,7 @@
     }
 
     pub fn print(&self) -> Result<()> {
-        println!("{} {} {}", self.name(), self.version(), self.relpath.display());
+        println!("{} {} {}", self.name(), self.version(), self.path());
         if let Some(output) = &self.generate_android_bp_output {
             println!("generate Android.bp exit status: {}", output.status);
             println!("{}", from_utf8(&output.stdout)?);
@@ -209,14 +191,14 @@
 
     // Make a clean copy of the crate in out/
     pub fn stage_crate(&self) -> Result<()> {
-        let staging_path_absolute = self.root().join(self.staging_path());
+        let staging_path_absolute = self.staging_path().abs();
         ensure_exists_and_empty(&staging_path_absolute)?;
         remove_dir_all(&staging_path_absolute)
             .context(format!("Failed to remove {}", staging_path_absolute.display()))?;
-        copy_dir(&self.path(), &staging_path_absolute).context(format!(
+        copy_dir(&self.path().abs(), &staging_path_absolute).context(format!(
             "Failed to copy {} to {}",
-            self.path().display(),
-            staging_path_absolute.display()
+            self.path(),
+            self.staging_path()
         ))?;
         if staging_path_absolute.join(".git").is_dir() {
             remove_dir_all(staging_path_absolute.join(".git"))
@@ -228,9 +210,9 @@
     pub fn diff_android_bp(&mut self) -> Result<()> {
         self.set_diff_output(
             diff_android_bp(
-                &self.android_bp(),
-                &self.staging_path().join("Android.bp"),
-                &self.root(),
+                &self.android_bp().rel(),
+                &self.staging_path().join(&"Android.bp").rel(),
+                &self.path.root(),
             )
             .context("Failed to diff Android.bp".to_string())?,
         );
@@ -238,7 +220,7 @@
     }
 
     pub fn apply_patches(&mut self) -> Result<()> {
-        let patch_dir_absolute = self.root().join(self.patch_dir());
+        let patch_dir_absolute = self.patch_dir().abs();
         if patch_dir_absolute.exists() {
             for entry in read_dir(&patch_dir_absolute)
                 .context(format!("Failed to read_dir {}", patch_dir_absolute.display()))?
@@ -254,7 +236,7 @@
                 let output = Command::new("patch")
                     .args(["-p1", "-l", "--no-backup-if-mismatch", "-i"])
                     .arg(&entry_path)
-                    .current_dir(self.root().join(self.staging_path()))
+                    .current_dir(self.staging_path().abs())
                     .output()?;
                 if !output.status.success() {
                     println!(
@@ -296,8 +278,8 @@
     fn is_migration_eligible(&self) -> bool {
         self.is_crates_io()
             && !self.is_migration_denied()
-            && self.root.join(self.android_bp()).exists()
-            && self.cargo_embargo_json().exists()
+            && self.android_bp().abs().exists()
+            && self.cargo_embargo_json().abs().exists()
     }
     fn is_migratable(&self) -> bool {
         self.patch_success() && self.generate_android_bp_success() && self.android_bp_unchanged()
@@ -328,12 +310,15 @@
     fn test_from_and_properties() -> Result<()> {
         let temp_crate_dir = tempdir()?;
         let cargo_toml = write_test_manifest(temp_crate_dir.path(), "foo", "1.2.0")?;
-        let krate = Crate::from(&cargo_toml, &"/", None::<&&str>)?;
+        let krate = Crate::from(&cargo_toml, &"/")?;
         assert_eq!(krate.name(), "foo");
         assert_eq!(krate.version().to_string(), "1.2.0");
         assert!(krate.is_crates_io());
-        assert_eq!(krate.root().join(krate.android_bp()), temp_crate_dir.path().join("Android.bp"));
-        assert_eq!(krate.cargo_embargo_json(), temp_crate_dir.path().join("cargo_embargo.json"));
+        assert_eq!(krate.android_bp().abs(), temp_crate_dir.path().join("Android.bp"));
+        assert_eq!(
+            krate.cargo_embargo_json().abs(),
+            temp_crate_dir.path().join("cargo_embargo.json")
+        );
         Ok(())
     }
 
@@ -341,7 +326,7 @@
     fn test_from_error() -> Result<()> {
         let temp_crate_dir = tempdir()?;
         let cargo_toml = write_test_manifest(temp_crate_dir.path(), "foo", "1.2.0")?;
-        assert!(Crate::from(&cargo_toml, &"/blah", None::<&&str>).is_err());
+        assert!(Crate::from(&cargo_toml, &"/blah").is_err());
         Ok(())
     }
 }
diff --git a/tools/external_crates/crate_health/src/lib.rs b/tools/external_crates/crate_health/src/lib.rs
index c6e66a0..a6f88a1 100644
--- a/tools/external_crates/crate_health/src/lib.rs
+++ b/tools/external_crates/crate_health/src/lib.rs
@@ -48,6 +48,9 @@
 };
 mod name_and_version;
 
+pub use self::repo_path::RepoPath;
+mod repo_path;
+
 #[cfg(test)]
 pub use self::name_and_version_map::try_name_version_map_from_iter;
 pub use self::name_and_version_map::{
diff --git a/tools/external_crates/crate_health/src/main.rs b/tools/external_crates/crate_health/src/main.rs
index 15d8865..46cd8bc 100644
--- a/tools/external_crates/crate_health/src/main.rs
+++ b/tools/external_crates/crate_health/src/main.rs
@@ -18,7 +18,7 @@
 use clap::{Parser, Subcommand};
 use crate_health::{
     default_repo_root, maybe_build_cargo_embargo, migrate, CrateCollection, Migratable,
-    NameAndVersionMap, NamedAndVersioned,
+    NameAndVersionMap, NamedAndVersioned, RepoPath,
 };
 
 #[derive(Parser)]
@@ -145,7 +145,7 @@
             }
 
             let mut cc = CrateCollection::new(&args.repo_root);
-            cc.add_from(&PathBuf::from("external/rust/crates").join(&crate_name), None::<&&str>)?;
+            cc.add_from(&PathBuf::from("external/rust/crates").join(&crate_name))?;
             cc.map_field_mut().retain(|_nv, krate| krate.is_crates_io());
             if cc.map_field().len() != 1 {
                 return Err(anyhow!(
@@ -160,34 +160,23 @@
             cc.diff_android_bps()?;
 
             let krate = cc.map_field().values().next().unwrap();
-            println!(
-                "Found {} v{} in {}",
-                krate.name(),
-                krate.version(),
-                krate.relpath().display()
-            );
+            println!("Found {} v{} in {}", krate.name(), krate.version(), krate.path());
             let migratable;
             if !krate.is_android_bp_healthy() {
                 if krate.is_migration_denied() {
                     println!("This crate is on the migration denylist");
                 }
-                if !krate.root().join(krate.android_bp()).exists() {
-                    println!("There is no Android.bp file in {}", krate.relpath().display());
+                if !krate.android_bp().abs().exists() {
+                    println!("There is no Android.bp file in {}", krate.path());
                 }
-                if !krate.cargo_embargo_json().exists() {
-                    println!(
-                        "There is no cargo_embargo.json file in {}",
-                        krate.relpath().display()
-                    );
+                if !krate.cargo_embargo_json().abs().exists() {
+                    println!("There is no cargo_embargo.json file in {}", krate.path());
                 } else if !krate.generate_android_bp_success() {
-                    println!(
-                        "cargo_embargo execution did not succeed for {}",
-                        krate.relpath().display()
-                    );
+                    println!("cargo_embargo execution did not succeed for {}", krate.path());
                 } else if !krate.android_bp_unchanged() {
                     println!(
                         "Running cargo_embargo on {} produced changes to the Android.bp file:",
-                        krate.relpath().display()
+                        krate.path()
                     );
                     println!(
                         "{}",
@@ -202,9 +191,11 @@
                 migratable = false;
             } else {
                 let migration = migrate(
-                    &args.repo_root,
-                    &PathBuf::from("external/rust/crates").join(&crate_name),
-                    &"out/rust-crate-migration-report",
+                    RepoPath::new(
+                        args.repo_root.clone(),
+                        PathBuf::from("external/rust/crates").join(&crate_name),
+                    ),
+                    RepoPath::new(args.repo_root.clone(), &"out/rust-crate-migration-report"),
                 )?;
                 let compatible_pairs = migration.compatible_pairs().collect::<Vec<_>>();
                 if compatible_pairs.len() != 1 {
@@ -243,23 +234,23 @@
                 let diff_status = Command::new("diff")
                     .args(["-u", "-r", "-w", "--no-dereference"])
                     .args(IGNORED_FILES.iter().map(|ignored| format!("--exclude={}", ignored)))
-                    .arg(pair.source.relpath())
-                    .arg(pair.dest.staging_path())
+                    .arg(pair.source.path().rel())
+                    .arg(pair.dest.staging_path().rel())
                     .current_dir(&args.repo_root)
                     .spawn()?
                     .wait()?;
                 if !diff_status.success() {
                     println!(
                         "Found differences between {} and {}",
-                        pair.source.relpath().display(),
-                        pair.dest.staging_path().display()
+                        pair.source.path(),
+                        pair.dest.staging_path()
                     );
                 }
                 println!("All diffs:");
                 Command::new("diff")
                     .args(["-u", "-r", "-w", "-q", "--no-dereference"])
-                    .arg(pair.source.relpath())
-                    .arg(pair.dest.staging_path())
+                    .arg(pair.source.path().rel())
+                    .arg(pair.dest.staging_path().rel())
                     .current_dir(&args.repo_root)
                     .spawn()?
                     .wait()?;
diff --git a/tools/external_crates/crate_health/src/migration.rs b/tools/external_crates/crate_health/src/migration.rs
index 318a4c0..b996e04 100644
--- a/tools/external_crates/crate_health/src/migration.rs
+++ b/tools/external_crates/crate_health/src/migration.rs
@@ -15,7 +15,6 @@
 use std::{
     fs::{copy, read_link, remove_dir_all},
     os::unix::fs::symlink,
-    path::{Path, PathBuf},
     process::Output,
 };
 
@@ -24,7 +23,7 @@
 
 use crate::{
     copy_dir, crate_type::diff_android_bp, most_recent_version, CompatibleVersionPair, Crate,
-    CrateCollection, Migratable, NameAndVersionMap, PseudoCrate, VersionMatch,
+    CrateCollection, Migratable, NameAndVersionMap, PseudoCrate, RepoPath, VersionMatch,
 };
 
 static CUSTOMIZATIONS: &'static [&'static str] =
@@ -34,13 +33,14 @@
 
 impl<'a> CompatibleVersionPair<'a, Crate> {
     pub fn copy_customizations(&self) -> Result<()> {
-        let dest_dir_absolute = self.dest.root().join(self.dest.staging_path());
+        let dest_dir_absolute = self.dest.staging_path().abs();
         for pattern in CUSTOMIZATIONS {
             let full_pattern = self.source.path().join(pattern);
             for entry in glob(
                 full_pattern
+                    .abs()
                     .to_str()
-                    .ok_or(anyhow!("Failed to convert path {} to str", full_pattern.display()))?,
+                    .ok_or(anyhow!("Failed to convert path {} to str", full_pattern))?,
             )? {
                 let entry = entry?;
                 let filename = entry
@@ -51,7 +51,7 @@
                     copy_dir(&entry, &dest_dir_absolute.join(filename)).context(format!(
                         "Failed to copy {} to {}",
                         entry.display(),
-                        dest_dir_absolute.display()
+                        self.dest.staging_path()
                     ))?;
                 } else {
                     let dest_file = dest_dir_absolute.join(&filename);
@@ -68,8 +68,8 @@
         }
         for link in SYMLINKS {
             let src_path = self.source.path().join(link);
-            if src_path.is_symlink() {
-                let dest = read_link(src_path)?;
+            if src_path.abs().is_symlink() {
+                let dest = read_link(src_path.abs())?;
                 if dest.exists() {
                     return Err(anyhow!(
                         "Can't symlink {} -> {} because destination exists",
@@ -84,27 +84,26 @@
     }
     pub fn diff_android_bps(&self) -> Result<Output> {
         diff_android_bp(
-            &self.source.android_bp(),
-            &self.dest.staging_path().join("Android.bp"),
-            &self.source.root(),
+            &self.source.android_bp().rel(),
+            &self.dest.staging_path().join(&"Android.bp").rel(),
+            &self.source.path().root(),
         )
         .context("Failed to diff Android.bp".to_string())
     }
 }
 
-pub fn migrate<P: Into<PathBuf>>(
-    repo_root: P,
-    source_dir: &impl AsRef<Path>,
-    pseudo_crate_dir: &impl AsRef<Path>,
+pub fn migrate(
+    source_dir: RepoPath,
+    pseudo_crate_dir: RepoPath,
 ) -> Result<VersionMatch<CrateCollection>> {
-    let mut source = CrateCollection::new(repo_root);
-    source.add_from(source_dir, None::<&&str>)?;
+    let mut source = CrateCollection::new(source_dir.root());
+    source.add_from(&source_dir.rel())?;
     source.map_field_mut().retain(|_nv, krate| krate.is_crates_io());
 
-    let pseudo_crate = PseudoCrate::new(source.repo_root().join(pseudo_crate_dir));
-    if pseudo_crate.get_path().exists() {
-        remove_dir_all(pseudo_crate.get_path())
-            .context(format!("Failed to remove {}", pseudo_crate.get_path().display()))?;
+    let pseudo_crate = PseudoCrate::new(pseudo_crate_dir);
+    if pseudo_crate.get_path().abs().exists() {
+        remove_dir_all(pseudo_crate.get_path().abs())
+            .context(format!("Failed to remove {}", pseudo_crate.get_path()))?;
     }
     pseudo_crate.init(
         source
@@ -114,7 +113,7 @@
     )?;
 
     let mut dest = CrateCollection::new(source.repo_root());
-    dest.add_from(&pseudo_crate_dir.as_ref().join("vendor"), Some(pseudo_crate_dir))?;
+    dest.add_from(&pseudo_crate.get_path().join(&"vendor").rel())?;
 
     let mut version_match = VersionMatch::new(source, dest)?;
 
diff --git a/tools/external_crates/crate_health/src/pseudo_crate.rs b/tools/external_crates/crate_health/src/pseudo_crate.rs
index 55472c0..bcde2f3 100644
--- a/tools/external_crates/crate_health/src/pseudo_crate.rs
+++ b/tools/external_crates/crate_health/src/pseudo_crate.rs
@@ -14,7 +14,6 @@
 
 use std::{
     fs::{create_dir, write},
-    path::{Path, PathBuf},
     process::Command,
     str::from_utf8,
 };
@@ -23,7 +22,7 @@
 use serde::Serialize;
 use tinytemplate::TinyTemplate;
 
-use crate::{ensure_exists_and_empty, NamedAndVersioned};
+use crate::{ensure_exists_and_empty, NamedAndVersioned, RepoPath};
 
 static CARGO_TOML_TEMPLATE: &'static str = include_str!("templates/Cargo.toml.template");
 
@@ -39,25 +38,21 @@
 }
 
 pub struct PseudoCrate {
-    // Absolute path to pseudo-crate.
-    path: PathBuf,
+    path: RepoPath,
 }
 
 impl PseudoCrate {
-    pub fn new<P: Into<PathBuf>>(path: P) -> PseudoCrate {
-        PseudoCrate { path: path.into() }
+    pub fn new(path: RepoPath) -> PseudoCrate {
+        PseudoCrate { path }
     }
     pub fn init<'a>(
         &self,
         crates: impl Iterator<Item = &'a (impl NamedAndVersioned + 'a)>,
     ) -> Result<()> {
-        if self.path.exists() {
-            return Err(anyhow!(
-                "Can't init pseudo-crate because {} already exists",
-                self.path.display()
-            ));
+        if self.path.abs().exists() {
+            return Err(anyhow!("Can't init pseudo-crate because {} already exists", self.path));
         }
-        ensure_exists_and_empty(&self.path)?;
+        ensure_exists_and_empty(&self.path.abs())?;
 
         let mut deps = Vec::new();
         for krate in crates {
@@ -80,23 +75,24 @@
 
         let mut tt = TinyTemplate::new();
         tt.add_template("cargo_toml", CARGO_TOML_TEMPLATE)?;
-        let cargo_toml = self.path.join("Cargo.toml");
+        let cargo_toml = self.path.join(&"Cargo.toml").abs();
         write(&cargo_toml, tt.render("cargo_toml", &CargoToml { deps })?)?;
 
-        create_dir(self.path.join("src")).context("Failed to create src dir")?;
-        write(self.path.join("src/lib.rs"), "// Nothing").context("Failed to create src/lib.rs")?;
+        create_dir(self.path.join(&"src").abs()).context("Failed to create src dir")?;
+        write(self.path.join(&"src/lib.rs").abs(), "// Nothing")
+            .context("Failed to create src/lib.rs")?;
 
         self.vendor()
 
         // TODO: Run "cargo deny"
     }
-    pub fn get_path(&self) -> &Path {
-        self.path.as_path()
+    pub fn get_path(&self) -> &RepoPath {
+        &self.path
     }
     pub fn add(&self, krate: &impl NamedAndVersioned) -> Result<()> {
         let status = Command::new("cargo")
             .args(["add", format!("{}@={}", krate.name(), krate.version()).as_str()])
-            .current_dir(&self.path)
+            .current_dir(self.path.abs())
             .spawn()
             .context("Failed to spawn 'cargo add'")?
             .wait()
@@ -107,7 +103,8 @@
         Ok(())
     }
     pub fn vendor(&self) -> Result<()> {
-        let output = Command::new("cargo").args(["vendor"]).current_dir(&self.path).output()?;
+        let output =
+            Command::new("cargo").args(["vendor"]).current_dir(self.path.abs()).output()?;
         if !output.status.success() {
             return Err(anyhow!(
                 "cargo vendor failed with exit code {}\nstdout:\n{}\nstderr:\n{}",
diff --git a/tools/external_crates/crate_health/src/repo_path.rs b/tools/external_crates/crate_health/src/repo_path.rs
new file mode 100644
index 0000000..2092b79
--- /dev/null
+++ b/tools/external_crates/crate_health/src/repo_path.rs
@@ -0,0 +1,68 @@
+// Copyright (C) 2024 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 core::fmt::Display;
+use std::path::{Path, PathBuf};
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct RepoPath {
+    root: PathBuf,
+    path: PathBuf,
+}
+
+impl Display for RepoPath {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.path.display())
+    }
+}
+
+impl RepoPath {
+    pub fn new<P: Into<PathBuf>, Q: Into<PathBuf>>(root: P, path: Q) -> RepoPath {
+        let root: PathBuf = root.into();
+        let path: PathBuf = path.into();
+        assert!(root.is_absolute());
+        assert!(path.is_relative());
+        RepoPath { root, path }
+    }
+    pub fn root(&self) -> &Path {
+        self.root.as_path()
+    }
+    pub fn rel(&self) -> &Path {
+        self.path.as_path()
+    }
+    pub fn abs(&self) -> PathBuf {
+        self.root.join(&self.path)
+    }
+    pub fn join(&self, path: &impl AsRef<Path>) -> RepoPath {
+        RepoPath::new(self.root.clone(), self.path.join(path))
+    }
+    pub fn with_same_root<P: Into<PathBuf>>(&self, path: P) -> RepoPath {
+        RepoPath::new(self.root.clone(), path)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_basic() {
+        let p = RepoPath::new(&"/foo", &"bar");
+        assert_eq!(p.root(), Path::new("/foo"));
+        assert_eq!(p.rel(), Path::new("bar"));
+        assert_eq!(p.abs(), PathBuf::from("/foo/bar"));
+        assert_eq!(p.join(&"baz"), RepoPath::new("/foo", "bar/baz"));
+        assert_eq!(p.with_same_root(&"baz"), RepoPath::new("/foo", "baz"));
+    }
+}
diff --git a/tools/external_crates/crate_health/src/reports.rs b/tools/external_crates/crate_health/src/reports.rs
index 488bc58..49756c0 100644
--- a/tools/external_crates/crate_health/src/reports.rs
+++ b/tools/external_crates/crate_health/src/reports.rs
@@ -80,9 +80,9 @@
             table.add_row(&[
                 &linkify(&krate.name(), &krate.crates_io_url()),
                 &krate.version().to_string(),
-                &krate.aosp_url().map_or(format!("{}", krate.relpath().display()), |url| {
-                    linkify(&krate.relpath().display(), &url)
-                }),
+                &krate
+                    .aosp_url()
+                    .map_or(format!("{}", krate.path()), |url| linkify(&krate.path(), &url)),
             ]);
         }
         Ok(self.tt.render("table", &table)?)
@@ -103,10 +103,10 @@
             table.add_row(&[
                 &linkify(&krate.name(), &krate.crates_io_url()),
                 &krate.version().to_string(),
-                &krate.aosp_url().map_or(format!("{}", krate.relpath().display()), |url| {
-                    linkify(&krate.relpath().display(), &url)
-                }),
-                &prefer_yes(krate.root().join(krate.android_bp()).exists()),
+                &krate
+                    .aosp_url()
+                    .map_or(format!("{}", krate.path()), |url| linkify(&krate.path(), &url)),
+                &prefer_yes(krate.android_bp().abs().exists()),
                 &prefer_yes_or_summarize(
                     krate.generate_android_bp_success(),
                     krate
@@ -126,7 +126,7 @@
                         .android_bp_diff()
                         .map_or("Error", |o| from_utf8(&o.stdout).unwrap_or("Error")),
                 ),
-                &prefer_yes(krate.cargo_embargo_json().exists()),
+                &prefer_yes(krate.cargo_embargo_json().abs().exists()),
                 &prefer_no(krate.is_migration_denied()),
             ]);
         }
@@ -149,9 +149,9 @@
                 &linkify(&source.name(), &source.crates_io_url()),
                 &source.version().to_string(),
                 &dest_version,
-                &source.aosp_url().map_or(format!("{}", source.relpath().display()), |url| {
-                    linkify(&source.relpath().display(), &url)
-                }),
+                &source
+                    .aosp_url()
+                    .map_or(format!("{}", source.path()), |url| linkify(&source.path(), &url)),
             ]);
         }
         Ok(self.tt.render("table", &table)?)
@@ -174,13 +174,13 @@
             table.add_row(&[
                 &linkify(&krate.name(), &krate.crates_io_url()),
                 &krate.version().to_string(),
-                &krate.aosp_url().map_or(format!("{}", krate.relpath().display()), |url| {
-                    linkify(&krate.relpath().display(), &url)
-                }),
+                &krate
+                    .aosp_url()
+                    .map_or(format!("{}", krate.path()), |url| linkify(&krate.path(), &url)),
                 &prefer_yes(krate.is_crates_io()),
                 &prefer_no(krate.is_migration_denied()),
-                &prefer_yes(krate.root().join(krate.android_bp()).exists()),
-                &prefer_yes(krate.cargo_embargo_json().exists()),
+                &prefer_yes(krate.android_bp().abs().exists()),
+                &prefer_yes(krate.cargo_embargo_json().abs().exists()),
             ]);
         }
         Ok(self.tt.render("table", &table)?)
@@ -205,9 +205,9 @@
             table.add_row(&[
                 &linkify(&source.name(), &source.crates_io_url()),
                 &source.version().to_string(),
-                &source.aosp_url().map_or(format!("{}", source.relpath().display()), |url| {
-                    linkify(&source.relpath().display(), &url)
-                }),
+                &source
+                    .aosp_url()
+                    .map_or(format!("{}", source.path()), |url| linkify(&source.path(), &url)),
                 maybe_dest.map_or(&"None", |dest| {
                     if dest.version() != source.version() {
                         dest.version()