use super::lockfile::{CargoLock, CargoLockPackage};
use super::manifest::{CargoToml, TomlDependency};
use super::registry_config::{EffectiveSource, RegistryConfig, CRATES_IO_LOCK_SOURCE};
use super::sparse_index::{SparseIndexClient, SparseIndexEntry};
use super::vendor_source::VendorDir;
use anyhow::{bail, Context, Result};
use semver::{Version, VersionReq};
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::{Path, PathBuf};
pub mod manifest_extract;
pub use manifest_extract::{extract_dependencies_with_reqs, extract_root_package};
use std::sync::{Arc, Mutex};
type DepSpecs = Vec<(String, Option<String>)>;
#[derive(Debug, Clone)]
pub struct ManifestDirectDep {
pub name: String,
pub req: String,
pub source: String,
pub features: Vec<String>,
pub default_features: bool,
}
impl ManifestDirectDep {
pub fn crates_io(name: impl Into<String>, req: impl Into<String>) -> Self {
Self {
name: name.into(),
req: req.into(),
source: CRATES_IO_LOCK_SOURCE.to_string(),
features: Vec::new(),
default_features: true,
}
}
}
pub(crate) enum IndexSource {
Sparse(Arc<SparseIndexClient>),
Vendor(Arc<VendorDir>),
}
impl IndexSource {
pub(crate) fn versions(&self, name: &str, offline: bool) -> Result<Vec<SparseIndexEntry>> {
match self {
IndexSource::Sparse(c) => c.fetch_crate_versions(name, offline),
IndexSource::Vendor(v) => Ok(v.entries(name)),
}
}
}
pub struct NativeResolver {
index_client: Arc<SparseIndexClient>,
pub(crate) cache_dir: Option<std::path::PathBuf>,
pub(crate) config: Mutex<RegistryConfig>,
sources: Mutex<HashMap<String, Arc<IndexSource>>>,
pub(crate) patches: Mutex<super::patch_resolve::PatchSet>,
}
impl NativeResolver {
pub fn new() -> Result<Self> {
Self::for_cache_dir(None)
}
pub fn for_cache_dir(cache_dir: Option<&Path>) -> Result<Self> {
Ok(Self {
index_client: Arc::new(SparseIndexClient::for_cache_dir(cache_dir)?),
cache_dir: cache_dir.map(Path::to_path_buf),
config: Mutex::new(RegistryConfig::default()),
sources: Mutex::new(HashMap::new()),
patches: Mutex::new(Default::default()),
})
}
pub fn with_client(index_client: SparseIndexClient) -> Self {
Self {
index_client: Arc::new(index_client),
cache_dir: None,
config: Mutex::new(RegistryConfig::default()),
sources: Mutex::new(HashMap::new()),
patches: Mutex::new(Default::default()),
}
}
pub fn with_registry_config(self, config: RegistryConfig) -> Self {
*self.config.lock().unwrap() = config;
self.sources.lock().unwrap().clear();
self
}
pub(crate) fn source_for(&self, lock_source: &str) -> Result<Arc<IndexSource>> {
if let Some(found) = self.sources.lock().unwrap().get(lock_source) {
return Ok(found.clone());
}
let effective = self.config.lock().unwrap().effective_source(lock_source)?;
let source = match effective {
EffectiveSource::CratesIo => IndexSource::Sparse(self.index_client.clone()),
EffectiveSource::Sparse { index_url, token } => IndexSource::Sparse(Arc::new(
SparseIndexClient::for_index(self.cache_dir.as_deref(), &index_url, token)?,
)),
EffectiveSource::Directory(dir) => IndexSource::Vendor(VendorDir::open(&dir)?),
EffectiveSource::Git => bail!("{lock_source} is not a registry"),
};
let source = Arc::new(source);
self.sources
.lock()
.unwrap()
.insert(lock_source.to_string(), source.clone());
Ok(source)
}
pub fn resolve_manifest(&self, manifest_path: &Path, offline: bool) -> Result<CargoLock> {
let manifest_content = fs::read_to_string(manifest_path)
.with_context(|| format!("Failed to read manifest at {:?}", manifest_path))?;
let manifest_dir = manifest_path.parent().unwrap_or(Path::new("."));
let mut path_deps = Vec::new();
let mut remote_deps = Vec::new();
let mut member_deps: HashMap<String, DepSpecs> = HashMap::new();
let config = RegistryConfig::load(manifest_dir)?;
*self.config.lock().unwrap() = config.clone();
self.sources.lock().unwrap().clear();
*self.patches.lock().unwrap() = CargoToml::from_file(manifest_path)
.ok()
.map(|t| super::patch_resolve::PatchSet::from_manifest(&t, manifest_dir, &config))
.transpose()?
.unwrap_or_default();
let mut git_resolver = super::git_resolve::GitResolver::for_cache_dir(
self.cache_dir.as_deref(),
config.clone(),
);
let mut git_out = super::git_resolve::GitResolution::default();
if let Ok(toml) = CargoToml::from_file(manifest_path) {
let mut manifests: Vec<(PathBuf, CargoToml, bool)> =
vec![(manifest_dir.to_path_buf(), toml.clone(), true)];
if toml.workspace.is_some() {
if let Ok(members) = toml.discover_workspace_members(manifest_dir) {
for (m_name, m_dir) in members {
if let Ok(m_toml) = CargoToml::from_file(&m_dir.join("Cargo.toml")) {
let ver = m_toml
.package
.as_ref()
.and_then(|p| p.version.clone())
.unwrap_or_else(|| "0.1.0".to_string());
path_deps.push((m_name, ver));
if m_dir != manifest_dir {
manifests.push((m_dir, m_toml, true));
}
}
}
}
}
let local_names: HashSet<String> = path_deps.iter().map(|(n, _)| n.clone()).collect();
let mut seen_path_dirs: HashSet<PathBuf> = HashSet::new();
let mut i = 0;
while i < manifests.len() {
let (m_dir, m_toml, is_member) = manifests[i].clone();
i += 1;
let mut m_dep_list: DepSpecs = Vec::new();
let mut dep_tables: Vec<&HashMap<String, TomlDependency>> =
vec![&m_toml.dependencies, &m_toml.build_dependencies];
if is_member {
dep_tables.push(&m_toml.dev_dependencies);
}
for spec in m_toml.target.values() {
dep_tables.push(&spec.dependencies);
dep_tables.push(&spec.build_dependencies);
if is_member {
dep_tables.push(&spec.dev_dependencies);
}
}
for (dep_name, dep_val) in dep_tables.into_iter().flatten() {
match dep_val {
TomlDependency::Detailed(d) if d.path.is_some() => {
let dep_dir = m_dir.join(d.path.as_ref().unwrap());
let dep_m = dep_dir.join("Cargo.toml");
if let Ok(dt) = CargoToml::from_file(&dep_m) {
let p_ref = dt.package.as_ref();
let name = p_ref
.map(|p| p.name.clone())
.unwrap_or_else(|| dep_name.clone());
let ver = p_ref
.and_then(|p| p.version.clone())
.unwrap_or_else(|| "0.1.0".to_string());
m_dep_list.push((name.clone(), Some(ver.clone())));
if !path_deps.iter().any(|(n, v)| n == &name && v == &ver) {
path_deps.push((name.clone(), ver));
let canon =
dep_dir.canonicalize().unwrap_or_else(|_| dep_dir.clone());
if seen_path_dirs.insert(canon) {
manifests.push((dep_dir, dt, false));
}
}
}
}
TomlDependency::Detailed(d) if d.git.is_some() => {
let name = git_resolver.resolve(dep_name, d, offline, &mut git_out)?;
m_dep_list.push((name, None));
}
TomlDependency::Simple(req) => {
m_dep_list.push((dep_name.clone(), None));
if !local_names.contains(dep_name) {
remote_deps.push(ManifestDirectDep::crates_io(dep_name, req));
}
}
TomlDependency::Detailed(d) => {
let pkg = d.package_name.as_ref().unwrap_or(dep_name).clone();
m_dep_list.push((pkg.clone(), None));
if !local_names.contains(&pkg) {
remote_deps.push(ManifestDirectDep {
name: pkg,
req: d.version.clone().unwrap_or_else(|| "*".to_string()),
source: config
.lock_source_for_registry(d.registry.as_deref())?,
features: d.features.clone().unwrap_or_default(),
default_features: d.default_features.unwrap_or(true),
});
}
}
}
}
for value in m_toml.features.values().flatten() {
let Some((dep, feat)) = value.split_once('/') else {
continue;
};
let dep = dep.trim_end_matches('?');
for r in remote_deps.iter_mut().filter(|r| r.name == dep) {
if !r.features.iter().any(|f| f == feat) {
r.features.push(feat.to_string());
}
}
}
if let Some(p) = &m_toml.package {
m_dep_list.sort();
m_dep_list.dedup();
let ver = p.version.clone().unwrap_or_else(|| "0.1.0".to_string());
member_deps.insert(format!("{} {}", p.name, ver), m_dep_list);
}
}
} else {
bail!("Failed to parse manifest at {:?}", manifest_path);
}
let direct_remote_names: Vec<String> = remote_deps.iter().map(|d| d.name.clone()).collect();
remote_deps.append(&mut git_out.registry_deps);
let mut lock = self.resolve_dependencies(&remote_deps, offline)?;
lock.packages.append(&mut git_out.packages);
path_deps.sort();
path_deps.dedup();
let mut pending_specs: Vec<(usize, DepSpecs)> = Vec::new();
for (p_name, p_ver) in &path_deps {
let specs = member_deps
.remove(&format!("{p_name} {p_ver}"))
.unwrap_or_default();
pending_specs.push((lock.packages.len(), specs));
lock.packages.push(CargoLockPackage {
name: p_name.clone(),
version: p_ver.clone(),
source: None,
checksum: None,
dependencies: Vec::new(),
});
}
if let Some((root_name, root_version)) = extract_root_package(&manifest_content) {
if !path_deps
.iter()
.any(|(n, v)| n == &root_name && v == &root_version)
{
let root_specs: DepSpecs =
match member_deps.remove(&format!("{root_name} {root_version}")) {
Some(listed) => listed,
None => direct_remote_names
.iter()
.map(|n| (n.clone(), None))
.chain(path_deps.iter().map(|(n, v)| (n.clone(), Some(v.clone()))))
.collect(),
};
pending_specs.push((lock.packages.len(), root_specs));
lock.packages.push(CargoLockPackage {
name: root_name,
version: root_version,
source: None,
checksum: None,
dependencies: Vec::new(),
});
}
}
let mut name_counts: HashMap<&str, usize> = HashMap::new();
for pkg in &lock.packages {
*name_counts.entry(pkg.name.as_str()).or_default() += 1;
}
let qualified: Vec<(usize, Vec<String>)> = pending_specs
.into_iter()
.map(|(idx, specs)| {
let mut list: Vec<String> = specs
.into_iter()
.map(|(name, ver)| match ver {
Some(v) if name_counts.get(name.as_str()).copied().unwrap_or(0) > 1 => {
format!("{name} {v}")
}
_ => name,
})
.collect();
list.sort();
list.dedup();
(idx, list)
})
.collect();
for (idx, list) in qualified {
lock.packages[idx].dependencies = list;
}
lock.packages.sort_by(|a, b| a.name.cmp(&b.name));
Ok(lock)
}
pub fn resolve_dependencies(
&self,
root_deps: &[ManifestDirectDep],
offline: bool,
) -> Result<CargoLock> {
self.resolve_graph(root_deps, offline)
}
}
pub fn select_best_version<'a>(
entries: &'a [super::sparse_index::SparseIndexEntry],
req_str: &str,
) -> Result<&'a super::sparse_index::SparseIndexEntry> {
let clean_req = req_str.trim().trim_matches('"');
let req = VersionReq::parse(if clean_req.is_empty() || clean_req == "*" {
"*"
} else {
clean_req
})
.unwrap_or(VersionReq::STAR);
for entry in entries.iter().rev() {
if entry.yanked {
continue;
}
if let Ok(ver) = Version::parse(&entry.vers) {
if req.matches(&ver) {
return Ok(entry);
}
}
}
if let Some(latest) = entries.iter().rev().find(|e| !e.yanked) {
return Ok(latest);
}
bail!(
"No compatible non-yanked version found for requirement '{}'",
req_str
);
}