use either::Either;
use rustc_hash::FxHashMap;
use same_file::is_same_file;
use tracing::debug;
use uv_cache_key::CanonicalUrl;
use uv_git::GitResolver;
use uv_normalize::PackageName;
use uv_pep508::VerbatimUrl;
use uv_pypi_types::{ParsedDirectoryUrl, ParsedUrl, VerbatimParsedUrl};
use crate::resolver::ForkMap;
use crate::{DependencyMode, Manifest, ResolveError, ResolverEnvironment};
#[derive(Debug, Default)]
pub(crate) struct Urls {
overrides: ForkMap<VerbatimParsedUrl>,
regular: FxHashMap<PackageName, Vec<VerbatimParsedUrl>>,
}
impl Urls {
pub(crate) fn from_manifest(
manifest: &Manifest,
env: &ResolverEnvironment,
git: &GitResolver,
dependencies: DependencyMode,
) -> Self {
let mut regular: FxHashMap<PackageName, Vec<VerbatimParsedUrl>> = FxHashMap::default();
let mut overrides = ForkMap::default();
for requirement in manifest.requirements_no_overrides(env, dependencies) {
let Some(url) = requirement.source.to_verbatim_parsed_url() else {
continue;
};
let package_urls = regular.entry(requirement.name.clone()).or_default();
if let Some(package_url) = package_urls
.iter_mut()
.find(|package_url| same_resource(&package_url.parsed_url, &url.parsed_url, git))
{
let previous_editable = package_url.is_editable();
*package_url = url;
if previous_editable {
if let VerbatimParsedUrl {
parsed_url: ParsedUrl::Directory(ParsedDirectoryUrl { editable, .. }),
verbatim: _,
} = package_url
{
if editable.is_none() {
debug!("Allowing an editable variant of {}", &package_url.verbatim);
*editable = Some(true);
}
}
}
} else {
package_urls.push(url);
}
}
for requirement in manifest.overrides(env, dependencies) {
let Some(url) = requirement.source.to_verbatim_parsed_url() else {
continue;
};
regular.remove(&requirement.name);
overrides.add(requirement.as_ref(), url);
}
Self { overrides, regular }
}
pub(crate) fn get_url<'a>(
&'a self,
env: &'a ResolverEnvironment,
name: &'a PackageName,
url: Option<&'a VerbatimParsedUrl>,
git: &'a GitResolver,
) -> Result<impl Iterator<Item = &'a VerbatimParsedUrl>, ResolveError> {
if self.overrides.contains_key(name) {
Ok(Either::Left(Either::Left(
self.overrides.get(name, env).into_iter(),
)))
} else if let Some(url) = url {
let url =
self.canonicalize_allowed_url(env, name, git, &url.verbatim, &url.parsed_url)?;
Ok(Either::Left(Either::Right(std::iter::once(url))))
} else {
Ok(Either::Right(std::iter::empty()))
}
}
pub(crate) fn any_url(&self, name: &PackageName) -> bool {
self.overrides.contains_key(name) || self.get_regular(name).is_some()
}
fn get_regular(&self, package: &PackageName) -> Option<&[VerbatimParsedUrl]> {
self.regular.get(package).map(Vec::as_slice)
}
fn canonicalize_allowed_url<'a>(
&'a self,
env: &ResolverEnvironment,
package_name: &'a PackageName,
git: &'a GitResolver,
verbatim_url: &'a VerbatimUrl,
parsed_url: &'a ParsedUrl,
) -> Result<&'a VerbatimParsedUrl, ResolveError> {
let Some(expected) = self.get_regular(package_name) else {
return Err(ResolveError::DisallowedUrl {
name: package_name.clone(),
url: verbatim_url.to_string(),
});
};
let matching_urls: Vec<_> = expected
.iter()
.filter(|requirement| same_resource(&requirement.parsed_url, parsed_url, git))
.collect();
let [allowed_url] = matching_urls.as_slice() else {
let mut conflicting_urls: Vec<_> = matching_urls
.into_iter()
.map(|parsed_url| parsed_url.parsed_url.clone())
.chain(std::iter::once(parsed_url.clone()))
.collect();
conflicting_urls.sort();
return Err(ResolveError::ConflictingUrls {
package_name: package_name.clone(),
urls: conflicting_urls,
env: env.clone(),
});
};
Ok(*allowed_url)
}
}
fn same_resource(a: &ParsedUrl, b: &ParsedUrl, git: &GitResolver) -> bool {
match (a, b) {
(ParsedUrl::Archive(a), ParsedUrl::Archive(b)) => {
a.subdirectory.as_deref().map(uv_fs::normalize_path)
== b.subdirectory.as_deref().map(uv_fs::normalize_path)
&& CanonicalUrl::new(&a.url) == CanonicalUrl::new(&b.url)
}
(ParsedUrl::Git(a), ParsedUrl::Git(b)) => {
a.subdirectory.as_deref().map(uv_fs::normalize_path)
== b.subdirectory.as_deref().map(uv_fs::normalize_path)
&& git.same_ref(&a.url, &b.url)
}
(ParsedUrl::Path(a), ParsedUrl::Path(b)) => {
a.install_path == b.install_path
|| is_same_file(&a.install_path, &b.install_path).unwrap_or(false)
}
(ParsedUrl::Directory(a), ParsedUrl::Directory(b)) => {
(a.install_path == b.install_path
|| is_same_file(&a.install_path, &b.install_path).unwrap_or(false))
&& a.editable.is_none_or(|a| b.editable.is_none_or(|b| a == b))
}
_ => false,
}
}