Only use relative paths in lockfile (#6490)

For users who were using absolute paths in the `pyproject.toml`
previously, this is a behavior change: We now convert all absolute paths
in `path` entries to relative paths. Since i assume that no-one relies
on absolute path in their lockfiles - they are intended to be portable -
I'm tagging this as a bugfix.

Closes https://github.com/astral-sh/uv/pull/6438
Fixes https://github.com/astral-sh/uv/issues/6371
This commit is contained in:
Charlie Marsh 2024-08-23 22:19:10 -04:00 committed by GitHub
parent 611a9003c9
commit f7835243c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 329 additions and 383 deletions

View file

@ -332,13 +332,13 @@ impl Source {
let source = match source {
RequirementSource::Registry { .. } => return Ok(None),
RequirementSource::Path { lock_path, .. } => Source::Path {
RequirementSource::Path { install_path, .. } => Source::Path {
editable,
path: lock_path.to_string_lossy().into_owned(),
path: install_path.to_string_lossy().into_owned(),
},
RequirementSource::Directory { lock_path, .. } => Source::Path {
RequirementSource::Directory { install_path, .. } => Source::Path {
editable,
path: lock_path.to_string_lossy().into_owned(),
path: install_path.to_string_lossy().into_owned(),
},
RequirementSource::Url {
subdirectory, url, ..

View file

@ -10,7 +10,7 @@ use tracing::{debug, trace, warn};
use pep508_rs::{MarkerTree, RequirementOrigin, VerbatimUrl};
use pypi_types::{Requirement, RequirementSource};
use uv_fs::{absolutize_path, normalize_path, relative_to, Simplified};
use uv_fs::{absolutize_path, Simplified};
use uv_normalize::{GroupName, PackageName, DEV_DEPENDENCIES};
use uv_warnings::warn_user;
@ -62,11 +62,6 @@ pub struct Workspace {
/// The workspace root is the directory containing the top level `pyproject.toml` with
/// the `uv.tool.workspace`, or the `pyproject.toml` in an implicit single workspace project.
install_path: PathBuf,
/// The same path as `install_path`, but relative to the main workspace.
///
/// We use this value to compute relative paths for workspace-to-workspace dependencies. It's an
/// empty path for the main workspace.
lock_path: PathBuf,
/// The members of the workspace.
packages: BTreeMap<PackageName, WorkspaceMember>,
/// The sources table from the workspace `pyproject.toml`.
@ -176,8 +171,6 @@ impl Workspace {
Self::collect_members(
workspace_root.clone(),
// This method supports only absolute paths.
workspace_root,
workspace_definition,
workspace_pyproject_toml,
current_project,
@ -286,11 +279,6 @@ impl Workspace {
marker: MarkerTree::TRUE,
source: RequirementSource::Directory {
install_path: member.root.clone(),
lock_path: member
.root
.strip_prefix(&self.install_path)
.expect("Project must be below workspace root")
.to_path_buf(),
editable: true,
url,
},
@ -421,12 +409,6 @@ impl Workspace {
&self.install_path
}
/// The same path as `install_path()`, but relative to the main workspace. We use this value
/// to compute relative paths for workspace-to-workspace dependencies.
pub fn lock_path(&self) -> &PathBuf {
&self.lock_path
}
/// The path to the workspace virtual environment.
pub fn venv(&self) -> PathBuf {
self.install_path.join(".venv")
@ -480,7 +462,6 @@ impl Workspace {
/// Collect the workspace member projects from the `members` and `excludes` entries.
async fn collect_members(
workspace_root: PathBuf,
lock_path: PathBuf,
workspace_definition: ToolUvWorkspace,
workspace_pyproject_toml: PyProjectToml,
current_project: Option<WorkspaceMember>,
@ -644,7 +625,6 @@ impl Workspace {
Ok(Workspace {
install_path: workspace_root,
lock_path,
packages: workspace_members,
sources: workspace_sources,
pyproject_toml: workspace_pyproject_toml,
@ -817,21 +797,13 @@ impl ProjectWorkspace {
.clone()
.ok_or_else(|| WorkspaceError::MissingProject(pyproject_path.clone()))?;
Self::from_project(
project_root,
Path::new(""),
&project,
&pyproject_toml,
options,
)
.await
Self::from_project(project_root, &project, &pyproject_toml, options).await
}
/// If the current directory contains a `pyproject.toml` with a `project` table, discover the
/// workspace and return it, otherwise it is a dynamic path dependency and we return `Ok(None)`.
pub async fn from_maybe_project_root(
install_path: &Path,
lock_path: &Path,
options: &DiscoveryOptions<'_>,
) -> Result<Option<Self>, WorkspaceError> {
// Read the `pyproject.toml`.
@ -849,8 +821,7 @@ impl ProjectWorkspace {
return Ok(None);
};
match Self::from_project(install_path, lock_path, &project, &pyproject_toml, options).await
{
match Self::from_project(install_path, &project, &pyproject_toml, options).await {
Ok(workspace) => Ok(Some(workspace)),
Err(WorkspaceError::NonWorkspace(_)) => Ok(None),
Err(err) => Err(err),
@ -894,7 +865,6 @@ impl ProjectWorkspace {
/// Find the workspace for a project.
pub async fn from_project(
install_path: &Path,
lock_path: &Path,
project: &Project,
project_pyproject_toml: &PyProjectToml,
options: &DiscoveryOptions<'_>,
@ -954,8 +924,6 @@ impl ProjectWorkspace {
project_name: project.name.clone(),
workspace: Workspace {
install_path: project_path.clone(),
// The workspace and the project are the same, so the relative path is, too.
lock_path: lock_path.to_path_buf(),
packages: current_project_as_members,
// There may be package sources, but we don't need to duplicate them into the
// workspace sources.
@ -970,28 +938,8 @@ impl ProjectWorkspace {
workspace_root.simplified_display()
);
// Say we have:
// ```
// root
// ├── main_workspace <- The reference point
// │ ├── pyproject.toml
// │ └── uv.lock
// └──current_workspace <- We want this relative to the main workspace
// └── packages
// └── current_package <- We have this relative to the main workspace
// └── pyproject.toml
// ```
// The lock path we need: `../current_workspace`
// workspace root: `/root/current_workspace`
// project path: `/root/current_workspace/packages/current_project`
// relative to workspace: `../..`
// lock path: `../current_workspace`
let up_to_root = relative_to(&workspace_root, &project_path)?;
let lock_path = normalize_path(&lock_path.join(up_to_root));
let workspace = Workspace::collect_members(
workspace_root,
lock_path,
workspace_definition,
workspace_pyproject_toml,
Some(current_project),
@ -1270,14 +1218,9 @@ impl VirtualProject {
if let Some(project) = pyproject_toml.project.as_ref() {
// If the `pyproject.toml` contains a `[project]` table, it's a project.
let project = ProjectWorkspace::from_project(
project_root,
Path::new(""),
project,
&pyproject_toml,
options,
)
.await?;
let project =
ProjectWorkspace::from_project(project_root, project, &pyproject_toml, options)
.await?;
Ok(Self::Project(project))
} else if let Some(workspace) = pyproject_toml
.tool
@ -1294,7 +1237,6 @@ impl VirtualProject {
let workspace = Workspace::collect_members(
project_path,
PathBuf::new(),
workspace.clone(),
pyproject_toml,
None,
@ -1452,7 +1394,6 @@ mod tests {
"project_name": "bird-feeder",
"workspace": {
"install_path": "[ROOT]/albatross-in-example/examples/bird-feeder",
"lock_path": "",
"packages": {
"bird-feeder": {
"root": "[ROOT]/albatross-in-example/examples/bird-feeder",
@ -1496,7 +1437,6 @@ mod tests {
"project_name": "bird-feeder",
"workspace": {
"install_path": "[ROOT]/albatross-project-in-excluded/excluded/bird-feeder",
"lock_path": "",
"packages": {
"bird-feeder": {
"root": "[ROOT]/albatross-project-in-excluded/excluded/bird-feeder",
@ -1539,7 +1479,6 @@ mod tests {
"project_name": "albatross",
"workspace": {
"install_path": "[ROOT]/albatross-root-workspace",
"lock_path": "",
"packages": {
"albatross": {
"root": "[ROOT]/albatross-root-workspace",
@ -1624,7 +1563,6 @@ mod tests {
"project_name": "albatross",
"workspace": {
"install_path": "[ROOT]/albatross-virtual-workspace",
"lock_path": "../..",
"packages": {
"albatross": {
"root": "[ROOT]/albatross-virtual-workspace/packages/albatross",
@ -1696,7 +1634,6 @@ mod tests {
"project_name": "albatross",
"workspace": {
"install_path": "[ROOT]/albatross-just-project",
"lock_path": "",
"packages": {
"albatross": {
"root": "[ROOT]/albatross-just-project",