Use the feature resolver for imports and updates.

Also rework and improve the dependency differ to use the fact that the
Dependency type is hashable.

Get rid of crate_tool/src/crates_io.rs because all functionality is now
superseded by crates_io_util.

Test: treehugger, cargo test, ./crate_tool analyze-updates ahash
Change-Id: If7f5a7985afe032f966d13686de791500fafe6fc
diff --git a/tools/external_crates/crate_tool/src/crates_io.rs b/tools/external_crates/crate_tool/src/crates_io.rs
deleted file mode 100644
index 0b7fda5..0000000
--- a/tools/external_crates/crate_tool/src/crates_io.rs
+++ /dev/null
@@ -1,56 +0,0 @@
-// 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 crates_index::{Dependency, DependencyKind, Version};
-use crates_io_util::AndroidTarget;
-use semver::VersionReq;
-use std::collections::HashMap;
-
-/// Filter dependencies for those likely to be relevant to Android.
-pub trait AndroidDependencies {
-    fn android_deps(&self) -> impl DoubleEndedIterator<Item = &Dependency>;
-    fn android_version_reqs_by_name(&self) -> HashMap<&str, &str> {
-        self.android_deps().map(|dep| (dep.crate_name(), dep.requirement())).collect()
-    }
-    fn android_deps_with_version_reqs(
-        &self,
-    ) -> impl DoubleEndedIterator<Item = (&Dependency, VersionReq)> {
-        self.android_deps().filter_map(|dep| {
-            VersionReq::parse(dep.requirement()).map_or(None, |req| Some((dep, req)))
-        })
-    }
-}
-impl AndroidDependencies for Version {
-    fn android_deps(&self) -> impl DoubleEndedIterator<Item = &Dependency> {
-        self.dependencies().iter().filter(|dep| {
-            dep.kind() == DependencyKind::Normal && !dep.is_optional() && dep.is_android_target()
-        })
-    }
-}
-
-pub trait DependencyChanges {
-    fn is_new_dep(&self, base_deps: &HashMap<&str, &str>) -> bool;
-    fn is_changed_dep(&self, base_deps: &HashMap<&str, &str>) -> bool;
-}
-
-impl DependencyChanges for Dependency {
-    fn is_new_dep(&self, base_deps: &HashMap<&str, &str>) -> bool {
-        !base_deps.contains_key(self.crate_name())
-    }
-
-    fn is_changed_dep(&self, base_deps: &HashMap<&str, &str>) -> bool {
-        let base_dep = base_deps.get(self.crate_name());
-        base_dep.is_none() || base_dep.is_some_and(|base_req| *base_req != self.requirement())
-    }
-}
diff --git a/tools/external_crates/crate_tool/src/lib.rs b/tools/external_crates/crate_tool/src/lib.rs
index 20ca22a..037b9b0 100644
--- a/tools/external_crates/crate_tool/src/lib.rs
+++ b/tools/external_crates/crate_tool/src/lib.rs
@@ -27,7 +27,6 @@
 mod android_bp;
 mod crate_collection;
 mod crate_type;
-mod crates_io;
 mod managed_crate;
 mod managed_repo;
 mod patch;
diff --git a/tools/external_crates/crate_tool/src/managed_repo.rs b/tools/external_crates/crate_tool/src/managed_repo.rs
index 3c62a55..6e44974 100644
--- a/tools/external_crates/crate_tool/src/managed_repo.rs
+++ b/tools/external_crates/crate_tool/src/managed_repo.rs
@@ -22,7 +22,10 @@
 
 use anyhow::{anyhow, bail, Context, Result};
 use crates_index::DependencyKind;
-use crates_io_util::{CratesIoIndex, GetVersion, SafeVersions};
+use crates_io_util::{
+    CratesIoIndex, DependencyDiffer, FeatureResolver, GetVersion, ParsedVersion, ParsedVersionReq,
+    SafeVersions,
+};
 use google_metadata::GoogleMetadata;
 use itertools::Itertools;
 use license_checker::find_licenses;
@@ -38,7 +41,6 @@
     copy_dir,
     crate_collection::CrateCollection,
     crate_type::Crate,
-    crates_io::{AndroidDependencies, DependencyChanges},
     managed_crate::ManagedCrate,
     pseudo_crate::{CargoVendorDirty, PseudoCrate},
     upgradable::{IsUpgradableTo, MatchesWithCompatibilityRule, SemverCompatibilityRule},
