mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 19:08:04 +00:00
Only collect path dependency source indexes during lockfile validation
This commit is contained in:
parent
3ad4ba7935
commit
9e5c16d833
3 changed files with 95 additions and 133 deletions
|
@ -114,10 +114,6 @@ pub struct Workspace {
|
||||||
///
|
///
|
||||||
/// This table is overridden by the project indexes.
|
/// This table is overridden by the project indexes.
|
||||||
indexes: Vec<Index>,
|
indexes: Vec<Index>,
|
||||||
/// Indexes defined as sources in transitive path dependencies of this workspace.
|
|
||||||
///
|
|
||||||
/// Used for lockfile validation.
|
|
||||||
path_dependency_source_indexes: Vec<Index>,
|
|
||||||
/// The `pyproject.toml` of the workspace root.
|
/// The `pyproject.toml` of the workspace root.
|
||||||
pyproject_toml: PyProjectToml,
|
pyproject_toml: PyProjectToml,
|
||||||
}
|
}
|
||||||
|
@ -639,11 +635,6 @@ impl Workspace {
|
||||||
&self.indexes
|
&self.indexes
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Indexes defined as sources in transitive path dependencies of this workspace.
|
|
||||||
pub fn path_dependency_source_indexes(&self) -> &[Index] {
|
|
||||||
&self.path_dependency_source_indexes
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The `pyproject.toml` of the workspace.
|
/// The `pyproject.toml` of the workspace.
|
||||||
pub fn pyproject_toml(&self) -> &PyProjectToml {
|
pub fn pyproject_toml(&self) -> &PyProjectToml {
|
||||||
&self.pyproject_toml
|
&self.pyproject_toml
|
||||||
|
@ -751,18 +742,11 @@ impl Workspace {
|
||||||
.and_then(|uv| uv.index)
|
.and_then(|uv| uv.index)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let path_dependency_source_indexes = collect_path_dependency_source_indexes(
|
|
||||||
&workspace_root,
|
|
||||||
&workspace_indexes,
|
|
||||||
&workspace_pyproject_toml,
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(Workspace {
|
Ok(Workspace {
|
||||||
install_path: workspace_root,
|
install_path: workspace_root,
|
||||||
packages: workspace_members,
|
packages: workspace_members,
|
||||||
sources: workspace_sources,
|
sources: workspace_sources,
|
||||||
indexes: workspace_indexes,
|
indexes: workspace_indexes,
|
||||||
path_dependency_source_indexes,
|
|
||||||
pyproject_toml: workspace_pyproject_toml,
|
pyproject_toml: workspace_pyproject_toml,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -945,6 +929,85 @@ impl Workspace {
|
||||||
}
|
}
|
||||||
Ok(workspace_members)
|
Ok(workspace_members)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collects indexes provided as sources in (transitive) path dependencies that
|
||||||
|
/// have not already been defined in the workspace.
|
||||||
|
pub fn collect_path_dependency_source_indexes(&self) -> Vec<Index> {
|
||||||
|
let mut dependency_indexes = FxHashSet::default();
|
||||||
|
let mut seen = FxHashSet::default();
|
||||||
|
|
||||||
|
// We will only add indexes if we have not already seen the URLs.
|
||||||
|
let known_urls: FxHashSet<_> = self.indexes.iter().map(Index::url).collect();
|
||||||
|
|
||||||
|
let mut pyprojects = std::collections::VecDeque::new();
|
||||||
|
pyprojects.push_back((self.install_path.clone(), self.pyproject_toml.clone()));
|
||||||
|
|
||||||
|
while let Some((base_path, pyproject)) = pyprojects.pop_front() {
|
||||||
|
if let Some(tool_uv_sources) = pyproject
|
||||||
|
.tool
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|tool| tool.uv.as_ref())
|
||||||
|
.and_then(|uv| uv.sources.as_ref())
|
||||||
|
{
|
||||||
|
for sources in tool_uv_sources.inner().values() {
|
||||||
|
for source in sources.iter() {
|
||||||
|
if let Source::Path { path, .. } = source {
|
||||||
|
let dep_path = if path.as_ref().is_absolute() {
|
||||||
|
path.as_ref().to_path_buf()
|
||||||
|
} else {
|
||||||
|
base_path.join(path)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Canonicalize path to compare symlinks and relative paths correctly
|
||||||
|
let Ok(canonical_path) = dep_path.canonicalize() else {
|
||||||
|
debug!(
|
||||||
|
"Failed to canonicalize path dependency path: {}",
|
||||||
|
dep_path.display()
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prevent infinite loops from circular dependencies
|
||||||
|
if !seen.insert(canonical_path.clone()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dep_pyproject_path = canonical_path.join("pyproject.toml");
|
||||||
|
|
||||||
|
match pyproject_toml_from_path(dep_pyproject_path.clone()) {
|
||||||
|
Ok(dep_pyproject) => {
|
||||||
|
if let Some(dep_indexes) = dep_pyproject
|
||||||
|
.tool
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|tool| tool.uv.as_ref())
|
||||||
|
.and_then(|uv| uv.index.as_ref())
|
||||||
|
{
|
||||||
|
dependency_indexes.extend(
|
||||||
|
dep_indexes
|
||||||
|
.iter()
|
||||||
|
.filter(|idx| !known_urls.contains(idx.url()))
|
||||||
|
.cloned(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pyprojects.push_back((canonical_path, dep_pyproject));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
debug!(
|
||||||
|
"Failed to read `pyproject.toml` in path dependency `{}`: {}",
|
||||||
|
dep_pyproject_path.display(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependency_indexes.into_iter().collect::<Vec<_>>()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A project in a workspace.
|
/// A project in a workspace.
|
||||||
|
@ -1252,7 +1315,6 @@ impl ProjectWorkspace {
|
||||||
// workspace sources.
|
// workspace sources.
|
||||||
sources: BTreeMap::default(),
|
sources: BTreeMap::default(),
|
||||||
indexes: Vec::default(),
|
indexes: Vec::default(),
|
||||||
path_dependency_source_indexes: Vec::default(),
|
|
||||||
pyproject_toml: project_pyproject_toml.clone(),
|
pyproject_toml: project_pyproject_toml.clone(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1579,92 +1641,6 @@ fn pyproject_toml_from_path(pyproject_path: PathBuf) -> Result<PyProjectToml, Wo
|
||||||
.map_err(|err| WorkspaceError::Toml(pyproject_path, Box::new(err)))
|
.map_err(|err| WorkspaceError::Toml(pyproject_path, Box::new(err)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Collects indexes provided as sources in (transitive) path dependencies that
|
|
||||||
/// have not already been defined in the workspace.
|
|
||||||
fn collect_path_dependency_source_indexes(
|
|
||||||
workspace_root: &Path,
|
|
||||||
workspace_indexes: &[Index],
|
|
||||||
workspace_pyproject_toml: &PyProjectToml,
|
|
||||||
) -> Vec<Index> {
|
|
||||||
let mut dependency_indexes = FxHashSet::default();
|
|
||||||
let mut seen = FxHashSet::default();
|
|
||||||
|
|
||||||
// We will only add indexes if we have not already seen the URLs.
|
|
||||||
let known_urls: FxHashSet<_> = workspace_indexes.iter().map(Index::url).collect();
|
|
||||||
|
|
||||||
let mut pyprojects = std::collections::VecDeque::new();
|
|
||||||
pyprojects.push_back((
|
|
||||||
workspace_root.to_path_buf(),
|
|
||||||
workspace_pyproject_toml.clone(),
|
|
||||||
));
|
|
||||||
|
|
||||||
while let Some((base_path, pyproject)) = pyprojects.pop_front() {
|
|
||||||
if let Some(tool_uv_sources) = pyproject
|
|
||||||
.tool
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|tool| tool.uv.as_ref())
|
|
||||||
.and_then(|uv| uv.sources.as_ref())
|
|
||||||
{
|
|
||||||
for sources in tool_uv_sources.inner().values() {
|
|
||||||
for source in sources.iter() {
|
|
||||||
if let Source::Path { path, .. } = source {
|
|
||||||
let dep_path = if path.as_ref().is_absolute() {
|
|
||||||
path.as_ref().to_path_buf()
|
|
||||||
} else {
|
|
||||||
base_path.join(path)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Canonicalize path to compare symlinks and relative paths correctly
|
|
||||||
let Ok(canonical_path) = dep_path.canonicalize() else {
|
|
||||||
debug!(
|
|
||||||
"Failed to canonicalize path dependency path: {}",
|
|
||||||
dep_path.display()
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Prevent infinite loops from circular dependencies
|
|
||||||
if !seen.insert(canonical_path.clone()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let dep_pyproject_path = canonical_path.join("pyproject.toml");
|
|
||||||
|
|
||||||
match pyproject_toml_from_path(dep_pyproject_path.clone()) {
|
|
||||||
Ok(dep_pyproject) => {
|
|
||||||
if let Some(dep_indexes) = dep_pyproject
|
|
||||||
.tool
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|tool| tool.uv.as_ref())
|
|
||||||
.and_then(|uv| uv.index.as_ref())
|
|
||||||
{
|
|
||||||
dependency_indexes.extend(
|
|
||||||
dep_indexes
|
|
||||||
.iter()
|
|
||||||
.filter(|idx| !known_urls.contains(idx.url()))
|
|
||||||
.cloned(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pyprojects.push_back((canonical_path, dep_pyproject));
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
debug!(
|
|
||||||
"Failed to read `pyproject.toml` in path dependency `{}`: {}",
|
|
||||||
dep_pyproject_path.display(),
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependency_indexes.into_iter().collect::<Vec<_>>()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[cfg(unix)] // Avoid path escaping for the unit tests
|
#[cfg(unix)] // Avoid path escaping for the unit tests
|
||||||
mod tests {
|
mod tests {
|
||||||
|
@ -1753,7 +1729,6 @@ mod tests {
|
||||||
},
|
},
|
||||||
"sources": {},
|
"sources": {},
|
||||||
"indexes": [],
|
"indexes": [],
|
||||||
"path_dependency_source_indexes": [],
|
|
||||||
"pyproject_toml": {
|
"pyproject_toml": {
|
||||||
"project": {
|
"project": {
|
||||||
"name": "bird-feeder",
|
"name": "bird-feeder",
|
||||||
|
@ -1807,7 +1782,6 @@ mod tests {
|
||||||
},
|
},
|
||||||
"sources": {},
|
"sources": {},
|
||||||
"indexes": [],
|
"indexes": [],
|
||||||
"path_dependency_source_indexes": [],
|
|
||||||
"pyproject_toml": {
|
"pyproject_toml": {
|
||||||
"project": {
|
"project": {
|
||||||
"name": "bird-feeder",
|
"name": "bird-feeder",
|
||||||
|
@ -1896,7 +1870,6 @@ mod tests {
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"indexes": [],
|
"indexes": [],
|
||||||
"path_dependency_source_indexes": [],
|
|
||||||
"pyproject_toml": {
|
"pyproject_toml": {
|
||||||
"project": {
|
"project": {
|
||||||
"name": "albatross",
|
"name": "albatross",
|
||||||
|
@ -2009,7 +1982,6 @@ mod tests {
|
||||||
},
|
},
|
||||||
"sources": {},
|
"sources": {},
|
||||||
"indexes": [],
|
"indexes": [],
|
||||||
"path_dependency_source_indexes": [],
|
|
||||||
"pyproject_toml": {
|
"pyproject_toml": {
|
||||||
"project": null,
|
"project": null,
|
||||||
"tool": {
|
"tool": {
|
||||||
|
@ -2076,7 +2048,6 @@ mod tests {
|
||||||
},
|
},
|
||||||
"sources": {},
|
"sources": {},
|
||||||
"indexes": [],
|
"indexes": [],
|
||||||
"path_dependency_source_indexes": [],
|
|
||||||
"pyproject_toml": {
|
"pyproject_toml": {
|
||||||
"project": {
|
"project": {
|
||||||
"name": "albatross",
|
"name": "albatross",
|
||||||
|
@ -2211,7 +2182,6 @@ mod tests {
|
||||||
},
|
},
|
||||||
"sources": {},
|
"sources": {},
|
||||||
"indexes": [],
|
"indexes": [],
|
||||||
"path_dependency_source_indexes": [],
|
|
||||||
"pyproject_toml": {
|
"pyproject_toml": {
|
||||||
"project": {
|
"project": {
|
||||||
"name": "albatross",
|
"name": "albatross",
|
||||||
|
@ -2318,7 +2288,6 @@ mod tests {
|
||||||
},
|
},
|
||||||
"sources": {},
|
"sources": {},
|
||||||
"indexes": [],
|
"indexes": [],
|
||||||
"path_dependency_source_indexes": [],
|
|
||||||
"pyproject_toml": {
|
"pyproject_toml": {
|
||||||
"project": {
|
"project": {
|
||||||
"name": "albatross",
|
"name": "albatross",
|
||||||
|
@ -2439,7 +2408,6 @@ mod tests {
|
||||||
},
|
},
|
||||||
"sources": {},
|
"sources": {},
|
||||||
"indexes": [],
|
"indexes": [],
|
||||||
"path_dependency_source_indexes": [],
|
|
||||||
"pyproject_toml": {
|
"pyproject_toml": {
|
||||||
"project": {
|
"project": {
|
||||||
"name": "albatross",
|
"name": "albatross",
|
||||||
|
@ -2534,7 +2502,6 @@ mod tests {
|
||||||
},
|
},
|
||||||
"sources": {},
|
"sources": {},
|
||||||
"indexes": [],
|
"indexes": [],
|
||||||
"path_dependency_source_indexes": [],
|
|
||||||
"pyproject_toml": {
|
"pyproject_toml": {
|
||||||
"project": {
|
"project": {
|
||||||
"name": "albatross",
|
"name": "albatross",
|
||||||
|
|
|
@ -680,7 +680,7 @@ async fn do_lock(
|
||||||
let existing_lock = if let Some(existing_lock) = existing_lock {
|
let existing_lock = if let Some(existing_lock) = existing_lock {
|
||||||
match ValidatedLock::validate(
|
match ValidatedLock::validate(
|
||||||
existing_lock,
|
existing_lock,
|
||||||
target.install_path(),
|
target,
|
||||||
packages,
|
packages,
|
||||||
&members,
|
&members,
|
||||||
&requirements,
|
&requirements,
|
||||||
|
@ -695,7 +695,6 @@ async fn do_lock(
|
||||||
interpreter,
|
interpreter,
|
||||||
&requires_python,
|
&requires_python,
|
||||||
index_locations,
|
index_locations,
|
||||||
target.path_dependency_source_indexes(),
|
|
||||||
upgrade,
|
upgrade,
|
||||||
&options,
|
&options,
|
||||||
&hasher,
|
&hasher,
|
||||||
|
@ -894,7 +893,7 @@ impl ValidatedLock {
|
||||||
/// Validate a [`Lock`] against the workspace requirements.
|
/// Validate a [`Lock`] against the workspace requirements.
|
||||||
async fn validate<Context: BuildContext>(
|
async fn validate<Context: BuildContext>(
|
||||||
lock: Lock,
|
lock: Lock,
|
||||||
install_path: &Path,
|
target: LockTarget<'_>,
|
||||||
packages: &BTreeMap<PackageName, WorkspaceMember>,
|
packages: &BTreeMap<PackageName, WorkspaceMember>,
|
||||||
members: &[PackageName],
|
members: &[PackageName],
|
||||||
requirements: &[Requirement],
|
requirements: &[Requirement],
|
||||||
|
@ -909,7 +908,6 @@ impl ValidatedLock {
|
||||||
interpreter: &Interpreter,
|
interpreter: &Interpreter,
|
||||||
requires_python: &RequiresPython,
|
requires_python: &RequiresPython,
|
||||||
index_locations: &IndexLocations,
|
index_locations: &IndexLocations,
|
||||||
path_dependency_source_indexes: Option<&[Index]>,
|
|
||||||
upgrade: &Upgrade,
|
upgrade: &Upgrade,
|
||||||
options: &Options,
|
options: &Options,
|
||||||
hasher: &HashStrategy,
|
hasher: &HashStrategy,
|
||||||
|
@ -1074,22 +1072,27 @@ impl ValidatedLock {
|
||||||
} else {
|
} else {
|
||||||
// If indexes were defined as sources in path dependencies, add them to the
|
// If indexes were defined as sources in path dependencies, add them to the
|
||||||
// index locations to use for validation.
|
// index locations to use for validation.
|
||||||
path_dependency_source_indexes
|
if let LockTarget::Workspace(workspace) = target {
|
||||||
.filter(|source_indexes| !source_indexes.is_empty())
|
let path_dependency_source_indexes =
|
||||||
.map(|source_indexes| {
|
workspace.collect_path_dependency_source_indexes();
|
||||||
Cow::Owned(index_locations.clone().combine(
|
if path_dependency_source_indexes.is_empty() {
|
||||||
source_indexes.to_vec(),
|
Some(Cow::Borrowed(index_locations))
|
||||||
|
} else {
|
||||||
|
Some(Cow::Owned(index_locations.clone().combine(
|
||||||
|
path_dependency_source_indexes,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
false,
|
false,
|
||||||
))
|
)))
|
||||||
})
|
}
|
||||||
.or(Some(Cow::Borrowed(index_locations)))
|
} else {
|
||||||
|
Some(Cow::Borrowed(index_locations))
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Determine whether the lockfile satisfies the workspace requirements.
|
// Determine whether the lockfile satisfies the workspace requirements.
|
||||||
match lock
|
match lock
|
||||||
.satisfies(
|
.satisfies(
|
||||||
install_path,
|
target.install_path(),
|
||||||
packages,
|
packages,
|
||||||
members,
|
members,
|
||||||
requirements,
|
requirements,
|
||||||
|
|
|
@ -215,14 +215,6 @@ impl<'lock> LockTarget<'lock> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If this is a workspace, returns path dependency source indexes, otherwise return [`None`].
|
|
||||||
pub(crate) fn path_dependency_source_indexes(&self) -> Option<&[Index]> {
|
|
||||||
match self {
|
|
||||||
Self::Workspace(workspace) => Some(workspace.path_dependency_source_indexes()),
|
|
||||||
Self::Script(..) => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the `Requires-Python` bound for the [`LockTarget`].
|
/// Return the `Requires-Python` bound for the [`LockTarget`].
|
||||||
#[allow(clippy::result_large_err)]
|
#[allow(clippy::result_large_err)]
|
||||||
pub(crate) fn requires_python(self) -> Result<Option<RequiresPython>, ProjectError> {
|
pub(crate) fn requires_python(self) -> Result<Option<RequiresPython>, ProjectError> {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue