1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
use either::Either;
use rustc_hash::FxHashMap;
use same_file::is_same_file;
use tracing::debug;
use uv_cache_key::CanonicalUrl;
use uv_distribution_types::RequirementSource;
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};
/// The URLs that are allowed for packages.
///
/// These are the URLs used in the root package or by other URL dependencies (including path
/// dependencies). They take precedence over requirements by version (except for the special case
/// where we are in a fork that doesn't use any of the URL(s) used in other forks). Each fork may
/// only use a single URL.
///
/// This type contains all URLs without checking, the validation happens in
/// [`crate::fork_urls::ForkUrls`].
#[derive(Debug, Default)]
pub(crate) struct Urls {
/// URL requirements in overrides. An override URL replaces all requirements and constraints
/// URLs. There can be multiple URLs for the same package as long as they are in different
/// forks.
overrides: ForkMap<VerbatimParsedUrl>,
/// URLs from regular requirements or from constraints. There can be multiple URLs for the same
/// package as long as they are in different forks.
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();
// Merge requirement and constraint URLs in their original order. Then replay requirements
// which aren't forced-relative (user-provided) to allow the URL spelling to take precedence.
// Partitioning (instead of appending) would impact unrelated merging semantics.
for (requirement, force_relative) in manifest
.requirements_no_overrides(env, dependencies)
.map(|requirement| (requirement, true))
.chain(
manifest
.requirements_no_overrides(env, dependencies)
.filter(|requirement| {
matches!(
&requirement.source,
RequirementSource::Path { url, .. }
| RequirementSource::Directory { url, .. }
if !url.force_relative()
)
})
.map(|requirement| (requirement, false)),
)
{
let Some(mut url) = requirement.source.to_verbatim_parsed_url() else {
// Registry requirement
continue;
};
if force_relative
&& matches!(
&url.parsed_url,
ParsedUrl::Path(_) | ParsedUrl::Directory(_)
)
{
// Force relative paths for the initial merge so replayed user-provided requirements
// determine the final path preference.
url.verbatim = url.verbatim.with_force_relative(true);
}
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))
{
// Allow editables to override non-editables.
let previous_editable = package_url.is_editable();
// The last specified URL spelling wins.
*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);
}
}
// Add all URLs from overrides. If there is an override URL, all other URLs from
// requirements and constraints are moot and will be removed.
for requirement in manifest.overrides(env, dependencies) {
let Some(url) = requirement.source.to_verbatim_parsed_url() else {
// Registry requirement
continue;
};
// We only clear for non-URL overrides, since e.g. with an override `anyio==0.0.0` and
// a requirements.txt entry `./anyio`, we still use the URL. See
// `allow_recursive_url_local_path_override_constraint`.
regular.remove(&requirement.name);
overrides.add(requirement.as_ref(), url);
}
Self { overrides, regular }
}
/// Return an iterator over the allowed URLs for the given package.
///
/// If we have a URL override, apply it unconditionally for registry and URL requirements.
/// Otherwise, there are two case: for a URL requirement (`url` isn't `None`), check that the
/// URL is allowed and return its canonical form.
///
/// For registry requirements, we return an empty iterator.
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()))
}
}
/// Return `true` if the package has any URL (from overrides or regular requirements).
pub(crate) fn any_url(&self, name: &PackageName) -> bool {
self.overrides.contains_key(name) || self.get_regular(name).is_some()
}
/// Return the allowed [`VerbatimUrl`]s for given package from regular requirements and
/// constraints (but not overrides), if any.
///
/// It's more than one more URL if they are in different forks (or conflict after forking).
fn get_regular(&self, package: &PackageName) -> Option<&[VerbatimParsedUrl]> {
self.regular.get(package).map(Vec::as_slice)
}
/// Check if a URL is allowed (known), and if so, return its canonical form.
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)
}
}
/// Returns `true` if the [`ParsedUrl`] instances point to the same resource.
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.clone()) == CanonicalUrl::new(b.url.clone())
}
(ParsedUrl::GitDirectory(a), ParsedUrl::GitDirectory(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::GitPath(a), ParsedUrl::GitPath(b)) => {
uv_fs::normalize_path(&a.install_path) == uv_fs::normalize_path(&b.install_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,
}
}