@@ -178,8 +180,10 @@
 
         for version in cio_crate.safe_versions() {
             println!("Version {}", version.version());
+            let resolver = FeatureResolver::new(version);
             let mut found_problems = false;
-            for (dep, req) in version.android_deps_with_version_reqs() {
+            for dep in resolver.resolve(None as Option<Box<dyn Iterator<Item = &str>>>)? {
+                let req = dep.parsed_version_req()?;
                 let cc = if managed_crates.contains_crate(dep.crate_name()) {
                     &managed_crates
                 } else {
@@ -473,7 +477,7 @@
             krate.name(),
             krate.android_version()
         ))?;
-        let base_deps = base_version.android_version_reqs_by_name();
+        let dep_differ = DependencyDiffer::new(base_version);
 
         let mut newer_versions = cio_crate.safe_versions_gt(krate.android_version()).peekable();
         if newer_versions.peek().is_none() {
@@ -482,7 +486,8 @@
         for version in newer_versions {
             println!("Version {}", version.version());
             let mut found_problems = false;
-            let parsed_version = semver::Version::parse(version.version())?;
+            let parsed_version = version.parsed_version()?;
+            let resolver = FeatureResolver::new(version);
             if !krate
                 .android_version()
                 .is_upgradable_to(&parsed_version, SemverCompatibilityRule::Strict)
@@ -497,12 +502,13 @@
                     println!("  Semver-compatible, but only by relaxed standards since major version is 0");
                 }
             }
+            let diff = dep_differ.diff(version);
             // Check to see if the update has any missing dependencies.
             // We try to be a little clever about this in the following ways:
             // * Only consider deps that are likely to be relevant to Android. For example, ignore Windows-only deps.
             // * If a dep is missing, but the same dep exists for the current version of the crate, it's probably not actually necessary.
             // * Use relaxed version requirements, treating 0.x and 0.y as compatible, even though they aren't according to semver rules.
-            for (dep, req) in version.android_deps_with_version_reqs() {
+            for dep in resolver.resolve(None as Option<Box<dyn Iterator<Item = &str>>>)? {
                 let cc = if managed_crates.contains_crate(dep.crate_name()) {
                     &managed_crates
                 } else {
@@ -515,14 +521,14 @@
                         dep.crate_name(),
                         dep.requirement()
                     );
-                    if !dep.is_new_dep(&base_deps) {
+                    if !diff.is_added(dep) {
                         println!("    But the current version has the same dependency, and it seems to work");
                     } else {
                         continue;
                     }
                 }
                 for (_, dep_crate) in cc.get_versions(dep.crate_name()) {
-                    if !req.matches_with_compatibility_rule(
+                    if !dep.parsed_version_req()?.matches_with_compatibility_rule(
                         dep_crate.version(),
                         SemverCompatibilityRule::Loose,
                     ) {
@@ -534,7 +540,7 @@
                             dep_crate.version(),
                             dep_crate.path()
                         );
-                        if !dep.is_changed_dep(&base_deps) {
+                        if !diff.is_changed(dep) {
                             println!("    But the current version has the same dependency and it seems to work.")
                         }
                     }
@@ -575,7 +581,7 @@
                 continue;
             }
             let base_version = base_version.unwrap();
-            let base_deps = base_version.android_version_reqs_by_name();
+            let dep_differ = DependencyDiffer::new(base_version);
 
             let patch_dir = krate.path().join("patches").unwrap();
             if patch_dir.abs().exists() && !consider_patched_crates {
@@ -594,8 +600,10 @@
                 if !krate.version().is_upgradable_to(&parsed_version, semver_compatibility) {
                     continue;
                 }
-                if !version.android_deps_with_version_reqs().any(|(dep, req)| {
-                    if !dep.is_changed_dep(&base_deps) {
+                let resolver = FeatureResolver::new(version);
+                if !resolver.resolve(None as Option<Box<dyn Iterator<Item = &str>>>)?.any(|dep| {
+                    let diff = dep_differ.diff(version);
+                    if !diff.is_changed(dep) {
                         return false;
                     }
                     let cc = if managed_crates.contains_crate(dep.crate_name()) {
@@ -603,6 +611,9 @@
                     } else {
                         &legacy_crates
                     };
+                    let Ok(req) = dep.parsed_version_req() else {
+                        return false;
+                    };
                     for (_, dep_crate) in cc.get_versions(dep.crate_name()) {
                         if req.matches_with_compatibility_rule(
                             dep_crate.version(),
diff --git a/tools/external_crates/crates_io_util/src/android_target.rs b/tools/external_crates/crates_io_util/src/android_target.rs
index 9bc542a..5c6fe40 100644
--- a/tools/external_crates/crates_io_util/src/android_target.rs
+++ b/tools/external_crates/crates_io_util/src/android_target.rs
@@ -16,7 +16,7 @@
     targets::{Arch, Family, Os},
     Predicate, TargetPredicate,
 };
-use crates_index::Dependency;
+use crates_index::{Dependency, Version};
 
 /// Parse cfg expressions in dependencies and determine if they refer to a target relevant to Android.
 /// Dependencies are relevant if they are for Unix, Android, or Linux, and for an architecture we care about (Arm, RISC-V, or X86)
@@ -51,9 +51,32 @@
     })
 }
 
+/// Get the required Android dependencies of a crate.
+pub trait RequiredAndroidDeps {
+    /// Returns the required Android dependencies of a crate.
+    /// That is, dependencies that are:
+    /// * Non-optional
+    /// * Normal (not Build or Dev)
+    /// * If they have a target cfg expression, it applies to Android.
+    fn required_android_deps(&self) -> impl DoubleEndedIterator<Item = &crates_index::Dependency>;
+}
+
+impl RequiredAndroidDeps for Version {
+    fn required_android_deps(&self) -> impl DoubleEndedIterator<Item = &crates_index::Dependency> {
+        self.dependencies().iter().filter(|dep| {
+            dep.kind() == crates_index::DependencyKind::Normal
+                && !dep.is_optional()
+                && dep.is_android_target()
+        })
+    }
+}
+
 #[cfg(test)]
 mod tests {
+    use itertools::assert_equal;
+
     use super::*;
+
     #[test]
     fn test_android_cfgs() {
         assert!(!is_android("asmjs-unknown-emscripten"), "Parse error");
@@ -83,4 +106,19 @@
             "cfg(all(not(windows), any(rustix_use_libc, miri, not(all(target_os = \"linux\", target_endian = \"little\", any(target_arch = \"arm\", all(target_arch = \"aarch64\", target_pointer_width = \"64\"), target_arch = \"riscv64\", all(rustix_use_experimental_asm, target_arch = \"powerpc64\"), all(rustix_use_experimental_asm, target_arch = \"mips\"), all(rustix_use_experimental_asm, target_arch = \"mips32r6\"), all(rustix_use_experimental_asm, target_arch = \"mips64\"), all(rustix_use_experimental_asm, target_arch = \"mips64r6\"), target_arch = \"x86\", all(target_arch = \"x86_64\", target_pointer_width = \"64\")))))))"
         ), "rustix 0.38.31");
     }
+
+    #[test]
+    fn test_required_android_deps() {
+        let aarch64_paging_0_7_1: Version =
+            serde_json::from_str(include_str!("testdata/aarch64-paging-0.7.1"))
+                .expect("Failed to parse JSON testdata");
+        assert_equal(
+            aarch64_paging_0_7_1.required_android_deps(),
+            [aarch64_paging_0_7_1
+                .dependencies()
+                .iter()
+                .find(|dep| dep.name() == "bitflags")
+                .unwrap()],
+        );
+    }
 }
diff --git a/tools/external_crates/crates_io_util/src/dependency.rs b/tools/external_crates/crates_io_util/src/dependency.rs
new file mode 100644
index 0000000..d020e04
--- /dev/null
+++ b/tools/external_crates/crates_io_util/src/dependency.rs
@@ -0,0 +1,42 @@
+// 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 crates_index::Dependency;
+use semver::VersionReq;
+
+/// Trait for parsing version requirement strings of dependencies.
+pub trait ParsedVersionReq {
+    /// Parses a version requirement, returning an error if unsuccessful.
+    fn parsed_version_req(&self) -> Result<VersionReq, semver::Error>;
+}
+
+impl ParsedVersionReq for Dependency {
+    fn parsed_version_req(&self) -> Result<VersionReq, semver::Error> {
+        VersionReq::parse(self.requirement())
+    }
+}
+
+/// Trait for determining if dependencies in different versions of
+/// a crate are the same.
+pub trait SameDep {
+    /// Returns true if the two dependencies from different versions are
+    /// (probably) the same. That is, they have the same name and kind.
+    fn same_name_and_kind(&self, other: &Dependency) -> bool;
+}
+
+impl SameDep for Dependency {
+    fn same_name_and_kind(&self, other: &Dependency) -> bool {
+        self.name() == other.name() && self.kind() == other.kind()
+    }
+}
diff --git a/tools/external_crates/crates_io_util/src/dependency_diff.rs b/tools/external_crates/crates_io_util/src/dependency_diff.rs
index 6e580c4..2992812 100644
--- a/tools/external_crates/crates_io_util/src/dependency_diff.rs
+++ b/tools/external_crates/crates_io_util/src/dependency_diff.rs
@@ -12,74 +12,116 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-use std::collections::BTreeMap;
+use std::collections::{HashMap, HashSet};
 
 use crates_index::{Dependency, Version};
+use itertools::Itertools;
+use log::debug;
 
-use crate::DepSet;
+use crate::dependency::SameDep;
 
 /// Diff dependencies between two versions of a crate.
 /// Holds a reference to the current (base) dependencies of a crate,
 /// against which multiple newer versions can be compared.
 #[derive(Debug)]
 pub struct DependencyDiffer<'a> {
-    base_deps: DepSet<'a>,
+    base_version: &'a Version,
 }
 
-/// A pair of dependencies that differ between two versions of a crate.
-#[derive(Debug)]
-pub struct ChangedDep<'base, 'other> {
-    /// The dependencies of the base version of the crate.
-    pub base: &'base Dependency,
-    /// The dependencies of the other version of the crate.
-    pub other: &'other Dependency,
-}
+impl<'a> DependencyDiffer<'a> {
+    /// Constructs a DependencyDiffer with the specified base version.
+    pub fn new(base_version: &'a Version) -> DependencyDiffer<'a> {
+        DependencyDiffer { base_version }
+    }
+    /// Compares the base version with `other` and returns the differences.
+    pub fn diff<'other>(&'a self, other: &'other Version) -> DependencyDiff<'a, 'other> {
+        debug!(
+            "diff base:\n{}",
+            self.base_version
+                .dependencies()
+                .iter()
+                .map(|d| format!("{} {:?}", d.name(), d.kind()))
+                .join("\n")
+        );
+        debug!(
+            "diff other:\n{}",
+            other
+                .dependencies()
+                .iter()
+                .map(|d| format!("{} {:?}", d.name(), d.kind()))
+                .sorted()
+                .join("\n")
+        );
+        let added: HashSet<&Dependency> = other
+            .dependencies()
+            .iter()
+            .filter(|other_dep| {
+                !self
+                    .base_version
+                    .dependencies()
+                    .iter()
+                    .any(|base_dep| base_dep.same_name_and_kind(other_dep))
+            })
+            .collect();
+        debug!(
+            "diff added:\n{}",
+            added.iter().map(|d| format!("{} {:?}", d.name(), d.kind())).sorted().join("\n")
+        );
+        let deleted: HashSet<&Dependency> = self
+            .base_version
+            .dependencies()
+            .iter()
+            .filter(|base_dep| {
+                !other.dependencies().iter().any(|other_dep| base_dep.same_name_and_kind(other_dep))
+            })
+            .collect();
+        debug!(
+            "diff deleted:\n{}",
+            deleted.iter().map(|d| format!("{} {:?}", d.name(), d.kind())).join("\n")
+        );
+        let mut changed = HashMap::new();
+        for base_dep in self.base_version.dependencies() {
+            if let Some(other_dep) =
+                other.dependencies().iter().find(|other_dep| base_dep.same_name_and_kind(other_dep))
+            {
+                if base_dep != other_dep {
+                    changed.insert(other_dep, base_dep);
+                }
+            }
+        }
+        debug!(
+            "diff changed:\n{}",
+            changed.keys().map(|d| format!("{} {:?}", d.name(), d.kind())).sorted().join("\n")
+        );
 
-type ChangedDeps<'base, 'other> = BTreeMap<&'base str, ChangedDep<'base, 'other>>;
+        DependencyDiff { added, deleted, changed }
+    }
+}
 
 /// The difference between the dependencies of two versions of a crate.
 #[derive(Debug)]
 pub struct DependencyDiff<'base, 'other> {
     /// Newly added dependencies.
-    pub added: DepSet<'other>,
+    added: HashSet<&'other Dependency>,
     /// Dependencies that were deleted.
-    pub deleted: DepSet<'base>,
+    deleted: HashSet<&'base Dependency>,
     /// Dependencies that differ between the two versions. For example,
     /// they could have different version requirements.
-    pub changed: ChangedDeps<'base, 'other>,
+    changed: HashMap<&'other Dependency, &'base Dependency>,
 }
 
-impl<'a> DependencyDiffer<'a> {
-    /// Constructs a DependencyDiffer with the specified base version.
-    pub fn new(base: &'a Version) -> DependencyDiffer<'a> {
-        let base_deps = BTreeMap::from_iter(base.dependencies().iter().map(|d| (d.name(), d)));
-        DependencyDiffer { base_deps }
+impl<'base, 'other> DependencyDiff<'base, 'other> {
+    /// Returns true if the dependency is added in the new version.
+    pub fn is_added(&self, other: &'other Dependency) -> bool {
+        self.added.contains(other)
     }
-    /// Compares the base version with `other` and returns the differences.
-    pub fn diff<'other>(&'a self, other: &'other Version) -> DependencyDiff<'a, 'other> {
-        let other_deps = BTreeMap::from_iter(other.dependencies().iter().map(|d| (d.name(), d)));
-
-        let added = other_deps
-            .iter()
-            .filter(|(name, _)| !self.base_deps.contains_key(**name))
-            .map(|(name, dep)| (*name, *dep))
-            .collect();
-        let deleted = self
-            .base_deps
-            .iter()
-            .filter(|(name, _)| !other_deps.contains_key(**name))
-            .map(|(name, dep)| (*name, *dep))
-            .collect();
-        let mut changed = ChangedDeps::new();
-        for (name, base_dep) in &self.base_deps {
-            if let Some(other_dep) = other_deps.get(name) {
-                if base_dep != other_dep {
-                    changed.insert(*name, ChangedDep { base: base_dep, other: other_dep });
-                }
-            }
-        }
-
-        DependencyDiff { added, deleted, changed }
+    /// Returns true if the dependency is deleted in the new version.
+    pub fn is_deleted(&self, base: &'base Dependency) -> bool {
+        self.deleted.contains(base)
+    }
+    /// Returns true if the dependency is changed.
+    pub fn is_changed(&self, other: &'other Dependency) -> bool {
+        self.changed.contains_key(other)
     }
 }
 
@@ -87,10 +129,17 @@
 mod tests {
     use super::*;
 
-    use itertools::assert_equal;
+    use crates_index::DependencyKind;
+    use itertools::{assert_equal, Itertools};
+
+    fn init_logger() {
+        let _ =
+            env_logger::builder().filter_level(log::LevelFilter::max()).is_test(true).try_init();
+    }
 
     #[test]
     fn trivial() {
+        init_logger();
         let hashbrown_0_12_3: Version =
             serde_json::from_str(include_str!("testdata/hashbrown-0.12.3"))
                 .expect("Failed to parse JSON testdata");
@@ -103,6 +152,7 @@
 
     #[test]
     fn hashbrown() {
+        init_logger();
         let hashbrown_0_12_3: Version =
             serde_json::from_str(include_str!("testdata/hashbrown-0.12.3"))
                 .expect("Failed to parse JSON testdata");
@@ -111,21 +161,49 @@
                 .expect("Failed to parse JSON testdata");
         let differ = DependencyDiffer::new(&hashbrown_0_12_3);
         let diff = differ.diff(&hashbrown_0_14_5);
-        assert_equal(diff.added.keys(), ["allocator-api2", "equivalent", "rkyv"].iter());
-        assert!(diff.deleted.is_empty());
-        assert_equal(diff.changed.keys(), ["ahash", "bumpalo"].iter());
+        assert_equal(
+            diff.added
+                .iter()
+                .filter_map(
+                    |d| if d.kind() == DependencyKind::Normal { Some(d.name()) } else { None },
+                )
+                .sorted(),
+            ["allocator-api2", "equivalent", "rkyv"],
+        );
+        assert_equal(
+            diff.deleted
+                .iter()
+                .filter_map(
+                    |d| if d.kind() == DependencyKind::Normal { Some(d.name()) } else { None },
+                )
+                .sorted(),
+            ["bumpalo"],
+        );
+        assert_equal(
+            diff.changed
+                .keys()
+                .filter_map(
+                    |d| if d.kind() == DependencyKind::Normal { Some(d.name()) } else { None },
+                )
+                .sorted(),
+            ["ahash"],
+        );
     }
 
     #[test]
     fn winnow() {
+        init_logger();
         let winnow_0_5_37: Version = serde_json::from_str(include_str!("testdata/winnow-0.5.37"))
             .expect("Failed to parse JSON testdata");
         let winnow_0_6_20: Version = serde_json::from_str(include_str!("testdata/winnow-0.6.20"))
             .expect("Failed to parse JSON testdata");
         let differ = DependencyDiffer::new(&winnow_0_5_37);
         let diff = differ.diff(&winnow_0_6_20);
-        assert_equal(diff.added.keys(), ["annotate-snippets", "anyhow", "automod"].iter());
-        assert_equal(diff.deleted.keys(), ["escargot"].iter());
-        assert_equal(diff.changed.keys(), ["snapbox", "terminal_size"].iter());
+        assert_equal(
+            diff.added.iter().map(|d| d.name()).sorted(),
+            ["annotate-snippets", "anyhow", "automod"],
+        );
+        assert_equal(diff.deleted.iter().map(|d| d.name()).sorted(), ["escargot"]);
+        assert_equal(diff.changed.keys().map(|d| d.name()).sorted(), ["snapbox", "terminal_size"]);
     }
 }
diff --git a/tools/external_crates/crates_io_util/src/feature_resolver.rs b/tools/external_crates/crates_io_util/src/feature_resolver.rs
index 0ee9d29..b79fab8 100644
--- a/tools/external_crates/crates_io_util/src/feature_resolver.rs
+++ b/tools/external_crates/crates_io_util/src/feature_resolver.rs
@@ -12,14 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-use std::collections::{BTreeMap, BTreeSet};
+use std::collections::{BTreeMap, BTreeSet, HashSet};
 
 use cargo_toml::DependencyDetail;
-use crates_index::Version;
+use crates_index::{Dependency, Version};
 use itertools::Itertools;
 use log::debug;
 
-use crate::{AndroidTarget, DepSet, Error};
+use crate::{AndroidTarget, Error, RequiredAndroidDeps};
 
 /// Resolves a list of enabled features to a set of optional dependencies that these features switch on.
 pub struct FeatureResolver<'a> {
@@ -31,11 +31,18 @@
     pub fn new(version: &'a Version) -> FeatureResolver<'a> {
         FeatureResolver { version }
     }
-    /// Resolves a list of enabled features into a set of optional dependencies.
+    /// Resolves a list of enabled features into a set of required and optional dependencies.
     pub fn resolve(
+        &'a self,
+        features: Option<impl Iterator<Item = impl AsRef<str>>>,
+    ) -> Result<impl Iterator<Item = &'a crates_index::Dependency>, Error> {
+        Ok(self.version.required_android_deps().chain(self.resolve_optional(features)?))
+    }
+    /// Resolves a list of enabled features into a set of optional dependencies.
+    pub fn resolve_optional(
         &self,
         features: Option<impl Iterator<Item = impl AsRef<str>>>,
-    ) -> Result<DepSet<'a>, Error> {
+    ) -> Result<HashSet<&Dependency>, Error> {
         debug!("Resolving {}", self.version.name());
         let resolver = cargo_toml::features::Resolver::new();
 
@@ -92,7 +99,7 @@
         let parsed_features =
             resolver.parse_custom(self.version.features(), parse_deps.into_iter());
 
-        let mut resolved = DepSet::new();
+        let mut resolved = HashSet::new();
         let mut resolved_features = BTreeSet::new();
         let mut frontier = BTreeSet::new();
         if let Some(features) = features {
@@ -124,7 +131,7 @@
                             && dep.is_optional()
                             && dep.is_android_target()
                         {
-                            resolved.insert(dep.crate_name(), dep);
+                            resolved.insert(*dep);
                         } else {
                             debug!("        skipping. is_conditional = {}, optional = {}, android_target = {}", dep_action.is_conditional, dep.is_optional(), dep.is_android_target());
                         }
@@ -161,15 +168,24 @@
             serde_json::from_str(include_str!("testdata/hashbrown-0.12.3"))
                 .expect("Failed to parse JSON testdata");
         let resolver = FeatureResolver::new(&hashbrown_0_12_3);
-        assert_equal(resolver.resolve(Some(["bumpalo"].iter()))?.keys(), ["bumpalo"].iter());
-        assert_equal(resolver.resolve(Some(["default"].iter()))?.keys(), ["ahash"].iter());
+        assert_equal(
+            resolver.resolve_optional(Some(["bumpalo"].iter()))?.into_iter().map(|d| d.name()),
+            ["bumpalo"],
+        );
+        assert_equal(
+            resolver.resolve_optional(Some(["default"].iter()))?.into_iter().map(|d| d.name()),
+            ["ahash"],
+        );
         // ahash-compile-time-rng depends on ahash/compile-time-rng
         assert_equal(
-            resolver.resolve(Some(["ahash-compile-time-rng"].iter()))?.keys(),
-            ["ahash"].iter(),
+            resolver
+                .resolve_optional(Some(["ahash-compile-time-rng"].iter()))?
+                .into_iter()
+                .map(|d| d.name()),
+            ["ahash"],
         );
         assert!(
-            resolver.resolve(Some(["inline-more"].iter()))?.is_empty(),
+            resolver.resolve_optional(Some(["inline-more"].iter()))?.is_empty(),
             "inline-more has no deps associated with it"
         );
 
@@ -178,12 +194,16 @@
                 .expect("Failed to parse JSON testdata");
         let resolver = FeatureResolver::new(&hashbrown_0_14_5);
         assert!(
-            resolver.resolve(Some(["bumpalo"].iter())).is_err(),
+            resolver.resolve_optional(Some(["bumpalo"].iter())).is_err(),
             "bumpalo is no longer an optional dep in hashbrown-0.14.5"
         );
         assert_equal(
-            resolver.resolve(Some(["default"].iter()))?.keys(),
-            ["ahash", "allocator-api2"].iter(),
+            resolver
+                .resolve_optional(Some(["default"].iter()))?
+                .into_iter()
+                .map(|d| d.name())
+                .sorted(),
+            ["ahash", "allocator-api2"],
         );
         Ok(())
     }
@@ -195,8 +215,11 @@
                 .expect("Failed to parse JSON testdata");
         let resolver = FeatureResolver::new(&hashbrown_0_12_3);
         assert_equal(
-            resolver.resolve(Some(["ahash", "default", "inline-more", "raw"].iter()))?.keys(),
-            ["ahash"].iter(),
+            resolver
+                .resolve_optional(Some(["ahash", "default", "inline-more", "raw"].iter()))?
+                .into_iter()
+                .map(|d| d.name()),
+            ["ahash"],
         );
         Ok(())
     }
@@ -208,28 +231,40 @@
                 .expect("Failed to parse JSON testdata");
         let resolver = FeatureResolver::new(&hashbrown_0_12_3);
         let empty: Option<Box<dyn Iterator<Item = &str>>> = None;
-        assert_equal(resolver.resolve(empty), resolver.resolve(Some(["default"].iter())));
+        assert_equal(
+            resolver.resolve_optional(empty),
+            resolver.resolve_optional(Some(["default"].iter())),
+        );
 
         let hashbrown_0_14_5: Version =
             serde_json::from_str(include_str!("testdata/hashbrown-0.14.5"))
                 .expect("Failed to parse JSON testdata");
         let resolver = FeatureResolver::new(&hashbrown_0_14_5);
         let empty: Option<Box<dyn Iterator<Item = &str>>> = None;
-        assert_equal(resolver.resolve(empty), resolver.resolve(Some(["default"].iter())));
+        assert_equal(
+            resolver.resolve_optional(empty),
+            resolver.resolve_optional(Some(["default"].iter())),
+        );
 
         let winnow_0_5_37: Version = serde_json::from_str(include_str!("testdata/winnow-0.5.37"))
             .expect("Failed to parse JSON testdata");
         let resolver = FeatureResolver::new(&winnow_0_5_37);
         let empty: Option<Box<dyn Iterator<Item = &str>>> = None;
-        assert_equal(resolver.resolve(empty), resolver.resolve(Some(["default"].iter())));
+        assert_equal(
+            resolver.resolve_optional(empty),
+            resolver.resolve_optional(Some(["default"].iter())),
+        );
 
         let cfg_if_1_0_0: Version = serde_json::from_str(include_str!("testdata/cfg-if-1.0.0"))
             .expect("Failed to parse JSON testdata");
         let resolver = FeatureResolver::new(&cfg_if_1_0_0);
         let empty: Option<Box<dyn Iterator<Item = &str>>> = None;
-        assert!(resolver.resolve(empty)?.is_empty(), "cfg-if has no explicit 'default' feature");
         assert!(
-            resolver.resolve(Some(["default"].iter())).is_err(),
+            resolver.resolve_optional(empty)?.is_empty(),
+            "cfg-if has no explicit 'default' feature"
+        );
+        assert!(
+            resolver.resolve_optional(Some(["default"].iter())).is_err(),
             "cfg-if has no explicit 'default' feature"
         );
 
@@ -242,9 +277,12 @@
             .expect("Failed to parse JSON testdata");
         let resolver = FeatureResolver::new(&winnow_0_5_37);
         // "simd" depends on "dep:memchr"
-        assert_equal(resolver.resolve(Some(["simd"].iter()))?.keys(), ["memchr"].iter());
+        assert_equal(
+            resolver.resolve_optional(Some(["simd"].iter()))?.into_iter().map(|d| d.name()),
+            ["memchr"],
+        );
         assert!(
-            resolver.resolve(Some(["std"].iter()))?.is_empty(),
+            resolver.resolve_optional(Some(["std"].iter()))?.is_empty(),
             "'std' depends on 'memchr?/std', which should be omitted since it's optional."
         );
 
@@ -258,7 +296,7 @@
                 .expect("Failed to parse JSON testdata");
         let resolver = FeatureResolver::new(&aarch64_paging_0_7_1);
         let empty: Option<Box<dyn Iterator<Item = &str>>> = None;
-        assert_equal(resolver.resolve(empty)?.keys(), ["zerocopy"].iter());
+        assert_equal(resolver.resolve_optional(empty)?.into_iter().map(|d| d.name()), ["zerocopy"]);
 
         Ok(())
     }
@@ -270,7 +308,7 @@
         let resolver = FeatureResolver::new(&axum_0_7_0);
         let empty: Option<Box<dyn Iterator<Item = &str>>> = None;
         assert_equal(
-            resolver.resolve(empty)?.keys(),
+            resolver.resolve_optional(empty)?.into_iter().map(|d| d.name()).sorted(),
             [
                 "hyper",
                 "hyper-util",
@@ -278,8 +316,7 @@
                 "serde_path_to_error",
                 "serde_urlencoded",
                 "tokio",
-            ]
-            .iter(),
+            ],
         );
 
         Ok(())
@@ -291,7 +328,7 @@
             .expect("Failed to parse JSON testdata");
         let resolver = FeatureResolver::new(&chrono_0_4_39);
         let empty: Option<Box<dyn Iterator<Item = &str>>> = None;
-        assert!(resolver.resolve(empty)?.contains_key("android-tzdata"));
+        assert!(resolver.resolve_optional(empty)?.iter().any(|d| d.name() == "android-tzdata"));
         Ok(())
     }
 }
diff --git a/tools/external_crates/crates_io_util/src/lib.rs b/tools/external_crates/crates_io_util/src/lib.rs
index 13d63af..a439490 100644
--- a/tools/external_crates/crates_io_util/src/lib.rs
+++ b/tools/external_crates/crates_io_util/src/lib.rs
@@ -16,13 +16,11 @@
 //! of crates_index, with Android-specific helpers for dealing with features,
 //! dependencies, etc.
 
-use std::{collections::BTreeMap, io};
-
-use crates_index::Dependency;
-use semver::VersionReq;
-
 mod android_target;
-pub use android_target::AndroidTarget;
+pub use android_target::{AndroidTarget, RequiredAndroidDeps};
+
+mod dependency;
+pub use dependency::{ParsedVersionReq, SameDep};
 
 mod dependency_diff;
 pub use dependency_diff::DependencyDiffer;
@@ -42,20 +40,6 @@
 mod versions;
 pub use versions::{GetVersion, IsSafe, ParsedVersion, SafeVersions};
 
-type DepSet<'a> = BTreeMap<&'a str, &'a Dependency>;
-
-/// Trait for parsing version requirement strings of dependencies.
-pub trait ParsedVersionReq {
-    /// Parses a version requirement, returning an error if unsuccessful.
-    fn parsed_version_req(&self) -> Result<VersionReq, semver::Error>;
-}
-
-impl ParsedVersionReq for Dependency {
-    fn parsed_version_req(&self) -> Result<VersionReq, semver::Error> {
-        VersionReq::parse(self.requirement())
-    }
-}
-
 /// Error types for the 'crates_io_util' crate.
 #[derive(thiserror::Error, Debug)]
 pub enum Error {
@@ -93,7 +77,7 @@
     CratesIndexHttp(#[from] crates_index::http::Error),
     /// Propagated io::Error
     #[error(transparent)]
-    IoError(#[from] io::Error),
+    IoError(#[from] std::io::Error),
     /// Error running cargo
     #[error(transparent)]
     CargoError(#[from] success_or_error::Error),
diff --git a/tools/external_crates/crates_io_util/tests/resolver_test.rs b/tools/external_crates/crates_io_util/tests/resolver_test.rs
index 415096c..e8853b6 100644
--- a/tools/external_crates/crates_io_util/tests/resolver_test.rs
+++ b/tools/external_crates/crates_io_util/tests/resolver_test.rs
@@ -177,10 +177,9 @@
             .collect::<BTreeSet<String>>();
         let resolver = FeatureResolver::new(version);
         let optional = resolver
-            // .resolve(None as Option<Box<dyn Iterator<Item = &str>>>)?
-            .resolve(features.as_ref().map(|f| f.iter()))?
-            .into_keys()
-            .map(String::from)
+            .resolve_optional(features.as_ref().map(|f| f.iter()))?
+            .into_iter()
+            .map(|d| d.name().to_string())
             .collect::<BTreeSet<String>>();
         Ok(Deps { required, optional })
     }