diff --git a/Cargo.lock b/Cargo.lock index dfa6e6fff8a6..a2f188be3762 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4828,7 +4828,6 @@ dependencies = [ "insta", "install-wheel-rs", "nanoid", - "path-absolutize", "pep440_rs", "pep508_rs", "platform-tags", diff --git a/crates/distribution-types/src/buildable.rs b/crates/distribution-types/src/buildable.rs index 61a37407e922..01c26bcaef50 100644 --- a/crates/distribution-types/src/buildable.rs +++ b/crates/distribution-types/src/buildable.rs @@ -172,7 +172,6 @@ impl<'a> From<&'a PathSourceDist> for PathSourceUrl<'a> { pub struct DirectorySourceUrl<'a> { pub url: &'a Url, pub install_path: Cow<'a, Path>, - pub lock_path: Cow<'a, Path>, pub editable: bool, } @@ -187,7 +186,6 @@ impl<'a> From<&'a DirectorySourceDist> for DirectorySourceUrl<'a> { Self { url: &dist.url, install_path: Cow::Borrowed(&dist.install_path), - lock_path: Cow::Borrowed(&dist.lock_path), editable: dist.editable, } } diff --git a/crates/distribution-types/src/cached.rs b/crates/distribution-types/src/cached.rs index 8aa99107a5f4..dc944f9487fc 100644 --- a/crates/distribution-types/src/cached.rs +++ b/crates/distribution-types/src/cached.rs @@ -122,8 +122,7 @@ impl CachedDist { .map_err(|()| anyhow!("Invalid path in file URL"))?; Ok(Some(ParsedUrl::Directory(ParsedDirectoryUrl { url: dist.url.raw().clone(), - install_path: path.clone(), - lock_path: path, + install_path: path, editable: dist.editable, }))) } else { diff --git a/crates/distribution-types/src/lib.rs b/crates/distribution-types/src/lib.rs index 471d49b7422c..1ecf10aac2d7 100644 --- a/crates/distribution-types/src/lib.rs +++ b/crates/distribution-types/src/lib.rs @@ -33,6 +33,7 @@ //! //! Since we read this information from [`direct_url.json`](https://packaging.python.org/en/latest/specifications/direct-url-data-structure/), it doesn't match the information [`Dist`] exactly. use std::borrow::Cow; +use std::path; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -237,10 +238,6 @@ pub struct PathBuiltDist { pub filename: WheelFilename, /// The absolute path to the wheel which we use for installing. pub install_path: PathBuf, - /// The absolute path or path relative to the workspace root pointing to the wheel - /// which we use for locking. Unlike `given` on the verbatim URL all environment variables - /// are resolved, and unlike the install path, we did not yet join it on the base directory. - pub lock_path: PathBuf, /// The URL as it was provided by the user. pub url: VerbatimUrl, } @@ -298,10 +295,6 @@ pub struct PathSourceDist { pub name: PackageName, /// The absolute path to the distribution which we use for installing. pub install_path: PathBuf, - /// The absolute path or path relative to the workspace root pointing to the distribution - /// which we use for locking. Unlike `given` on the verbatim URL all environment variables - /// are resolved, and unlike the install path, we did not yet join it on the base directory. - pub lock_path: PathBuf, /// The file extension, e.g. `tar.gz`, `zip`, etc. pub ext: SourceDistExtension, /// The URL as it was provided by the user. @@ -314,10 +307,6 @@ pub struct DirectorySourceDist { pub name: PackageName, /// The absolute path to the distribution which we use for installing. pub install_path: PathBuf, - /// The absolute path or path relative to the workspace root pointing to the distribution - /// which we use for locking. Unlike `given` on the verbatim URL all environment variables - /// are resolved, and unlike the install path, we did not yet join it on the base directory. - pub lock_path: PathBuf, /// Whether the package should be installed in editable mode. pub editable: bool, /// The URL as it was provided by the user. @@ -369,11 +358,10 @@ impl Dist { name: PackageName, url: VerbatimUrl, install_path: &Path, - lock_path: &Path, ext: DistExtension, ) -> Result { // Convert to an absolute path. - let install_path = std::path::absolute(install_path)?; + let install_path = path::absolute(install_path)?; // Normalize the path. let install_path = normalize_absolute_path(&install_path)?; @@ -398,14 +386,12 @@ impl Dist { Ok(Self::Built(BuiltDist::Path(PathBuiltDist { filename, install_path, - lock_path: lock_path.to_path_buf(), url, }))) } DistExtension::Source(ext) => Ok(Self::Source(SourceDist::Path(PathSourceDist { name, install_path, - lock_path: lock_path.to_path_buf(), ext, url, }))), @@ -417,11 +403,10 @@ impl Dist { name: PackageName, url: VerbatimUrl, install_path: &Path, - lock_path: &Path, editable: bool, ) -> Result { // Convert to an absolute path. - let install_path = std::path::absolute(install_path)?; + let install_path = path::absolute(install_path)?; // Normalize the path. let install_path = normalize_absolute_path(&install_path)?; @@ -435,7 +420,6 @@ impl Dist { Ok(Self::Source(SourceDist::Directory(DirectorySourceDist { name, install_path, - lock_path: lock_path.to_path_buf(), editable, url, }))) @@ -466,18 +450,13 @@ impl Dist { archive.subdirectory, archive.ext, ), - ParsedUrl::Path(file) => Self::from_file_url( - name, - url.verbatim, - &file.install_path, - &file.lock_path, - file.ext, - ), + ParsedUrl::Path(file) => { + Self::from_file_url(name, url.verbatim, &file.install_path, file.ext) + } ParsedUrl::Directory(directory) => Self::from_directory_url( name, url.verbatim, &directory.install_path, - &directory.lock_path, directory.editable, ), ParsedUrl::Git(git) => { diff --git a/crates/distribution-types/src/resolution.rs b/crates/distribution-types/src/resolution.rs index 8a1f2a4fc64d..6cc263abc99d 100644 --- a/crates/distribution-types/src/resolution.rs +++ b/crates/distribution-types/src/resolution.rs @@ -185,7 +185,6 @@ impl From<&ResolvedDist> for Requirement { } Dist::Built(BuiltDist::Path(wheel)) => RequirementSource::Path { install_path: wheel.install_path.clone(), - lock_path: wheel.lock_path.clone(), url: wheel.url.clone(), ext: DistExtension::Wheel, }, @@ -214,13 +213,11 @@ impl From<&ResolvedDist> for Requirement { }, Dist::Source(SourceDist::Path(sdist)) => RequirementSource::Path { install_path: sdist.install_path.clone(), - lock_path: sdist.lock_path.clone(), url: sdist.url.clone(), ext: DistExtension::Source(sdist.ext), }, Dist::Source(SourceDist::Directory(sdist)) => RequirementSource::Directory { install_path: sdist.install_path.clone(), - lock_path: sdist.lock_path.clone(), url: sdist.url.clone(), editable: sdist.editable, }, diff --git a/crates/pypi-types/src/parsed_url.rs b/crates/pypi-types/src/parsed_url.rs index 4dca849ac944..614fc487b301 100644 --- a/crates/pypi-types/src/parsed_url.rs +++ b/crates/pypi-types/src/parsed_url.rs @@ -71,14 +71,12 @@ impl UnnamedRequirementUrl for VerbatimParsedUrl { ParsedUrl::Directory(ParsedDirectoryUrl { url: verbatim.to_url(), install_path: verbatim.as_path()?, - lock_path: path.as_ref().to_path_buf(), editable: false, }) } else { ParsedUrl::Path(ParsedPathUrl { url: verbatim.to_url(), install_path: verbatim.as_path()?, - lock_path: path.as_ref().to_path_buf(), ext: DistExtension::from_path(&path).map_err(|err| { ParsedUrlError::MissingExtensionPath(path.as_ref().to_path_buf(), err) })?, @@ -102,14 +100,12 @@ impl UnnamedRequirementUrl for VerbatimParsedUrl { ParsedUrl::Directory(ParsedDirectoryUrl { url: verbatim.to_url(), install_path: verbatim.as_path()?, - lock_path: path.as_ref().to_path_buf(), editable: false, }) } else { ParsedUrl::Path(ParsedPathUrl { url: verbatim.to_url(), install_path: verbatim.as_path()?, - lock_path: path.as_ref().to_path_buf(), ext: DistExtension::from_path(&path).map_err(|err| { ParsedUrlError::MissingExtensionPath(path.as_ref().to_path_buf(), err) })?, @@ -187,26 +183,16 @@ pub struct ParsedPathUrl { pub url: Url, /// The absolute path to the distribution which we use for installing. pub install_path: PathBuf, - /// The absolute path or path relative to the workspace root pointing to the distribution - /// which we use for locking. Unlike `given` on the verbatim URL all environment variables - /// are resolved, and unlike the install path, we did not yet join it on the base directory. - pub lock_path: PathBuf, /// The file extension, e.g. `tar.gz`, `zip`, etc. pub ext: DistExtension, } impl ParsedPathUrl { /// Construct a [`ParsedPathUrl`] from a path requirement source. - pub fn from_source( - install_path: PathBuf, - lock_path: PathBuf, - ext: DistExtension, - url: Url, - ) -> Self { + pub fn from_source(install_path: PathBuf, ext: DistExtension, url: Url) -> Self { Self { url, install_path, - lock_path, ext, } } @@ -221,25 +207,15 @@ pub struct ParsedDirectoryUrl { pub url: Url, /// The absolute path to the distribution which we use for installing. pub install_path: PathBuf, - /// The absolute path or path relative to the workspace root pointing to the distribution - /// which we use for locking. Unlike `given` on the verbatim URL all environment variables - /// are resolved, and unlike the install path, we did not yet join it on the base directory. - pub lock_path: PathBuf, pub editable: bool, } impl ParsedDirectoryUrl { /// Construct a [`ParsedDirectoryUrl`] from a path requirement source. - pub fn from_source( - install_path: PathBuf, - lock_path: PathBuf, - editable: bool, - url: Url, - ) -> Self { + pub fn from_source(install_path: PathBuf, editable: bool, url: Url) -> Self { Self { url, install_path, - lock_path, editable, } } @@ -393,7 +369,6 @@ impl TryFrom for ParsedUrl { Ok(Self::Directory(ParsedDirectoryUrl { url, install_path: path.clone(), - lock_path: path, editable: false, })) } else { @@ -402,7 +377,6 @@ impl TryFrom for ParsedUrl { ext: DistExtension::from_path(&path) .map_err(|err| ParsedUrlError::MissingExtensionPath(path.clone(), err))?, install_path: path.clone(), - lock_path: path, })) } } else { diff --git a/crates/pypi-types/src/requirement.rs b/crates/pypi-types/src/requirement.rs index d03348721670..b98e834bde2e 100644 --- a/crates/pypi-types/src/requirement.rs +++ b/crates/pypi-types/src/requirement.rs @@ -1,4 +1,5 @@ use std::fmt::{Display, Formatter}; +use std::io; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -10,7 +11,7 @@ use pep440_rs::VersionSpecifiers; use pep508_rs::{ marker, MarkerEnvironment, MarkerTree, RequirementOrigin, VerbatimUrl, VersionOrUrl, }; -use uv_fs::{PortablePathBuf, CWD}; +use uv_fs::{relative_to, PortablePathBuf, CWD}; use uv_git::{GitReference, GitSha, GitUrl}; use uv_normalize::{ExtraName, PackageName}; @@ -105,6 +106,14 @@ impl Requirement { _ => self, } } + + /// Convert the requirement to a [`Requirement`] relative to the given path. + pub fn relative_to(self, path: &Path) -> Result { + Ok(Self { + source: self.source.relative_to(path)?, + ..self + }) + } } impl From for pep508_rs::Requirement { @@ -175,28 +184,24 @@ impl From for pep508_rs::Requirement { } RequirementSource::Path { install_path, - lock_path, ext, url, } => Some(VersionOrUrl::Url(VerbatimParsedUrl { parsed_url: ParsedUrl::Path(ParsedPathUrl { url: url.to_url(), install_path, - lock_path, ext, }), verbatim: url, })), RequirementSource::Directory { install_path, - lock_path, editable, url, } => Some(VersionOrUrl::Url(VerbatimParsedUrl { parsed_url: ParsedUrl::Directory(ParsedDirectoryUrl { url: url.to_url(), install_path, - lock_path, editable, }), verbatim: url, @@ -342,10 +347,6 @@ pub enum RequirementSource { Path { /// The absolute path to the distribution which we use for installing. install_path: PathBuf, - /// The absolute path or path relative to the workspace root pointing to the distribution - /// which we use for locking. Unlike `given` on the verbatim URL all environment variables - /// are resolved, and unlike the install path, we did not yet join it on the base directory. - lock_path: PathBuf, /// The file extension, e.g. `tar.gz`, `zip`, etc. ext: DistExtension, /// The PEP 508 style URL in the format @@ -357,10 +358,6 @@ pub enum RequirementSource { Directory { /// The absolute path to the distribution which we use for installing. install_path: PathBuf, - /// The absolute path or path relative to the workspace root pointing to the distribution - /// which we use for locking. Unlike `given` on the verbatim URL all environment variables - /// are resolved, and unlike the install path, we did not yet join it on the base directory. - lock_path: PathBuf, /// For a source tree (a directory), whether to install as an editable. editable: bool, /// The PEP 508 style URL in the format @@ -376,13 +373,11 @@ impl RequirementSource { match parsed_url { ParsedUrl::Path(local_file) => RequirementSource::Path { install_path: local_file.install_path.clone(), - lock_path: local_file.lock_path.clone(), ext: local_file.ext, url, }, ParsedUrl::Directory(directory) => RequirementSource::Directory { install_path: directory.install_path.clone(), - lock_path: directory.lock_path.clone(), editable: directory.editable, url, }, @@ -427,13 +422,11 @@ impl RequirementSource { }), Self::Path { install_path, - lock_path, ext, url, } => Some(VerbatimParsedUrl { parsed_url: ParsedUrl::Path(ParsedPathUrl::from_source( install_path.clone(), - lock_path.clone(), *ext, url.to_url(), )), @@ -441,13 +434,11 @@ impl RequirementSource { }), Self::Directory { install_path, - lock_path, editable, url, } => Some(VerbatimParsedUrl { parsed_url: ParsedUrl::Directory(ParsedDirectoryUrl::from_source( install_path.clone(), - lock_path.clone(), *editable, url.to_url(), )), @@ -504,6 +495,33 @@ impl RequirementSource { | RequirementSource::Directory { .. } => None, } } + + /// Convert the source to a [`RequirementSource`] relative to the given path. + pub fn relative_to(self, path: &Path) -> Result { + match self { + RequirementSource::Registry { .. } + | RequirementSource::Url { .. } + | RequirementSource::Git { .. } => Ok(self), + RequirementSource::Path { + install_path, + ext, + url, + } => Ok(Self::Path { + install_path: relative_to(&install_path, path)?, + ext, + url, + }), + RequirementSource::Directory { + install_path, + editable, + url, + } => Ok(Self::Directory { + install_path: relative_to(&install_path, path)?, + editable, + url, + }), + } + } } impl Display for RequirementSource { @@ -639,26 +657,24 @@ impl From for RequirementSourceWire { } } RequirementSource::Path { - install_path: _, - lock_path, + install_path, ext: _, url: _, } => Self::Path { - path: PortablePathBuf::from(lock_path), + path: PortablePathBuf::from(install_path), }, RequirementSource::Directory { - install_path: _, - lock_path, + install_path, editable, url: _, } => { if editable { Self::Editable { - editable: PortablePathBuf::from(lock_path), + editable: PortablePathBuf::from(install_path), } } else { Self::Directory { - directory: PortablePathBuf::from(lock_path), + directory: PortablePathBuf::from(install_path), } } } @@ -732,8 +748,7 @@ impl TryFrom for RequirementSource { Ok(Self::Path { ext: DistExtension::from_path(path.as_path()) .map_err(|err| ParsedUrlError::MissingExtensionPath(path.clone(), err))?, - install_path: path.clone(), - lock_path: path, + install_path: path, url, }) } @@ -741,8 +756,7 @@ impl TryFrom for RequirementSource { let directory = PathBuf::from(directory); let url = VerbatimUrl::parse_path(&directory, &*CWD)?; Ok(Self::Directory { - install_path: directory.clone(), - lock_path: directory, + install_path: directory, editable: false, url, }) @@ -751,8 +765,7 @@ impl TryFrom for RequirementSource { let editable = PathBuf::from(editable); let url = VerbatimUrl::parse_path(&editable, &*CWD)?; Ok(Self::Directory { - install_path: editable.clone(), - lock_path: editable, + install_path: editable, editable: true, url, }) @@ -809,7 +822,6 @@ mod tests { marker: MarkerTree::TRUE, source: RequirementSource::Directory { install_path: PathBuf::from(path), - lock_path: PathBuf::from(path), editable: false, url: VerbatimUrl::from_path(Path::new(path)).unwrap(), }, diff --git a/crates/requirements-txt/src/lib.rs b/crates/requirements-txt/src/lib.rs index 1bc5c7d287c5..3d00cb485494 100644 --- a/crates/requirements-txt/src/lib.rs +++ b/crates/requirements-txt/src/lib.rs @@ -1845,7 +1845,6 @@ mod test { fragment: None, }, install_path: "/foo/bar", - lock_path: "/foo/bar", editable: true, }, ), diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-bare-url.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-bare-url.txt.snap index 32a0becfafe1..e3a0a6bf6038 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-bare-url.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-bare-url.txt.snap @@ -22,7 +22,6 @@ RequirementsTxt { fragment: None, }, install_path: "/scripts/packages/black_editable", - lock_path: "./scripts/packages/black_editable", editable: false, }, ), @@ -72,7 +71,6 @@ RequirementsTxt { fragment: None, }, install_path: "/scripts/packages/black_editable", - lock_path: "./scripts/packages/black_editable", editable: false, }, ), @@ -126,7 +124,6 @@ RequirementsTxt { fragment: None, }, install_path: "/scripts/packages/black_editable", - lock_path: "/scripts/packages/black_editable", editable: false, }, ), diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-editable.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-editable.txt.snap index 9dda8d2f6589..6eba9d2928f2 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-editable.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-editable.txt.snap @@ -24,7 +24,6 @@ RequirementsTxt { fragment: None, }, install_path: "/editable", - lock_path: "./editable", editable: true, }, ), @@ -81,7 +80,6 @@ RequirementsTxt { fragment: None, }, install_path: "/editable", - lock_path: "./editable", editable: true, }, ), @@ -138,7 +136,6 @@ RequirementsTxt { fragment: None, }, install_path: "/editable", - lock_path: "./editable", editable: true, }, ), @@ -195,7 +192,6 @@ RequirementsTxt { fragment: None, }, install_path: "/editable", - lock_path: "./editable", editable: true, }, ), @@ -252,7 +248,6 @@ RequirementsTxt { fragment: None, }, install_path: "/editable", - lock_path: "./editable", editable: true, }, ), @@ -302,7 +297,6 @@ RequirementsTxt { fragment: None, }, install_path: "/editable[d", - lock_path: "./editable[d", editable: true, }, ), diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-windows-bare-url.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-windows-bare-url.txt.snap index 68a137a00c20..f005012dc419 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-windows-bare-url.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-windows-bare-url.txt.snap @@ -22,7 +22,6 @@ RequirementsTxt { fragment: None, }, install_path: "/scripts/packages/black_editable", - lock_path: "./scripts/packages/black_editable", editable: false, }, ), @@ -72,7 +71,6 @@ RequirementsTxt { fragment: None, }, install_path: "/scripts/packages/black_editable", - lock_path: "./scripts/packages/black_editable", editable: false, }, ), @@ -126,7 +124,6 @@ RequirementsTxt { fragment: None, }, install_path: "/scripts/packages/black_editable", - lock_path: "scripts/packages/black_editable", editable: false, }, ), diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-windows-editable.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-windows-editable.txt.snap index f3129dda14c7..39250d9388ca 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-windows-editable.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-windows-editable.txt.snap @@ -24,7 +24,6 @@ RequirementsTxt { fragment: None, }, install_path: "/editable", - lock_path: "./editable", editable: true, }, ), @@ -81,7 +80,6 @@ RequirementsTxt { fragment: None, }, install_path: "/editable", - lock_path: "./editable", editable: true, }, ), @@ -138,7 +136,6 @@ RequirementsTxt { fragment: None, }, install_path: "/editable", - lock_path: "./editable", editable: true, }, ), @@ -195,7 +192,6 @@ RequirementsTxt { fragment: None, }, install_path: "/editable", - lock_path: "./editable", editable: true, }, ), @@ -252,7 +248,6 @@ RequirementsTxt { fragment: None, }, install_path: "/editable", - lock_path: "./editable", editable: true, }, ), @@ -302,7 +297,6 @@ RequirementsTxt { fragment: None, }, install_path: "/editable[d", - lock_path: "./editable[d", editable: true, }, ), diff --git a/crates/uv-distribution/Cargo.toml b/crates/uv-distribution/Cargo.toml index 6fc8b98bd6e0..0aaef1a3e320 100644 --- a/crates/uv-distribution/Cargo.toml +++ b/crates/uv-distribution/Cargo.toml @@ -35,7 +35,6 @@ anyhow = { workspace = true } fs-err = { workspace = true } futures = { workspace = true } nanoid = { workspace = true } -path-absolutize = { workspace = true } reqwest = { workspace = true } reqwest-middleware = { workspace = true } rmp-serde = { workspace = true } diff --git a/crates/uv-distribution/src/metadata/lowering.rs b/crates/uv-distribution/src/metadata/lowering.rs index 4a59da47305a..669ebcfe3be5 100644 --- a/crates/uv-distribution/src/metadata/lowering.rs +++ b/crates/uv-distribution/src/metadata/lowering.rs @@ -2,14 +2,14 @@ use std::collections::BTreeMap; use std::io; use std::path::{Path, PathBuf}; +use thiserror::Error; +use url::Url; + use distribution_filename::DistExtension; -use path_absolutize::Absolutize; use pep440_rs::VersionSpecifiers; use pep508_rs::{VerbatimUrl, VersionOrUrl}; use pypi_types::{ParsedUrlError, Requirement, RequirementSource, VerbatimParsedUrl}; -use thiserror::Error; -use url::Url; -use uv_fs::{relative_to, Simplified}; +use uv_fs::Simplified; use uv_git::GitReference; use uv_normalize::PackageName; use uv_warnings::warn_user_once; @@ -142,14 +142,15 @@ impl LoweredRequirement { // relative to workspace: `packages/current_project` // workspace lock root: `../current_workspace` // relative to main workspace: `../current_workspace/packages/current_project` - let relative_to_workspace = relative_to(member.root(), workspace.install_path()) - .map_err(LoweringError::RelativeTo)?; - let relative_to_main_workspace = workspace.lock_path().join(relative_to_workspace); - let url = VerbatimUrl::parse_absolute_path(member.root())? - .with_given(relative_to_main_workspace.to_string_lossy()); + let url = VerbatimUrl::parse_absolute_path(member.root())?; + let install_path = url.to_file_path().map_err(|()| { + LoweringError::RelativeTo(io::Error::new( + io::ErrorKind::Other, + "Invalid path in file URL", + )) + })?; RequirementSource::Directory { - install_path: member.root().clone(), - lock_path: relative_to_main_workspace, + install_path, url, editable: true, } @@ -360,28 +361,20 @@ fn path_source( Origin::Workspace => workspace_root, }; let url = VerbatimUrl::parse_path(path, base)?.with_given(path.to_string_lossy()); - let absolute_path = path - .to_path_buf() - .absolutize_from(base) - .map_err(|err| LoweringError::Absolutize(path.to_path_buf(), err))? - .to_path_buf(); - let relative_to_workspace = if path.is_relative() { - // Relative paths in a project are relative to the project root, but the lockfile is - // relative to the workspace root. - relative_to(&absolute_path, workspace_root).map_err(LoweringError::RelativeTo)? - } else { - // If the user gave us an absolute path, we respect that. - path.to_path_buf() - }; - let is_dir = if let Ok(metadata) = absolute_path.metadata() { + let install_path = url.to_file_path().map_err(|()| { + LoweringError::RelativeTo(io::Error::new( + io::ErrorKind::Other, + "Invalid path in file URL", + )) + })?; + let is_dir = if let Ok(metadata) = install_path.metadata() { metadata.is_dir() } else { - absolute_path.extension().is_none() + install_path.extension().is_none() }; if is_dir { Ok(RequirementSource::Directory { - install_path: absolute_path, - lock_path: relative_to_workspace, + install_path, url, editable, }) @@ -390,10 +383,9 @@ fn path_source( return Err(LoweringError::EditableFile(url.to_string())); } Ok(RequirementSource::Path { - install_path: absolute_path, - lock_path: relative_to_workspace, - ext: DistExtension::from_path(path) + ext: DistExtension::from_path(&install_path) .map_err(|err| ParsedUrlError::MissingExtensionPath(path.to_path_buf(), err))?, + install_path, url, }) } diff --git a/crates/uv-distribution/src/metadata/mod.rs b/crates/uv-distribution/src/metadata/mod.rs index f28c48a8fe51..999fabd17607 100644 --- a/crates/uv-distribution/src/metadata/mod.rs +++ b/crates/uv-distribution/src/metadata/mod.rs @@ -59,7 +59,6 @@ impl Metadata { pub async fn from_workspace( metadata: Metadata23, install_path: &Path, - lock_path: &Path, sources: SourceStrategy, ) -> Result { // Lower the requirements. @@ -75,7 +74,6 @@ impl Metadata { provides_extras: metadata.provides_extras, }, install_path, - lock_path, sources, ) .await?; diff --git a/crates/uv-distribution/src/metadata/requires_dist.rs b/crates/uv-distribution/src/metadata/requires_dist.rs index 90808203ed84..4e1ce9eb52b5 100644 --- a/crates/uv-distribution/src/metadata/requires_dist.rs +++ b/crates/uv-distribution/src/metadata/requires_dist.rs @@ -37,7 +37,6 @@ impl RequiresDist { pub async fn from_project_maybe_workspace( metadata: pypi_types::RequiresDist, install_path: &Path, - lock_path: &Path, sources: SourceStrategy, ) -> Result { match sources { @@ -46,7 +45,6 @@ impl RequiresDist { // TODO(konsti): Cache workspace discovery. let Some(project_workspace) = ProjectWorkspace::from_maybe_project_root( install_path, - lock_path, &DiscoveryOptions::default(), ) .await? @@ -160,7 +158,6 @@ mod test { let pyproject_toml = PyProjectToml::from_string(contents.to_string())?; let path = Path::new("pyproject.toml"); let project_workspace = ProjectWorkspace::from_project( - path, path, pyproject_toml .project diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 7672848c795c..9f7a796dad0e 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -425,7 +425,6 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { let requires_dist = RequiresDist::from_project_maybe_workspace( requires_dist, project_root, - project_root, self.build_context.sources(), ) .await?; @@ -1009,7 +1008,6 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { Metadata::from_workspace( metadata, resource.install_path.as_ref(), - resource.lock_path.as_ref(), self.build_context.sources(), ) .await?, @@ -1024,7 +1022,6 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { Metadata::from_workspace( metadata, resource.install_path.as_ref(), - resource.lock_path.as_ref(), self.build_context.sources(), ) .await?, @@ -1049,7 +1046,6 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { Metadata::from_workspace( metadata, resource.install_path.as_ref(), - resource.lock_path.as_ref(), self.build_context.sources(), ) .await?, @@ -1081,7 +1077,6 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { Metadata::from_workspace( metadata, resource.install_path.as_ref(), - resource.lock_path.as_ref(), self.build_context.sources(), ) .await?, @@ -1252,8 +1247,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { Self::read_static_metadata(source, fetch.path(), resource.subdirectory).await? { return Ok(ArchiveMetadata::from( - Metadata::from_workspace(metadata, &path, &path, self.build_context.sources()) - .await?, + Metadata::from_workspace(metadata, &path, self.build_context.sources()).await?, )); } @@ -1276,8 +1270,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { debug!("Using cached metadata for: {source}"); return Ok(ArchiveMetadata::from( - Metadata::from_workspace(metadata, &path, &path, self.build_context.sources()) - .await?, + Metadata::from_workspace(metadata, &path, self.build_context.sources()).await?, )); } } @@ -1297,8 +1290,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .map_err(Error::CacheWrite)?; return Ok(ArchiveMetadata::from( - Metadata::from_workspace(metadata, &path, &path, self.build_context.sources()) - .await?, + Metadata::from_workspace(metadata, &path, self.build_context.sources()).await?, )); } @@ -1324,13 +1316,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .map_err(Error::CacheWrite)?; Ok(ArchiveMetadata::from( - Metadata::from_workspace( - metadata, - fetch.path(), - fetch.path(), - self.build_context.sources(), - ) - .await?, + Metadata::from_workspace(metadata, fetch.path(), self.build_context.sources()).await?, )) } diff --git a/crates/uv-installer/src/plan.rs b/crates/uv-installer/src/plan.rs index 804abc41d568..8f396c8e08a4 100644 --- a/crates/uv-installer/src/plan.rs +++ b/crates/uv-installer/src/plan.rs @@ -266,7 +266,6 @@ impl<'a> Planner<'a> { url, editable, install_path, - lock_path, } => { // Convert to an absolute path. let install_path = std::path::absolute(install_path)?; @@ -283,7 +282,6 @@ impl<'a> Planner<'a> { name: requirement.name.clone(), url: url.clone(), install_path, - lock_path: lock_path.clone(), editable: *editable, }; @@ -305,7 +303,6 @@ impl<'a> Planner<'a> { ext, url, install_path, - lock_path, } => { // Convert to an absolute path. let install_path = std::path::absolute(install_path)?; @@ -335,21 +332,20 @@ impl<'a> Planner<'a> { filename, url: url.clone(), install_path, - lock_path: lock_path.clone(), }; if !wheel.filename.is_compatible(tags) { bail!( - "A path dependency is incompatible with the current platform: {}", - wheel.lock_path.user_display() - ); + "A path dependency is incompatible with the current platform: {}", + wheel.install_path.user_display() + ); } if no_binary { bail!( - "A path dependency points to a wheel which conflicts with `--no-binary`: {}", - wheel.url - ); + "A path dependency points to a wheel which conflicts with `--no-binary`: {}", + wheel.url + ); } // Find the exact wheel from the cache, since we know the filename in @@ -387,7 +383,6 @@ impl<'a> Planner<'a> { name: requirement.name.clone(), url: url.clone(), install_path, - lock_path: lock_path.clone(), ext: *ext, }; diff --git a/crates/uv-installer/src/satisfies.rs b/crates/uv-installer/src/satisfies.rs index 1a5aac961fbd..500fc079873d 100644 --- a/crates/uv-installer/src/satisfies.rs +++ b/crates/uv-installer/src/satisfies.rs @@ -150,7 +150,6 @@ impl RequirementSatisfaction { } RequirementSource::Path { install_path: requested_path, - lock_path: _, ext: _, url: _, } => { @@ -197,7 +196,6 @@ impl RequirementSatisfaction { } RequirementSource::Directory { install_path: requested_path, - lock_path: _, editable: requested_editable, url: _, } => { diff --git a/crates/uv-requirements/src/lookahead.rs b/crates/uv-requirements/src/lookahead.rs index 426affea4398..f703581e0784 100644 --- a/crates/uv-requirements/src/lookahead.rs +++ b/crates/uv-requirements/src/lookahead.rs @@ -283,26 +283,17 @@ fn required_dist(requirement: &Requirement) -> Result, distribution } RequirementSource::Path { install_path, - lock_path, ext, url, - } => Dist::from_file_url( - requirement.name.clone(), - url.clone(), - install_path, - lock_path, - *ext, - )?, + } => Dist::from_file_url(requirement.name.clone(), url.clone(), install_path, *ext)?, RequirementSource::Directory { install_path, - lock_path, url, editable, } => Dist::from_directory_url( requirement.name.clone(), url.clone(), install_path, - lock_path, *editable, )?, })) diff --git a/crates/uv-requirements/src/source_tree.rs b/crates/uv-requirements/src/source_tree.rs index c259a6339896..556e7dec4c38 100644 --- a/crates/uv-requirements/src/source_tree.rs +++ b/crates/uv-requirements/src/source_tree.rs @@ -171,7 +171,6 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> { let source = SourceUrl::Directory(DirectorySourceUrl { url: &url, install_path: Cow::Borrowed(source_tree), - lock_path: Cow::Borrowed(source_tree), editable: false, }); diff --git a/crates/uv-requirements/src/unnamed.rs b/crates/uv-requirements/src/unnamed.rs index 0cc2a994c005..436d30d56bdb 100644 --- a/crates/uv-requirements/src/unnamed.rs +++ b/crates/uv-requirements/src/unnamed.rs @@ -256,7 +256,6 @@ impl<'a, Context: BuildContext> NamedRequirementsResolver<'a, Context> { SourceUrl::Directory(DirectorySourceUrl { url: &requirement.url.verbatim, install_path: Cow::Borrowed(&parsed_directory_url.install_path), - lock_path: Cow::Borrowed(&parsed_directory_url.lock_path), editable: parsed_directory_url.editable, }) } diff --git a/crates/uv-resolver/src/lock.rs b/crates/uv-resolver/src/lock.rs index 557f6c30706b..6820bee065ed 100644 --- a/crates/uv-resolver/src/lock.rs +++ b/crates/uv-resolver/src/lock.rs @@ -2,6 +2,7 @@ use std::borrow::Cow; use std::collections::{BTreeMap, BTreeSet, VecDeque}; use std::convert::Infallible; use std::fmt::{Debug, Display}; +use std::io; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -30,7 +31,7 @@ use pypi_types::{ }; use uv_configuration::ExtrasSpecification; use uv_distribution::DistributionDatabase; -use uv_fs::{relative_to, PortablePath, PortablePathBuf, Simplified}; +use uv_fs::{relative_to, PortablePath, PortablePathBuf}; use uv_git::{GitReference, GitSha, RepositoryReference, ResolvedRepositoryReference}; use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_types::BuildContext; @@ -75,7 +76,7 @@ pub struct Lock { impl Lock { /// Initialize a [`Lock`] from a [`ResolutionGraph`]. - pub fn from_resolution_graph(graph: &ResolutionGraph) -> Result { + pub fn from_resolution_graph(graph: &ResolutionGraph, root: &Path) -> Result { let mut locked_dists = BTreeMap::new(); // Lock all base packages. @@ -88,7 +89,7 @@ impl Lock { .fork_markers(dist.name(), &dist.version, dist.dist.version_or_url().url()) .cloned() .unwrap_or_default(); - let mut locked_dist = Package::from_annotated_dist(dist, fork_markers)?; + let mut locked_dist = Package::from_annotated_dist(dist, fork_markers, root)?; // Add all dependencies for edge in graph.petgraph.edges(node_index) { @@ -97,7 +98,7 @@ impl Lock { continue; }; let marker = edge.weight().clone(); - locked_dist.add_dependency(dependency_dist, marker); + locked_dist.add_dependency(dependency_dist, marker, root)?; } let id = locked_dist.id.clone(); if let Some(locked_dist) = locked_dists.insert(id, locked_dist) { @@ -115,7 +116,7 @@ impl Lock { continue; }; if let Some(extra) = dist.extra.as_ref() { - let id = PackageId::from_annotated_dist(dist); + let id = PackageId::from_annotated_dist(dist, root)?; let Some(locked_dist) = locked_dists.get_mut(&id) else { return Err(LockErrorKind::MissingExtraBase { id, @@ -129,11 +130,16 @@ impl Lock { continue; }; let marker = edge.weight().clone(); - locked_dist.add_optional_dependency(extra.clone(), dependency_dist, marker); + locked_dist.add_optional_dependency( + extra.clone(), + dependency_dist, + marker, + root, + )?; } } if let Some(group) = dist.dev.as_ref() { - let id = PackageId::from_annotated_dist(dist); + let id = PackageId::from_annotated_dist(dist, root)?; let Some(locked_dist) = locked_dists.get_mut(&id) else { return Err(LockErrorKind::MissingDevBase { id, @@ -147,7 +153,7 @@ impl Lock { continue; }; let marker = edge.weight().clone(); - locked_dist.add_dev_dependency(group.clone(), dependency_dist, marker); + locked_dist.add_dev_dependency(group.clone(), dependency_dist, marker, root)?; } } } @@ -981,6 +987,8 @@ pub struct ResolverManifest { } impl ResolverManifest { + /// Initialize a [`ResolverManifest`] with the given members, requirements, constraints, and + /// overrides. pub fn new( members: impl IntoIterator, requirements: impl IntoIterator, @@ -994,6 +1002,28 @@ impl ResolverManifest { overrides: overrides.into_iter().collect(), } } + + /// Convert the manifest to a relative form using the given workspace. + pub fn relative_to(self, workspace: &Workspace) -> Result { + Ok(Self { + members: self.members, + requirements: self + .requirements + .into_iter() + .map(|requirement| requirement.relative_to(workspace.install_path())) + .collect::, _>>()?, + constraints: self + .constraints + .into_iter() + .map(|requirement| requirement.relative_to(workspace.install_path())) + .collect::, _>>()?, + overrides: self + .overrides + .into_iter() + .map(|requirement| requirement.relative_to(workspace.install_path())) + .collect::, _>>()?, + }) + } } #[derive(Clone, Debug, serde::Deserialize)] @@ -1094,8 +1124,9 @@ impl Package { fn from_annotated_dist( annotated_dist: &AnnotatedDist, fork_markers: Vec, + root: &Path, ) -> Result { - let id = PackageId::from_annotated_dist(annotated_dist); + let id = PackageId::from_annotated_dist(annotated_dist, root)?; let sdist = SourceDist::from_annotated_dist(&id, annotated_dist)?; let wheels = Wheel::from_annotated_dist(annotated_dist)?; let requires_dist = if id.source.is_immutable() { @@ -1106,7 +1137,9 @@ impl Package { .requires_dist .iter() .cloned() - .collect() + .map(|requirement| requirement.relative_to(root)) + .collect::>() + .map_err(LockErrorKind::RequirementRelativePath)? }; let requires_dev = if id.source.is_immutable() { BTreeMap::default() @@ -1115,8 +1148,16 @@ impl Package { .metadata .dev_dependencies .iter() - .map(|(k, v)| (k.clone(), v.iter().cloned().collect())) - .collect() + .map(|(group, requirements)| { + let requirements = requirements + .iter() + .cloned() + .map(|requirement| requirement.relative_to(root)) + .collect::>() + .map_err(LockErrorKind::RequirementRelativePath)?; + Ok::<_, LockError>((group.clone(), requirements)) + }) + .collect::>()? }; Ok(Package { id, @@ -1134,17 +1175,24 @@ impl Package { } /// Add the [`AnnotatedDist`] as a dependency of the [`Package`]. - fn add_dependency(&mut self, annotated_dist: &AnnotatedDist, marker: MarkerTree) { - let new_dep = Dependency::from_annotated_dist(annotated_dist, marker); + fn add_dependency( + &mut self, + annotated_dist: &AnnotatedDist, + marker: MarkerTree, + root: &Path, + ) -> Result<(), LockError> { + let new_dep = Dependency::from_annotated_dist(annotated_dist, marker, root)?; for existing_dep in &mut self.dependencies { if existing_dep.package_id == new_dep.package_id && existing_dep.marker == new_dep.marker { existing_dep.extra.extend(new_dep.extra); - return; + return Ok(()); } } + self.dependencies.push(new_dep); + Ok(()) } /// Add the [`AnnotatedDist`] as an optional dependency of the [`Package`]. @@ -1153,16 +1201,19 @@ impl Package { extra: ExtraName, annotated_dist: &AnnotatedDist, marker: MarkerTree, - ) { - let dep = Dependency::from_annotated_dist(annotated_dist, marker); + root: &Path, + ) -> Result<(), LockError> { + let dep = Dependency::from_annotated_dist(annotated_dist, marker, root)?; let optional_deps = self.optional_dependencies.entry(extra).or_default(); for existing_dep in &mut *optional_deps { if existing_dep.package_id == dep.package_id && existing_dep.marker == dep.marker { existing_dep.extra.extend(dep.extra); - return; + return Ok(()); } } + optional_deps.push(dep); + Ok(()) } /// Add the [`AnnotatedDist`] as a development dependency of the [`Package`]. @@ -1171,16 +1222,19 @@ impl Package { dev: GroupName, annotated_dist: &AnnotatedDist, marker: MarkerTree, - ) { - let dep = Dependency::from_annotated_dist(annotated_dist, marker); + root: &Path, + ) -> Result<(), LockError> { + let dep = Dependency::from_annotated_dist(annotated_dist, marker, root)?; let dev_deps = self.dev_dependencies.entry(dev).or_default(); for existing_dep in &mut *dev_deps { if existing_dep.package_id == dep.package_id && existing_dep.marker == dep.marker { existing_dep.extra.extend(dep.extra); - return; + return Ok(()); } } + dev_deps.push(dep); + Ok(()) } /// Convert the [`Package`] to a [`Dist`] that can be used in installation. @@ -1206,7 +1260,6 @@ impl Package { filename, url: verbatim_url(workspace_root.join(path), &self.id)?, install_path: workspace_root.join(path), - lock_path: path.clone(), }; let built_dist = BuiltDist::Path(path_dist); Ok(Dist::Built(built_dist)) @@ -1268,7 +1321,6 @@ impl Package { name: self.id.name.clone(), url: verbatim_url(workspace_root.join(path), &self.id)?, install_path: workspace_root.join(path), - lock_path: path.clone(), ext: SourceDistExtension::from_path(path)?, }; distribution_types::SourceDist::Path(path_dist) @@ -1278,7 +1330,6 @@ impl Package { name: self.id.name.clone(), url: verbatim_url(workspace_root.join(path), &self.id)?, install_path: workspace_root.join(path), - lock_path: path.clone(), editable: false, }; distribution_types::SourceDist::Directory(dir_dist) @@ -1288,7 +1339,6 @@ impl Package { name: self.id.name.clone(), url: verbatim_url(workspace_root.join(path), &self.id)?, install_path: workspace_root.join(path), - lock_path: path.clone(), editable: true, }; distribution_types::SourceDist::Directory(dir_dist) @@ -1691,15 +1741,18 @@ pub(crate) struct PackageId { } impl PackageId { - fn from_annotated_dist(annotated_dist: &AnnotatedDist) -> PackageId { + fn from_annotated_dist( + annotated_dist: &AnnotatedDist, + root: &Path, + ) -> Result { let name = annotated_dist.metadata.name.clone(); let version = annotated_dist.metadata.version.clone(); - let source = Source::from_resolved_dist(&annotated_dist.dist); - PackageId { + let source = Source::from_resolved_dist(&annotated_dist.dist, root)?; + Ok(Self { name, version, source, - } + }) } /// Writes this package ID inline into the table given. @@ -1792,43 +1845,50 @@ enum Source { } impl Source { - fn from_resolved_dist(resolved_dist: &ResolvedDist) -> Source { + fn from_resolved_dist(resolved_dist: &ResolvedDist, root: &Path) -> Result { match *resolved_dist { // We pass empty installed packages for locking. ResolvedDist::Installed(_) => unreachable!(), - ResolvedDist::Installable(ref dist) => Source::from_dist(dist), + ResolvedDist::Installable(ref dist) => Source::from_dist(dist, root), } } - fn from_dist(dist: &Dist) -> Source { + fn from_dist(dist: &Dist, root: &Path) -> Result { match *dist { - Dist::Built(ref built_dist) => Source::from_built_dist(built_dist), - Dist::Source(ref source_dist) => Source::from_source_dist(source_dist), + Dist::Built(ref built_dist) => Source::from_built_dist(built_dist, root), + Dist::Source(ref source_dist) => Source::from_source_dist(source_dist, root), } } - fn from_built_dist(built_dist: &BuiltDist) -> Source { + fn from_built_dist(built_dist: &BuiltDist, root: &Path) -> Result { match *built_dist { - BuiltDist::Registry(ref reg_dist) => Source::from_registry_built_dist(reg_dist), - BuiltDist::DirectUrl(ref direct_dist) => Source::from_direct_built_dist(direct_dist), - BuiltDist::Path(ref path_dist) => Source::from_path_built_dist(path_dist), + BuiltDist::Registry(ref reg_dist) => Ok(Source::from_registry_built_dist(reg_dist)), + BuiltDist::DirectUrl(ref direct_dist) => { + Ok(Source::from_direct_built_dist(direct_dist)) + } + BuiltDist::Path(ref path_dist) => Source::from_path_built_dist(path_dist, root), } } - fn from_source_dist(source_dist: &distribution_types::SourceDist) -> Source { + fn from_source_dist( + source_dist: &distribution_types::SourceDist, + root: &Path, + ) -> Result { match *source_dist { distribution_types::SourceDist::Registry(ref reg_dist) => { - Source::from_registry_source_dist(reg_dist) + Ok(Source::from_registry_source_dist(reg_dist)) } distribution_types::SourceDist::DirectUrl(ref direct_dist) => { - Source::from_direct_source_dist(direct_dist) + Ok(Source::from_direct_source_dist(direct_dist)) + } + distribution_types::SourceDist::Git(ref git_dist) => { + Ok(Source::from_git_dist(git_dist)) } - distribution_types::SourceDist::Git(ref git_dist) => Source::from_git_dist(git_dist), distribution_types::SourceDist::Path(ref path_dist) => { - Source::from_path_source_dist(path_dist) + Source::from_path_source_dist(path_dist, root) } distribution_types::SourceDist::Directory(ref directory) => { - Source::from_directory_source_dist(directory) + Source::from_directory_source_dist(directory, root) } } } @@ -1861,22 +1921,29 @@ impl Source { ) } - fn from_path_built_dist(path_dist: &PathBuiltDist) -> Source { - let path = path_dist.lock_path.simplified().to_path_buf(); - Source::Path(path) + fn from_path_built_dist(path_dist: &PathBuiltDist, root: &Path) -> Result { + let path = relative_to(&path_dist.install_path, root) + .map_err(LockErrorKind::DistributionRelativePath)?; + Ok(Source::Path(path)) } - fn from_path_source_dist(path_dist: &PathSourceDist) -> Source { - let path = path_dist.install_path.simplified().to_path_buf(); - Source::Path(path) + fn from_path_source_dist(path_dist: &PathSourceDist, root: &Path) -> Result { + let path = relative_to(&path_dist.install_path, root) + .map_err(LockErrorKind::DistributionRelativePath)?; + + Ok(Source::Path(path)) } - fn from_directory_source_dist(directory_dist: &DirectorySourceDist) -> Source { - let path = directory_dist.lock_path.simplified().to_path_buf(); + fn from_directory_source_dist( + directory_dist: &DirectorySourceDist, + root: &Path, + ) -> Result { + let path = relative_to(&directory_dist.install_path, root) + .map_err(LockErrorKind::DistributionRelativePath)?; if directory_dist.editable { - Source::Editable(path) + Ok(Source::Editable(path)) } else { - Source::Directory(path) + Ok(Source::Directory(path)) } } @@ -2649,14 +2716,18 @@ struct Dependency { } impl Dependency { - fn from_annotated_dist(annotated_dist: &AnnotatedDist, marker: MarkerTree) -> Dependency { - let package_id = PackageId::from_annotated_dist(annotated_dist); + fn from_annotated_dist( + annotated_dist: &AnnotatedDist, + marker: MarkerTree, + root: &Path, + ) -> Result { + let package_id = PackageId::from_annotated_dist(annotated_dist, root)?; let extra = annotated_dist.extra.iter().cloned().collect(); - Dependency { + Ok(Self { package_id, extra, marker, - } + }) } /// Returns the TOML representation of this dependency. @@ -2838,19 +2909,11 @@ fn normalize_requirement( }) } RequirementSource::Path { - install_path: _, - lock_path, + install_path, ext, url: _, } => { - // When a path requirement comes from the lockfile, `install_path` and `lock_path` are - // both relative to the lockfile. - // - // When a path requirement is deserialized from package metadata, `install_path` is - // absolute, and `lock_path` is relative to the lockfile. - let install_path = uv_fs::normalize_path(&workspace.install_path().join(&lock_path)); - let lock_path = relative_to(workspace.install_path(), &lock_path) - .map_err(LockErrorKind::RequirementRelativePath)?; + let install_path = uv_fs::normalize_path(&workspace.install_path().join(&install_path)); let url = VerbatimUrl::from_path(&install_path) .map_err(LockErrorKind::RequirementVerbatimUrl)?; @@ -2860,7 +2923,6 @@ fn normalize_requirement( marker: requirement.marker, source: RequirementSource::Path { install_path, - lock_path, ext, url, }, @@ -2868,14 +2930,11 @@ fn normalize_requirement( }) } RequirementSource::Directory { - install_path: _, - lock_path, + install_path, editable, url: _, } => { - let install_path = uv_fs::normalize_path(&workspace.install_path().join(&lock_path)); - let lock_path = relative_to(workspace.install_path(), &lock_path) - .map_err(LockErrorKind::RequirementRelativePath)?; + let install_path = uv_fs::normalize_path(&workspace.install_path().join(&install_path)); let url = VerbatimUrl::from_path(&install_path) .map_err(LockErrorKind::RequirementVerbatimUrl)?; @@ -2885,7 +2944,6 @@ fn normalize_requirement( marker: requirement.marker, source: RequirementSource::Directory { install_path, - lock_path, editable, url, }, @@ -3072,6 +3130,13 @@ enum LockErrorKind { #[source] err: VerbatimUrlError, }, + /// An error that occurs when parsing an existing requirement. + #[error("could not compute relative path between workspace and distribution")] + DistributionRelativePath( + /// The inner error we forward. + #[source] + std::io::Error, + ), /// An error that occurs when an ambiguous `package.dependency` is /// missing a `version` field. #[error( diff --git a/crates/uv-resolver/src/pubgrub/dependencies.rs b/crates/uv-resolver/src/pubgrub/dependencies.rs index ae59ac98ff7a..4aecef952d80 100644 --- a/crates/uv-resolver/src/pubgrub/dependencies.rs +++ b/crates/uv-resolver/src/pubgrub/dependencies.rs @@ -135,11 +135,9 @@ impl PubGrubRequirement { ext, url, install_path, - lock_path, } => { let parsed_url = ParsedUrl::Path(ParsedPathUrl::from_source( install_path.clone(), - lock_path.clone(), *ext, url.to_url(), )); @@ -149,11 +147,9 @@ impl PubGrubRequirement { editable, url, install_path, - lock_path, } => { let parsed_url = ParsedUrl::Directory(ParsedDirectoryUrl::from_source( install_path.clone(), - lock_path.clone(), *editable, url.to_url(), )); diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index 6914f70e35e0..f4d86ae3b390 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -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, .. diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs index 204eefbfbc93..6a1c69a6afc4 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -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, /// 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, @@ -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, 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", diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 99ade36704a5..21b11ac5b674 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -67,7 +67,6 @@ pub(crate) async fn lock( frozen: bool, python: Option, settings: ResolverSettings, - python_preference: PythonPreference, python_downloads: PythonDownloads, connectivity: Connectivity, @@ -523,14 +522,12 @@ async fn do_lock( // Notify the user of any resolution diagnostics. pip::operations::diagnose_resolution(resolution.diagnostics(), printer)?; + let manifest = ResolverManifest::new(members, requirements, constraints, overrides) + .relative_to(workspace)?; + let previous = existing_lock.map(ValidatedLock::into_lock); - let lock = Lock::from_resolution_graph(&resolution)? - .with_manifest(ResolverManifest::new( - members, - requirements, - constraints, - overrides, - )) + let lock = Lock::from_resolution_graph(&resolution, workspace.install_path())? + .with_manifest(manifest) .with_supported_environments( environments .cloned() diff --git a/crates/uv/tests/branching_urls.rs b/crates/uv/tests/branching_urls.rs index 11697c164d36..63702fb46d57 100644 --- a/crates/uv/tests/branching_urls.rs +++ b/crates/uv/tests/branching_urls.rs @@ -276,14 +276,14 @@ fn root_package_splits_transitive_too() -> Result<()> { [package.metadata] requires-dist = [ - { name = "b1", marker = "python_full_version < '3.12'", directory = "../b1" }, - { name = "b2", marker = "python_full_version >= '3.12'", directory = "../b2" }, + { name = "b1", marker = "python_full_version < '3.12'", directory = "b1" }, + { name = "b2", marker = "python_full_version >= '3.12'", directory = "b2" }, ] [[package]] name = "b1" version = "0.1.0" - source = { directory = "../b1" } + source = { directory = "b1" } dependencies = [ { name = "iniconfig", version = "1.1.1", source = { url = "https://files.pythonhosted.org/packages/9b/dd/b3c12c6d707058fa947864b67f0c4e0c39ef8610988d7baea9578f3c48f3/iniconfig-1.1.1-py2.py3-none-any.whl" }, marker = "python_full_version < '3.12'" }, ] @@ -294,7 +294,7 @@ fn root_package_splits_transitive_too() -> Result<()> { [[package]] name = "b2" version = "0.1.0" - source = { directory = "../b2" } + source = { directory = "b2" } dependencies = [ { name = "iniconfig", version = "2.0.0", source = { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" }, marker = "python_full_version >= '3.12'" }, ] @@ -804,12 +804,12 @@ fn dont_pre_visit_url_packages() -> Result<()> { ] [package.metadata] - requires-dist = [{ name = "c", directory = "../c" }] + requires-dist = [{ name = "c", directory = "c" }] [[package]] name = "c" version = "0.1.0" - source = { directory = "../c" } + source = { directory = "c" } "###); Ok(()) diff --git a/crates/uv/tests/lock.rs b/crates/uv/tests/lock.rs index bdf581a95edd..e35a49de0d8c 100644 --- a/crates/uv/tests/lock.rs +++ b/crates/uv/tests/lock.rs @@ -4159,7 +4159,7 @@ fn lock_relative_and_absolute_paths() -> Result<()> { [package.metadata] requires-dist = [ { name = "b", directory = "b" }, - { name = "c", directory = "[TEMP_DIR]/c" }, + { name = "c", directory = "c" }, ] [[package]] @@ -4170,7 +4170,7 @@ fn lock_relative_and_absolute_paths() -> Result<()> { [[package]] name = "c" version = "0.1.0" - source = { directory = "[TEMP_DIR]/c" } + source = { directory = "c" } "### ); }); @@ -5074,7 +5074,7 @@ fn lock_same_version_multiple_urls() -> Result<()> { [[package]] name = "dependency" version = "0.0.1" - source = { directory = "[TEMP_DIR]/v1" } + source = { directory = "v1" } resolution-markers = [ "sys_platform == 'darwin'", ] @@ -5088,7 +5088,7 @@ fn lock_same_version_multiple_urls() -> Result<()> { [[package]] name = "dependency" version = "0.0.1" - source = { directory = "[TEMP_DIR]/v2" } + source = { directory = "v2" } resolution-markers = [ "sys_platform != 'darwin'", ] @@ -5113,14 +5113,14 @@ fn lock_same_version_multiple_urls() -> Result<()> { version = "0.1.0" source = { editable = "." } dependencies = [ - { name = "dependency", version = "0.0.1", source = { directory = "[TEMP_DIR]/v1" }, marker = "sys_platform == 'darwin'" }, - { name = "dependency", version = "0.0.1", source = { directory = "[TEMP_DIR]/v2" }, marker = "sys_platform != 'darwin'" }, + { name = "dependency", version = "0.0.1", source = { directory = "v1" }, marker = "sys_platform == 'darwin'" }, + { name = "dependency", version = "0.0.1", source = { directory = "v2" }, marker = "sys_platform != 'darwin'" }, ] [package.metadata] requires-dist = [ - { name = "dependency", marker = "sys_platform != 'darwin'", directory = "[TEMP_DIR]/v2" }, - { name = "dependency", marker = "sys_platform == 'darwin'", directory = "[TEMP_DIR]/v1" }, + { name = "dependency", marker = "sys_platform != 'darwin'", directory = "v2" }, + { name = "dependency", marker = "sys_platform == 'darwin'", directory = "v1" }, ] [[package]] @@ -7118,7 +7118,7 @@ fn lock_sources_archive() -> Result<()> { Url::from_file_path(&workspace_archive).unwrap(), })?; - uv_snapshot!(context.filters(), context.lock(), @r###" + uv_snapshot!( context.lock(), @r###" success: true exit_code: 0 ----- stdout ----- @@ -7129,11 +7129,11 @@ fn lock_sources_archive() -> Result<()> { let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); - insta::with_settings!({ - filters => context.filters(), - }, { - assert_snapshot!( - lock, @r###" + // insta::with_settings!({ + // filters => context.filters(), + // }, { + assert_snapshot!( + lock, @r###" version = 1 requires-python = ">=3.12" @@ -7171,7 +7171,7 @@ fn lock_sources_archive() -> Result<()> { ] [package.metadata] - requires-dist = [{ name = "workspace", path = "[TEMP_DIR]/workspace.zip" }] + requires-dist = [{ name = "workspace", path = "workspace.zip" }] [[package]] name = "sniffio" @@ -7185,7 +7185,7 @@ fn lock_sources_archive() -> Result<()> { [[package]] name = "workspace" version = "0.1.0" - source = { path = "[TEMP_DIR]/workspace.zip" } + source = { path = "workspace.zip" } dependencies = [ { name = "anyio" }, ] @@ -7193,8 +7193,8 @@ fn lock_sources_archive() -> Result<()> { [package.metadata] requires-dist = [{ name = "anyio" }] "### - ); - }); + ); + // }); // Re-run with `--locked`. uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" @@ -7293,7 +7293,7 @@ fn lock_sources_source_tree() -> Result<()> { [[package]] name = "anyio" version = "0.1.0" - source = { editable = "[TEMP_DIR]/workspace/anyio" } + source = { editable = "workspace/anyio" } [[package]] name = "project" @@ -7304,18 +7304,18 @@ fn lock_sources_source_tree() -> Result<()> { ] [package.metadata] - requires-dist = [{ name = "workspace", directory = "[TEMP_DIR]/workspace" }] + requires-dist = [{ name = "workspace", directory = "workspace" }] [[package]] name = "workspace" version = "0.1.0" - source = { directory = "[TEMP_DIR]/workspace" } + source = { directory = "workspace" } dependencies = [ { name = "anyio" }, ] [package.metadata] - requires-dist = [{ name = "anyio", editable = "[TEMP_DIR]/workspace/anyio" }] + requires-dist = [{ name = "anyio", editable = "workspace/anyio" }] "### ); }); diff --git a/crates/uv/tests/workspace.rs b/crates/uv/tests/workspace.rs index 46d8872c3c7a..b3607331d279 100644 --- a/crates/uv/tests/workspace.rs +++ b/crates/uv/tests/workspace.rs @@ -1497,3 +1497,61 @@ fn workspace_member_name_shadows_dependencies() -> Result<()> { Ok(()) } + +/// Test that path dependencies with path dependencies resolve paths correctly across workspaces. +/// +/// Each package is its own workspace. We put the other projects into a separate directory `libs` so +/// the paths don't line up by accident. +#[test] +fn test_path_hopping() -> Result<()> { + let context = TestContext::new("3.12"); + + // Build the main project ... + let deps = indoc! {r#" + dependencies = ["foo"] + [tool.uv.sources] + foo = { path = "../libs/foo", editable = true } + "#}; + let main_project_dir = context.temp_dir.join("project"); + make_project(&main_project_dir, "project", deps)?; + + // ... that depends on foo ... + let deps = indoc! {r#" + dependencies = ["bar"] + [tool.uv.sources] + bar = { path = "../../libs/bar", editable = true } + "#}; + make_project(&context.temp_dir.join("libs").join("foo"), "foo", deps)?; + + // ... that depends on bar, a stub project. + make_project(&context.temp_dir.join("libs").join("bar"), "bar", "")?; + + uv_snapshot!(context.filters(), context.lock().arg("--preview").current_dir(&main_project_dir), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using Python 3.12.[X] interpreter at: [PYTHON-3.12] + Resolved 3 packages in [TIME] + "### + ); + + let lock: SourceLock = + toml::from_str(&fs_err::read_to_string(main_project_dir.join("uv.lock"))?)?; + assert_json_snapshot!(lock.sources(), @r###" + { + "bar": { + "editable": "../libs/bar" + }, + "foo": { + "editable": "../libs/foo" + }, + "project": { + "editable": "." + } + } + "###); + + Ok(()) +}