diff --git a/crates/distribution-types/src/buildable.rs b/crates/distribution-types/src/buildable.rs index c693402ec..b6e3bd076 100644 --- a/crates/distribution-types/src/buildable.rs +++ b/crates/distribution-types/src/buildable.rs @@ -158,7 +158,7 @@ impl<'a> From<&'a PathSourceDist> for PathSourceUrl<'a> { fn from(dist: &'a PathSourceDist) -> Self { Self { url: &dist.url, - path: Cow::Borrowed(&dist.path), + path: Cow::Borrowed(&dist.install_path), } } } @@ -180,7 +180,7 @@ impl<'a> From<&'a DirectorySourceDist> for DirectorySourceUrl<'a> { fn from(dist: &'a DirectorySourceDist) -> Self { Self { url: &dist.url, - path: Cow::Borrowed(&dist.path), + path: Cow::Borrowed(&dist.install_path), editable: dist.editable, } } diff --git a/crates/distribution-types/src/cached.rs b/crates/distribution-types/src/cached.rs index 2d8264a2f..7193bf593 100644 --- a/crates/distribution-types/src/cached.rs +++ b/crates/distribution-types/src/cached.rs @@ -116,12 +116,14 @@ impl CachedDist { Self::Url(dist) => { if dist.editable { assert_eq!(dist.url.scheme(), "file", "{}", dist.url); + let path = dist + .url + .to_file_path() + .map_err(|()| anyhow!("Invalid path in file URL"))?; Ok(Some(ParsedUrl::Path(ParsedPathUrl { url: dist.url.raw().clone(), - path: dist - .url - .to_file_path() - .map_err(|()| anyhow!("Invalid path in file URL"))?, + install_path: path.clone(), + lock_path: path, editable: dist.editable, }))) } else { diff --git a/crates/distribution-types/src/lib.rs b/crates/distribution-types/src/lib.rs index eea24da61..87b0542a3 100644 --- a/crates/distribution-types/src/lib.rs +++ b/crates/distribution-types/src/lib.rs @@ -259,8 +259,12 @@ pub struct GitSourceDist { #[derive(Debug, Clone)] pub struct PathSourceDist { pub name: PackageName, - /// The path to the archive. - pub path: PathBuf, + /// The resolved, 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 URL as it was provided by the user. pub url: VerbatimUrl, } @@ -269,8 +273,12 @@ pub struct PathSourceDist { #[derive(Debug, Clone)] pub struct DirectorySourceDist { pub name: PackageName, - /// The path to the directory. - pub path: PathBuf, + /// The resolved, 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. @@ -319,11 +327,12 @@ impl Dist { pub fn from_file_url( name: PackageName, url: VerbatimUrl, - path: &Path, + install_path: &Path, + lock_path: &Path, editable: bool, ) -> Result { // Store the canonicalized path, which also serves to validate that it exists. - let path = match path.canonicalize() { + let canonicalized_path = match install_path.canonicalize() { Ok(path) => path, Err(err) if err.kind() == std::io::ErrorKind::NotFound => { return Err(Error::NotFound(url.to_url())); @@ -332,14 +341,15 @@ impl Dist { }; // Determine whether the path represents an archive or a directory. - if path.is_dir() { + if canonicalized_path.is_dir() { Ok(Self::Source(SourceDist::Directory(DirectorySourceDist { name, - path, + install_path: canonicalized_path.clone(), + lock_path: lock_path.to_path_buf(), editable, url, }))) - } else if path + } else if canonicalized_path .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case("whl")) { @@ -359,7 +369,7 @@ impl Dist { Ok(Self::Built(BuiltDist::Path(PathBuiltDist { filename, - path, + path: canonicalized_path, url, }))) } else { @@ -369,7 +379,8 @@ impl Dist { Ok(Self::Source(SourceDist::Path(PathSourceDist { name, - path, + install_path: canonicalized_path.clone(), + lock_path: canonicalized_path, url, }))) } @@ -396,9 +407,13 @@ impl Dist { ParsedUrl::Archive(archive) => { Self::from_http_url(name, url.verbatim, archive.url, archive.subdirectory) } - ParsedUrl::Path(file) => { - Self::from_file_url(name, url.verbatim, &file.path, file.editable) - } + ParsedUrl::Path(file) => Self::from_file_url( + name, + url.verbatim, + &file.install_path, + &file.lock_path, + file.editable, + ), ParsedUrl::Git(git) => { Self::from_git_url(name, url.verbatim, git.url, git.subdirectory) } @@ -517,8 +532,8 @@ impl SourceDist { /// Returns the path to the source distribution, if it's a local distribution. pub fn as_path(&self) -> Option<&Path> { match self { - Self::Path(dist) => Some(&dist.path), - Self::Directory(dist) => Some(&dist.path), + Self::Path(dist) => Some(&dist.install_path), + Self::Directory(dist) => Some(&dist.install_path), _ => None, } } diff --git a/crates/distribution-types/src/resolution.rs b/crates/distribution-types/src/resolution.rs index 991fd7be5..bb7f15d40 100644 --- a/crates/distribution-types/src/resolution.rs +++ b/crates/distribution-types/src/resolution.rs @@ -141,7 +141,8 @@ impl From<&ResolvedDist> for Requirement { } } Dist::Built(BuiltDist::Path(wheel)) => RequirementSource::Path { - path: wheel.path.clone(), + install_path: wheel.path.clone(), + lock_path: wheel.path.clone(), url: wheel.url.clone(), editable: false, }, @@ -168,12 +169,14 @@ impl From<&ResolvedDist> for Requirement { subdirectory: sdist.subdirectory.clone(), }, Dist::Source(SourceDist::Path(sdist)) => RequirementSource::Path { - path: sdist.path.clone(), + install_path: sdist.install_path.clone(), + lock_path: sdist.lock_path.clone(), url: sdist.url.clone(), editable: false, }, Dist::Source(SourceDist::Directory(sdist)) => RequirementSource::Path { - path: sdist.path.clone(), + 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 044fbaa19..56ccd463a 100644 --- a/crates/pypi-types/src/parsed_url.rs +++ b/crates/pypi-types/src/parsed_url.rs @@ -59,8 +59,9 @@ impl UnnamedRequirementUrl for VerbatimParsedUrl { ) -> Result { let verbatim = VerbatimUrl::parse_path(&path, &working_dir)?; let parsed_path_url = ParsedPathUrl { - path: verbatim.as_path()?, url: verbatim.to_url(), + install_path: verbatim.as_path()?, + lock_path: path.as_ref().to_path_buf(), editable: false, }; Ok(Self { @@ -72,8 +73,9 @@ impl UnnamedRequirementUrl for VerbatimParsedUrl { fn parse_absolute_path(path: impl AsRef) -> Result { let verbatim = VerbatimUrl::parse_absolute_path(&path)?; let parsed_path_url = ParsedPathUrl { - path: verbatim.as_path()?, url: verbatim.to_url(), + install_path: verbatim.as_path()?, + lock_path: path.as_ref().to_path_buf(), editable: false, }; Ok(Self { @@ -171,16 +173,27 @@ impl ParsedUrl { #[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Hash, Ord)] pub struct ParsedPathUrl { pub url: Url, - pub path: PathBuf, + /// The resolved, 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 ParsedPathUrl { /// Construct a [`ParsedPathUrl`] from a path requirement source. - pub fn from_source(path: PathBuf, editable: bool, url: Url) -> Self { + pub fn from_source( + install_path: PathBuf, + lock_path: PathBuf, + editable: bool, + url: Url, + ) -> Self { Self { url, - path, + install_path, + lock_path, editable, } } @@ -311,7 +324,8 @@ impl TryFrom for ParsedUrl { .map_err(|()| ParsedUrlError::InvalidFileUrl(url.clone()))?; Ok(Self::Path(ParsedPathUrl { url, - path, + install_path: path.clone(), + lock_path: path, editable: false, })) } else { diff --git a/crates/pypi-types/src/requirement.rs b/crates/pypi-types/src/requirement.rs index 1ef94fba4..809dc1d99 100644 --- a/crates/pypi-types/src/requirement.rs +++ b/crates/pypi-types/src/requirement.rs @@ -170,7 +170,12 @@ pub enum RequirementSource { /// `.tag.gz` file) or a source tree (a directory with a pyproject.toml in, or a legacy /// source distribution with only a setup.py but non pyproject.toml in it). Path { - path: PathBuf, + /// The resolved, 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 @@ -185,7 +190,8 @@ impl RequirementSource { pub fn from_parsed_url(parsed_url: ParsedUrl, url: VerbatimUrl) -> Self { match parsed_url { ParsedUrl::Path(local_file) => RequirementSource::Path { - path: local_file.path, + install_path: local_file.install_path.clone(), + lock_path: local_file.lock_path.clone(), url, editable: local_file.editable, }, diff --git a/crates/requirements-txt/src/lib.rs b/crates/requirements-txt/src/lib.rs index ca00632ec..ffd0a4406 100644 --- a/crates/requirements-txt/src/lib.rs +++ b/crates/requirements-txt/src/lib.rs @@ -1843,7 +1843,8 @@ mod test { query: None, fragment: None, }, - path: "/foo/bar", + 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 9cb5dd264..5e826f4c3 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 @@ -21,7 +21,8 @@ RequirementsTxt { query: None, fragment: None, }, - path: "/scripts/packages/black_editable", + install_path: "/scripts/packages/black_editable", + lock_path: "./scripts/packages/black_editable", editable: false, }, ), @@ -70,7 +71,8 @@ RequirementsTxt { query: None, fragment: None, }, - path: "/scripts/packages/black_editable", + install_path: "/scripts/packages/black_editable", + lock_path: "./scripts/packages/black_editable", editable: false, }, ), @@ -123,7 +125,8 @@ RequirementsTxt { query: None, fragment: None, }, - path: "/scripts/packages/black_editable", + 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 de4536bb9..6b13c153b 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 @@ -23,7 +23,8 @@ RequirementsTxt { query: None, fragment: None, }, - path: "/editable", + install_path: "/editable", + lock_path: "./editable", editable: true, }, ), @@ -79,7 +80,8 @@ RequirementsTxt { query: None, fragment: None, }, - path: "/editable", + install_path: "/editable", + lock_path: "./editable", editable: true, }, ), @@ -135,7 +137,8 @@ RequirementsTxt { query: None, fragment: None, }, - path: "/editable", + install_path: "/editable", + lock_path: "./editable", editable: true, }, ), @@ -212,7 +215,8 @@ RequirementsTxt { query: None, fragment: None, }, - path: "/editable", + install_path: "/editable", + lock_path: "./editable", editable: true, }, ), @@ -289,7 +293,8 @@ RequirementsTxt { query: None, fragment: None, }, - path: "/editable", + install_path: "/editable", + lock_path: "./editable", editable: true, }, ), @@ -359,7 +364,8 @@ RequirementsTxt { query: None, fragment: None, }, - path: "/editable[d", + 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 9050369c1..53ff4c233 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 @@ -21,7 +21,8 @@ RequirementsTxt { query: None, fragment: None, }, - path: "/scripts/packages/black_editable", + install_path: "/scripts/packages/black_editable", + lock_path: "./scripts/packages/black_editable", editable: false, }, ), @@ -70,7 +71,8 @@ RequirementsTxt { query: None, fragment: None, }, - path: "/scripts/packages/black_editable", + install_path: "/scripts/packages/black_editable", + lock_path: "./scripts/packages/black_editable", editable: false, }, ), @@ -123,7 +125,8 @@ RequirementsTxt { query: None, fragment: None, }, - path: "/scripts/packages/black_editable", + 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 07f40e8d5..1ba08e3be 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 @@ -23,7 +23,8 @@ RequirementsTxt { query: None, fragment: None, }, - path: "/editable", + install_path: "/editable", + lock_path: "./editable", editable: true, }, ), @@ -79,7 +80,8 @@ RequirementsTxt { query: None, fragment: None, }, - path: "/editable", + install_path: "/editable", + lock_path: "./editable", editable: true, }, ), @@ -135,7 +137,8 @@ RequirementsTxt { query: None, fragment: None, }, - path: "/editable", + install_path: "/editable", + lock_path: "./editable", editable: true, }, ), @@ -212,7 +215,8 @@ RequirementsTxt { query: None, fragment: None, }, - path: "/editable", + install_path: "/editable", + lock_path: "./editable", editable: true, }, ), @@ -289,7 +293,8 @@ RequirementsTxt { query: None, fragment: None, }, - path: "/editable", + install_path: "/editable", + lock_path: "./editable", editable: true, }, ), @@ -359,7 +364,8 @@ RequirementsTxt { query: None, fragment: None, }, - path: "/editable[d", + install_path: "/editable[d", + lock_path: "./editable[d", editable: true, }, ), diff --git a/crates/uv-distribution/src/index/built_wheel_index.rs b/crates/uv-distribution/src/index/built_wheel_index.rs index 5927929c0..1d79e7f74 100644 --- a/crates/uv-distribution/src/index/built_wheel_index.rs +++ b/crates/uv-distribution/src/index/built_wheel_index.rs @@ -68,7 +68,8 @@ impl<'a> BuiltWheelIndex<'a> { }; // Determine the last-modified time of the source distribution. - let modified = ArchiveTimestamp::from_file(&source_dist.path).map_err(Error::CacheRead)?; + let modified = + ArchiveTimestamp::from_file(&source_dist.install_path).map_err(Error::CacheRead)?; // If the distribution is stale, omit it from the index. if !pointer.is_up_to_date(modified) { @@ -106,10 +107,12 @@ impl<'a> BuiltWheelIndex<'a> { }; // Determine the last-modified time of the source distribution. - let Some(modified) = - ArchiveTimestamp::from_source_tree(&source_dist.path).map_err(Error::CacheRead)? + let Some(modified) = ArchiveTimestamp::from_source_tree(&source_dist.install_path) + .map_err(Error::CacheRead)? else { - return Err(Error::DirWithoutEntrypoint(source_dist.path.clone())); + return Err(Error::DirWithoutEntrypoint( + source_dist.install_path.clone(), + )); }; // If the distribution is stale, omit it from the index. diff --git a/crates/uv-distribution/src/metadata/lowering.rs b/crates/uv-distribution/src/metadata/lowering.rs index 8ec10f05e..3ac3d4cce 100644 --- a/crates/uv-distribution/src/metadata/lowering.rs +++ b/crates/uv-distribution/src/metadata/lowering.rs @@ -165,7 +165,12 @@ pub(crate) fn lower_requirement( if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) { return Err(LoweringError::ConflictingUrls); } - path_source(path, project_dir, editable.unwrap_or(false))? + path_source( + path, + project_dir, + workspace.root(), + editable.unwrap_or(false), + )? } Source::Registry { index } => match requirement.version_or_url { None => { @@ -199,7 +204,12 @@ pub(crate) fn lower_requirement( .get(&requirement.name) .ok_or(LoweringError::UndeclaredWorkspacePackage)? .clone(); - path_source(path.root(), workspace.root(), editable.unwrap_or(true))? + path_source( + path.root(), + workspace.root(), + workspace.root(), + editable.unwrap_or(true), + )? } Source::CatchAll { .. } => { // Emit a dedicated error message, which is an improvement over Serde's default error. @@ -219,6 +229,7 @@ pub(crate) fn lower_requirement( fn path_source( path: impl AsRef, project_dir: &Path, + workspace_root: &Path, editable: bool, ) -> Result { let url = VerbatimUrl::parse_path(path.as_ref(), project_dir)? @@ -228,8 +239,12 @@ fn path_source( .absolutize_from(project_dir) .map_err(|err| LoweringError::Absolutize(path.as_ref().to_path_buf(), err))? .to_path_buf(); + let ascend_to_workspace = project_dir + .strip_prefix(workspace_root) + .expect("Project must be below workspace root"); Ok(RequirementSource::Path { - path: path_buf, + install_path: path_buf, + lock_path: ascend_to_workspace.join(project_dir), url, editable, }) diff --git a/crates/uv-distribution/src/workspace.rs b/crates/uv-distribution/src/workspace.rs index b7a17b999..5689f5bad 100644 --- a/crates/uv-distribution/src/workspace.rs +++ b/crates/uv-distribution/src/workspace.rs @@ -177,7 +177,12 @@ impl Workspace { extras, marker: None, source: RequirementSource::Path { - path: member.root.clone(), + install_path: member.root.clone(), + lock_path: member + .root + .strip_prefix(&self.root) + .expect("Project must be below workspace root") + .to_path_buf(), editable: true, url, }, diff --git a/crates/uv-installer/src/plan.rs b/crates/uv-installer/src/plan.rs index 6cc3d467c..322e0b99b 100644 --- a/crates/uv-installer/src/plan.rs +++ b/crates/uv-installer/src/plan.rs @@ -272,13 +272,14 @@ impl<'a> Planner<'a> { continue; } } - RequirementSource::Path { url, editable, .. } => { + RequirementSource::Path { + url, + editable, + install_path, + lock_path, + } => { // Store the canonicalized path, which also serves to validate that it exists. - let path = match url - .to_file_path() - .map_err(|()| Error::MissingFilePath(url.to_url()))? - .canonicalize() - { + let path = match install_path.canonicalize() { Ok(path) => path, Err(err) if err.kind() == std::io::ErrorKind::NotFound => { return Err(Error::NotFound(url.to_url()).into()); @@ -291,7 +292,8 @@ impl<'a> Planner<'a> { let sdist = DirectorySourceDist { name: requirement.name.clone(), url: url.clone(), - path, + install_path: path, + lock_path: lock_path.clone(), editable: *editable, }; @@ -369,7 +371,8 @@ impl<'a> Planner<'a> { let sdist = PathSourceDist { name: requirement.name.clone(), url: url.clone(), - path, + install_path: path, + lock_path: lock_path.clone(), }; // Find the most-compatible wheel from the cache, since we don't know diff --git a/crates/uv-installer/src/satisfies.rs b/crates/uv-installer/src/satisfies.rs index c2395eba7..5fa3aac08 100644 --- a/crates/uv-installer/src/satisfies.rs +++ b/crates/uv-installer/src/satisfies.rs @@ -149,9 +149,10 @@ impl RequirementSatisfaction { Ok(Self::Satisfied) } RequirementSource::Path { - url: _, - path: requested_path, + install_path: requested_path, + lock_path: _, editable: requested_editable, + url: _, } => { let InstalledDist::Url(InstalledDirectUrlDist { direct_url, .. }) = &distribution else { diff --git a/crates/uv-requirements/src/lookahead.rs b/crates/uv-requirements/src/lookahead.rs index 4889d2073..604b58147 100644 --- a/crates/uv-requirements/src/lookahead.rs +++ b/crates/uv-requirements/src/lookahead.rs @@ -254,9 +254,16 @@ fn required_dist(requirement: &Requirement) -> Result, distribution })) } RequirementSource::Path { - path, + install_path, + lock_path, url, editable, - } => Dist::from_file_url(requirement.name.clone(), url.clone(), path, *editable)?, + } => Dist::from_file_url( + requirement.name.clone(), + url.clone(), + install_path, + lock_path, + *editable, + )?, })) } diff --git a/crates/uv-requirements/src/unnamed.rs b/crates/uv-requirements/src/unnamed.rs index 16d16b649..74cf0d0de 100644 --- a/crates/uv-requirements/src/unnamed.rs +++ b/crates/uv-requirements/src/unnamed.rs @@ -139,15 +139,15 @@ impl<'a, Context: BuildContext> NamedRequirementsResolver<'a, Context> { let source = match &requirement.url.parsed_url { // If the path points to a directory, attempt to read the name from static metadata. - ParsedUrl::Path(parsed_path_url) if parsed_path_url.path.is_dir() => { + ParsedUrl::Path(parsed_path_url) if parsed_path_url.install_path.is_dir() => { // Attempt to read a `PKG-INFO` from the directory. - if let Some(metadata) = fs_err::read(parsed_path_url.path.join("PKG-INFO")) + if let Some(metadata) = fs_err::read(parsed_path_url.install_path.join("PKG-INFO")) .ok() .and_then(|contents| Metadata10::parse_pkg_info(&contents).ok()) { debug!( "Found PKG-INFO metadata for {path} ({name})", - path = parsed_path_url.path.display(), + path = parsed_path_url.install_path.display(), name = metadata.name ); return Ok(pep508_rs::Requirement { @@ -160,7 +160,7 @@ impl<'a, Context: BuildContext> NamedRequirementsResolver<'a, Context> { } // Attempt to read a `pyproject.toml` file. - let project_path = parsed_path_url.path.join("pyproject.toml"); + let project_path = parsed_path_url.install_path.join("pyproject.toml"); if let Some(pyproject) = fs_err::read_to_string(project_path) .ok() .and_then(|contents| toml::from_str::(&contents).ok()) @@ -169,7 +169,7 @@ impl<'a, Context: BuildContext> NamedRequirementsResolver<'a, Context> { if let Some(project) = pyproject.project { debug!( "Found PEP 621 metadata for {path} in `pyproject.toml` ({name})", - path = parsed_path_url.path.display(), + path = parsed_path_url.install_path.display(), name = project.name ); return Ok(pep508_rs::Requirement { @@ -187,7 +187,7 @@ impl<'a, Context: BuildContext> NamedRequirementsResolver<'a, Context> { if let Some(name) = poetry.name { debug!( "Found Poetry metadata for {path} in `pyproject.toml` ({name})", - path = parsed_path_url.path.display(), + path = parsed_path_url.install_path.display(), name = name ); return Ok(pep508_rs::Requirement { @@ -204,7 +204,7 @@ impl<'a, Context: BuildContext> NamedRequirementsResolver<'a, Context> { // Attempt to read a `setup.cfg` from the directory. if let Some(setup_cfg) = - fs_err::read_to_string(parsed_path_url.path.join("setup.cfg")) + fs_err::read_to_string(parsed_path_url.install_path.join("setup.cfg")) .ok() .and_then(|contents| { let mut ini = Ini::new_cs(); @@ -217,7 +217,7 @@ impl<'a, Context: BuildContext> NamedRequirementsResolver<'a, Context> { if let Ok(name) = PackageName::from_str(name) { debug!( "Found setuptools metadata for {path} in `setup.cfg` ({name})", - path = parsed_path_url.path.display(), + path = parsed_path_url.install_path.display(), name = name ); return Ok(pep508_rs::Requirement { @@ -234,14 +234,14 @@ impl<'a, Context: BuildContext> NamedRequirementsResolver<'a, Context> { SourceUrl::Directory(DirectorySourceUrl { url: &requirement.url.verbatim, - path: Cow::Borrowed(&parsed_path_url.path), + path: Cow::Borrowed(&parsed_path_url.install_path), editable: parsed_path_url.editable, }) } // If it's not a directory, assume it's a file. ParsedUrl::Path(parsed_path_url) => SourceUrl::Path(PathSourceUrl { url: &requirement.url.verbatim, - path: Cow::Borrowed(&parsed_path_url.path), + path: Cow::Borrowed(&parsed_path_url.install_path), }), ParsedUrl::Archive(parsed_archive_url) => SourceUrl::Direct(DirectSourceUrl { url: &parsed_archive_url.url, diff --git a/crates/uv-resolver/src/lock.rs b/crates/uv-resolver/src/lock.rs index 236ce41b7..034b87611 100644 --- a/crates/uv-resolver/src/lock.rs +++ b/crates/uv-resolver/src/lock.rs @@ -2,7 +2,9 @@ // as we build out universal locking. #![allow(dead_code, unreachable_code, unused_variables)] +use std::borrow::Cow; use std::collections::{BTreeMap, VecDeque}; +use std::fmt::{Debug, Display}; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -11,6 +13,7 @@ use either::Either; use indexmap::IndexMap; use petgraph::visit::EdgeRef; use rustc_hash::FxHashMap; +use serde::{Deserialize, Deserializer}; use toml_edit::{value, Array, ArrayOfTables, InlineTable, Item, Table, Value}; use url::Url; @@ -22,7 +25,7 @@ use distribution_types::{ RegistrySourceDist, RemoteSource, Resolution, ResolvedDist, ToUrlError, }; use pep440_rs::Version; -use pep508_rs::{MarkerEnvironment, MarkerTree, VerbatimUrl}; +use pep508_rs::{MarkerEnvironment, MarkerTree, VerbatimUrl, VerbatimUrlError}; use platform_tags::{TagCompatibility, TagPriority, Tags}; use pypi_types::{HashDigest, ParsedArchiveUrl, ParsedGitUrl}; use uv_configuration::ExtrasSpecification; @@ -145,6 +148,7 @@ impl Lock { /// Convert the [`Lock`] to a [`Resolution`] using the given marker environment, tags, and root. pub fn to_resolution( &self, + workspace_root: &Path, marker_env: &MarkerEnvironment, tags: &Tags, root_name: &PackageName, @@ -202,7 +206,7 @@ impl Lock { } } let name = dist.id.name.clone(); - let resolved_dist = ResolvedDist::Installable(dist.to_dist(tags)?); + let resolved_dist = ResolvedDist::Installable(dist.to_dist(workspace_root, tags)?); map.insert(name, resolved_dist); } let diagnostics = vec![]; @@ -485,9 +489,9 @@ impl TryFrom for Lock { // Also check that our sources are consistent with whether we have // hashes or not. - let requires_hash = dist.id.source.kind.requires_hash(); + let requires_hash = dist.id.source.requires_hash(); if let Some(ref sdist) = dist.sdist { - if requires_hash != sdist.hash.is_some() { + if requires_hash != sdist.hash().is_some() { return Err(LockErrorKind::Hash { id: dist.id.clone(), artifact_type: "source distribution", @@ -581,14 +585,14 @@ impl Distribution { } /// Convert the [`Distribution`] to a [`Dist`] that can be used in installation. - fn to_dist(&self, tags: &Tags) -> Result { + fn to_dist(&self, workspace_root: &Path, tags: &Tags) -> Result { if let Some(best_wheel_index) = self.find_best_wheel(tags) { - return match &self.id.source.kind { - SourceKind::Registry => { + return match &self.id.source { + Source::Registry(url) => { let wheels = self .wheels .iter() - .map(|wheel| wheel.to_registry_dist(&self.id.source)) + .map(|wheel| wheel.to_registry_dist(url, &self.id.source)) .collect(); let reg_built_dist = RegistryBuiltDist { wheels, @@ -597,41 +601,46 @@ impl Distribution { }; Ok(Dist::Built(BuiltDist::Registry(reg_built_dist))) } - SourceKind::Path => { + Source::Path(path) => { let filename: WheelFilename = self.wheels[best_wheel_index].filename.clone(); let path_dist = PathBuiltDist { filename, - url: VerbatimUrl::from_url(self.id.source.url.clone()), - path: self.id.source.url.to_file_path().unwrap(), + url: VerbatimUrl::from_path(workspace_root.join(path)).map_err(|err| { + LockErrorKind::VerbatimUrl { + id: self.id.clone(), + err, + } + })?, + path: path.clone(), }; let built_dist = BuiltDist::Path(path_dist); Ok(Dist::Built(built_dist)) } - SourceKind::Direct(direct) => { + Source::Direct(url, direct) => { let filename: WheelFilename = self.wheels[best_wheel_index].filename.clone(); let url = Url::from(ParsedArchiveUrl { - url: self.id.source.url.clone(), + url: url.clone(), subdirectory: direct.subdirectory.as_ref().map(PathBuf::from), }); let direct_dist = DirectUrlBuiltDist { filename, - location: self.id.source.url.clone(), + location: url.clone(), url: VerbatimUrl::from_url(url), }; let built_dist = BuiltDist::DirectUrl(direct_dist); Ok(Dist::Built(built_dist)) } - SourceKind::Git(_) => Err(LockErrorKind::InvalidWheelSource { + Source::Git(_, _) => Err(LockErrorKind::InvalidWheelSource { id: self.id.clone(), source_type: "Git", } .into()), - SourceKind::Directory => Err(LockErrorKind::InvalidWheelSource { + Source::Directory(_) => Err(LockErrorKind::InvalidWheelSource { id: self.id.clone(), source_type: "directory", } .into()), - SourceKind::Editable => Err(LockErrorKind::InvalidWheelSource { + Source::Editable(_) => Err(LockErrorKind::InvalidWheelSource { id: self.id.clone(), source_type: "editable", } @@ -640,43 +649,59 @@ impl Distribution { } if let Some(sdist) = &self.sdist { - return match &self.id.source.kind { - SourceKind::Path => { + return match &self.id.source { + Source::Path(path) => { let path_dist = PathSourceDist { name: self.id.name.clone(), - url: VerbatimUrl::from_url(self.id.source.url.clone()), - path: self.id.source.url.to_file_path().unwrap(), + url: VerbatimUrl::from_path(workspace_root.join(path)).map_err(|err| { + LockErrorKind::VerbatimUrl { + id: self.id.clone(), + err, + } + })?, + install_path: workspace_root.join(path), + lock_path: path.clone(), }; let source_dist = distribution_types::SourceDist::Path(path_dist); Ok(Dist::Source(source_dist)) } - SourceKind::Directory => { + Source::Directory(path) => { let dir_dist = DirectorySourceDist { name: self.id.name.clone(), - url: VerbatimUrl::from_url(self.id.source.url.clone()), - path: self.id.source.url.to_file_path().unwrap(), + url: VerbatimUrl::from_path(workspace_root.join(path)).map_err(|err| { + LockErrorKind::VerbatimUrl { + id: self.id.clone(), + err, + } + })?, + install_path: workspace_root.join(path), + lock_path: path.clone(), editable: false, }; let source_dist = distribution_types::SourceDist::Directory(dir_dist); Ok(Dist::Source(source_dist)) } - SourceKind::Editable => { + Source::Editable(path) => { let dir_dist = DirectorySourceDist { name: self.id.name.clone(), - url: VerbatimUrl::from_url(self.id.source.url.clone()), - path: self.id.source.url.to_file_path().unwrap(), + url: VerbatimUrl::from_path(workspace_root.join(path)).map_err(|err| { + LockErrorKind::VerbatimUrl { + id: self.id.clone(), + err, + } + })?, + install_path: workspace_root.join(path), + lock_path: path.clone(), editable: true, }; let source_dist = distribution_types::SourceDist::Directory(dir_dist); Ok(Dist::Source(source_dist)) } - SourceKind::Git(git) => { + Source::Git(url, git) => { // Reconstruct the `GitUrl` from the `GitSource`. - let git_url = uv_git::GitUrl::new( - self.id.source.url.clone(), - GitReference::from(git.kind.clone()), - ) - .with_precise(git.precise); + let git_url = + uv_git::GitUrl::new(url.clone(), GitReference::from(git.kind.clone())) + .with_precise(git.precise); // Reconstruct the PEP 508-compatible URL from the `GitSource`. let url = Url::from(ParsedGitUrl { @@ -693,32 +718,32 @@ impl Distribution { let source_dist = distribution_types::SourceDist::Git(git_dist); Ok(Dist::Source(source_dist)) } - SourceKind::Direct(direct) => { + Source::Direct(url, direct) => { let url = Url::from(ParsedArchiveUrl { - url: self.id.source.url.clone(), + url: url.clone(), subdirectory: direct.subdirectory.as_ref().map(PathBuf::from), }); let direct_dist = DirectUrlSourceDist { name: self.id.name.clone(), - location: self.id.source.url.clone(), + location: url.clone(), subdirectory: direct.subdirectory.as_ref().map(PathBuf::from), url: VerbatimUrl::from_url(url), }; let source_dist = distribution_types::SourceDist::DirectUrl(direct_dist); Ok(Dist::Source(source_dist)) } - SourceKind::Registry => { + Source::Registry(url) => { let file = Box::new(distribution_types::File { dist_info_metadata: false, - filename: sdist.url.filename().unwrap().to_string(), + filename: sdist.filename().unwrap().to_string(), hashes: vec![], requires_python: None, - size: sdist.size, + size: sdist.size(), upload_time_utc_ms: None, - url: FileLocation::AbsoluteUrl(sdist.url.to_string()), + url: FileLocation::AbsoluteUrl(url.to_string()), yanked: None, }); - let index = IndexUrl::Url(VerbatimUrl::from_url(self.id.source.url.clone())); + let index = IndexUrl::Url(VerbatimUrl::from_url(url.clone())); let reg_dist = RegistrySourceDist { name: self.id.name.clone(), version: self.id.version.clone(), @@ -765,10 +790,10 @@ impl Distribution { /// Returns the [`ResolvedRepositoryReference`] for the distribution, if it is a Git source. pub fn as_git_ref(&self) -> Option { - match &self.id.source.kind { - SourceKind::Git(git) => Some(ResolvedRepositoryReference { + match &self.id.source { + Source::Git(url, git) => Some(ResolvedRepositoryReference { reference: RepositoryReference { - url: RepositoryUrl::new(&self.id.source.url), + url: RepositoryUrl::new(url), reference: GitReference::from(git.kind.clone()), }, sha: git.precise, @@ -804,10 +829,28 @@ impl std::fmt::Display for DistributionId { } } +/// NOTE: Care should be taken when adding variants to this enum. Namely, new +/// variants should be added without changing the relative ordering of other +/// variants. Otherwise, this could cause the lock file to have a different +/// canonical ordering of distributions. #[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] -struct Source { - kind: SourceKind, - url: Url, +enum Source { + Registry(Url), + Git(Url, GitSource), + Direct(Url, DirectSource), + Path(PathBuf), + Directory(PathBuf), + Editable(PathBuf), +} + +/// A [`PathBuf`], but we show `.` instead of an empty path. +fn serialize_path_with_dot(path: &Path) -> Cow { + let path = path.to_string_lossy(); + if path.is_empty() { + Cow::Borrowed(".") + } else { + path + } } impl Source { @@ -862,70 +905,58 @@ impl Source { } fn from_direct_built_dist(direct_dist: &DirectUrlBuiltDist) -> Source { - Source { - kind: SourceKind::Direct(DirectSource { subdirectory: None }), - url: direct_dist.url.to_url(), - } + Source::Direct( + direct_dist.url.to_url(), + DirectSource { subdirectory: None }, + ) } fn from_direct_source_dist(direct_dist: &DirectUrlSourceDist) -> Source { - Source { - kind: SourceKind::Direct(DirectSource { + Source::Direct( + direct_dist.url.to_url(), + DirectSource { subdirectory: direct_dist .subdirectory .as_deref() .and_then(Path::to_str) .map(ToString::to_string), - }), - url: direct_dist.url.to_url(), - } + }, + ) } fn from_path_built_dist(path_dist: &PathBuiltDist) -> Source { - Source { - kind: SourceKind::Path, - url: path_dist.url.to_url(), - } + Source::Path(path_dist.path.clone()) } fn from_path_source_dist(path_dist: &PathSourceDist) -> Source { - Source { - kind: SourceKind::Path, - url: path_dist.url.to_url(), - } + Source::Path(path_dist.install_path.clone()) } fn from_directory_source_dist(directory_dist: &DirectorySourceDist) -> Source { - Source { - kind: if directory_dist.editable { - SourceKind::Editable - } else { - SourceKind::Directory - }, - url: directory_dist.url.to_url(), + if directory_dist.editable { + Source::Editable(directory_dist.lock_path.clone()) + } else { + Source::Directory(directory_dist.lock_path.clone()) } } fn from_index_url(index_url: &IndexUrl) -> Source { match *index_url { - IndexUrl::Pypi(ref verbatim_url) => Source { - kind: SourceKind::Registry, - url: verbatim_url.to_url(), - }, - IndexUrl::Url(ref verbatim_url) => Source { - kind: SourceKind::Registry, - url: verbatim_url.to_url(), - }, - IndexUrl::Path(ref verbatim_url) => Source { - kind: SourceKind::Path, - url: verbatim_url.to_url(), - }, + IndexUrl::Pypi(ref verbatim_url) => Source::Registry(verbatim_url.to_url()), + IndexUrl::Url(ref verbatim_url) => Source::Registry(verbatim_url.to_url()), + // TODO(konsti): Retain path on index url without converting to URL. + IndexUrl::Path(ref verbatim_url) => Source::Path( + verbatim_url + .to_file_path() + .expect("Could not convert index url to path"), + ), } } fn from_git_dist(git_dist: &GitSourceDist) -> Source { - Source { - kind: SourceKind::Git(GitSource { + Source::Git( + locked_git_url(git_dist), + GitSource { kind: GitSourceKind::from(git_dist.git.reference().clone()), precise: git_dist.git.precise().expect("precise commit"), subdirectory: git_dist @@ -933,9 +964,8 @@ impl Source { .as_deref() .and_then(Path::to_str) .map(ToString::to_string), - }), - url: locked_git_url(git_dist), - } + }, + ) } } @@ -943,45 +973,45 @@ impl std::str::FromStr for Source { type Err = SourceParseError; fn from_str(s: &str) -> Result { - let (kind, url) = s.split_once('+').ok_or_else(|| SourceParseError::NoPlus { + let (kind, url_or_path) = s.split_once('+').ok_or_else(|| SourceParseError::NoPlus { given: s.to_string(), })?; - let mut url = Url::parse(url).map_err(|err| SourceParseError::InvalidUrl { - given: s.to_string(), - err, - })?; match kind { - "registry" => Ok(Source { - kind: SourceKind::Registry, - url, - }), - "git" => Ok(Source { - kind: SourceKind::Git(GitSource::from_url(&mut url).map_err(|err| match err { + "registry" => { + let url = Url::parse(url_or_path).map_err(|err| SourceParseError::InvalidUrl { + given: s.to_string(), + err, + })?; + Ok(Source::Registry(url)) + } + "git" => { + let mut url = + Url::parse(url_or_path).map_err(|err| SourceParseError::InvalidUrl { + given: s.to_string(), + err, + })?; + let git_source = GitSource::from_url(&mut url).map_err(|err| match err { GitSourceError::InvalidSha => SourceParseError::InvalidSha { given: s.to_string(), }, GitSourceError::MissingSha => SourceParseError::MissingSha { given: s.to_string(), }, - })?), - url, - }), - "direct" => Ok(Source { - kind: SourceKind::Direct(DirectSource::from_url(&mut url)), - url, - }), - "path" => Ok(Source { - kind: SourceKind::Path, - url, - }), - "directory" => Ok(Source { - kind: SourceKind::Directory, - url, - }), - "editable" => Ok(Source { - kind: SourceKind::Editable, - url, - }), + })?; + Ok(Source::Git(url, git_source)) + } + "direct" => { + let mut url = + Url::parse(url_or_path).map_err(|err| SourceParseError::InvalidUrl { + given: s.to_string(), + err, + })?; + let direct_source = DirectSource::from_url(&mut url); + Ok(Source::Direct(url, direct_source)) + } + "path" => Ok(Source::Path(PathBuf::from(url_or_path))), + "directory" => Ok(Source::Directory(PathBuf::from(url_or_path))), + "editable" => Ok(Source::Editable(PathBuf::from(url_or_path))), name => Err(SourceParseError::UnrecognizedSourceName { given: s.to_string(), name: name.to_string(), @@ -992,7 +1022,14 @@ impl std::str::FromStr for Source { impl std::fmt::Display for Source { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}+{}", self.kind.name(), self.url) + match self { + Source::Registry(url) | Source::Git(url, _) | Source::Direct(url, _) => { + write!(f, "{}+{}", self.name(), url) + } + Source::Path(path) | Source::Directory(path) | Source::Editable(path) => { + write!(f, "{}+{}", self.name(), serialize_path_with_dot(path)) + } + } } } @@ -1006,29 +1043,15 @@ impl<'de> serde::Deserialize<'de> for Source { } } -/// NOTE: Care should be taken when adding variants to this enum. Namely, new -/// variants should be added without changing the relative ordering of other -/// variants. Otherwise, this could cause the lock file to have a different -/// canonical ordering of distributions. -#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] -enum SourceKind { - Registry, - Git(GitSource), - Direct(DirectSource), - Path, - Directory, - Editable, -} - -impl SourceKind { +impl Source { fn name(&self) -> &str { match *self { - SourceKind::Registry => "registry", - SourceKind::Git(_) => "git", - SourceKind::Direct(_) => "direct", - SourceKind::Path => "path", - SourceKind::Directory => "directory", - SourceKind::Editable => "editable", + Self::Registry(..) => "registry", + Self::Git(..) => "git", + Self::Direct(..) => "direct", + Self::Path(..) => "path", + Self::Directory(..) => "directory", + Self::Editable(..) => "editable", } } @@ -1038,8 +1061,8 @@ impl SourceKind { /// _not_ be present. fn requires_hash(&self) -> bool { match *self { - SourceKind::Registry | SourceKind::Direct(_) | SourceKind::Path => true, - SourceKind::Git(_) | SourceKind::Directory | SourceKind::Editable => false, + Self::Registry(..) | Self::Direct(..) | Self::Path(..) => true, + Self::Git(..) | Self::Directory(..) | Self::Editable(..) => false, } } } @@ -1127,12 +1150,7 @@ enum GitSourceKind { /// Inspired by: #[derive(Clone, Debug, serde::Deserialize)] -struct SourceDist { - /// A URL or file path (via `file://`) where the source dist that was - /// locked against was found. The location does not need to exist in the - /// future, so this should be treated as only a hint to where to look - /// and/or recording where the source dist file originally came from. - url: Url, +struct SourceDistMetadata { /// A hash of the source distribution. /// /// This is only present for source distributions that come from registries @@ -1145,15 +1163,79 @@ struct SourceDist { size: Option, } +/// A URL or file path where the source dist that was +/// locked against was found. The location does not need to exist in the +/// future, so this should be treated as only a hint to where to look +/// and/or recording where the source dist file originally came from. +#[derive(Clone, Debug, serde::Deserialize)] +#[serde(untagged)] +enum SourceDist { + Url { + url: Url, + #[serde(flatten)] + metadata: SourceDistMetadata, + }, + Path { + #[serde(deserialize_with = "deserialize_path_with_dot")] + path: PathBuf, + #[serde(flatten)] + metadata: SourceDistMetadata, + }, +} + +/// A [`PathBuf`], but we show `.` instead of an empty path. +fn deserialize_path_with_dot<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let path = String::deserialize(deserializer)?; + if path == "." { + Ok(PathBuf::new()) + } else { + Ok(PathBuf::from(path)) + } +} + +impl SourceDist { + pub(crate) fn filename(&self) -> Option> { + match self { + SourceDist::Url { url, .. } => url.filename().ok(), + SourceDist::Path { path, .. } => { + path.file_name().map(|filename| filename.to_string_lossy()) + } + } + } + + fn hash(&self) -> Option<&Hash> { + match &self { + SourceDist::Url { metadata, .. } => metadata.hash.as_ref(), + SourceDist::Path { metadata, .. } => metadata.hash.as_ref(), + } + } + fn size(&self) -> Option { + match &self { + SourceDist::Url { metadata, .. } => metadata.size, + SourceDist::Path { metadata, .. } => metadata.size, + } + } +} + impl SourceDist { /// Returns the TOML representation of this source distribution. fn to_toml(&self) -> Result { let mut table = InlineTable::new(); - table.insert("url", Value::from(self.url.to_string())); - if let Some(ref hash) = self.hash { + match &self { + SourceDist::Url { url, .. } => { + table.insert("url", Value::from(url.as_str())); + } + SourceDist::Path { path, .. } => { + table.insert("path", Value::from(serialize_path_with_dot(path).as_ref())); + } + } + if let Some(hash) = self.hash() { table.insert("hash", Value::from(hash.to_string())); } - if let Some(size) = self.size { + if let Some(size) = self.size() { table.insert("size", Value::from(i64::try_from(size)?)); } Ok(table) @@ -1219,30 +1301,39 @@ impl SourceDist { .map_err(LockError::from)?; let hash = reg_dist.file.hashes.first().cloned().map(Hash::from); let size = reg_dist.file.size; - Ok(SourceDist { url, hash, size }) + Ok(SourceDist::Url { + url, + metadata: SourceDistMetadata { hash, size }, + }) } fn from_direct_dist(direct_dist: &DirectUrlSourceDist, hashes: &[HashDigest]) -> SourceDist { - SourceDist { + SourceDist::Url { url: direct_dist.url.to_url(), - hash: hashes.first().cloned().map(Hash::from), - size: None, + metadata: SourceDistMetadata { + hash: hashes.first().cloned().map(Hash::from), + size: None, + }, } } fn from_git_dist(git_dist: &GitSourceDist, hashes: &[HashDigest]) -> SourceDist { - SourceDist { + SourceDist::Url { url: locked_git_url(git_dist), - hash: hashes.first().cloned().map(Hash::from), - size: None, + metadata: SourceDistMetadata { + hash: hashes.first().cloned().map(Hash::from), + size: None, + }, } } fn from_path_dist(path_dist: &PathSourceDist, hashes: &[HashDigest]) -> SourceDist { - SourceDist { - url: path_dist.url.to_url(), - hash: hashes.first().cloned().map(Hash::from), - size: None, + SourceDist::Path { + path: path_dist.lock_path.clone(), + metadata: SourceDistMetadata { + hash: hashes.first().cloned().map(Hash::from), + size: None, + }, } } @@ -1250,10 +1341,12 @@ impl SourceDist { directory_dist: &DirectorySourceDist, hashes: &[HashDigest], ) -> SourceDist { - SourceDist { - url: directory_dist.url.to_url(), - hash: hashes.first().cloned().map(Hash::from), - size: None, + SourceDist::Path { + path: directory_dist.lock_path.clone(), + metadata: SourceDistMetadata { + hash: hashes.first().cloned().map(Hash::from), + size: None, + }, } } } @@ -1439,7 +1532,7 @@ impl Wheel { } } - fn to_registry_dist(&self, source: &Source) -> RegistryBuiltWheel { + fn to_registry_dist(&self, url: &Url, source: &Source) -> RegistryBuiltWheel { let filename: WheelFilename = self.filename.clone(); let file = Box::new(distribution_types::File { dist_info_metadata: false, @@ -1451,7 +1544,7 @@ impl Wheel { url: FileLocation::AbsoluteUrl(self.url.to_string()), yanked: None, }); - let index = IndexUrl::Url(VerbatimUrl::from_url(source.url.clone())); + let index = IndexUrl::Url(VerbatimUrl::from_url(url.clone())); RegistryBuiltWheel { filename, file, @@ -1632,7 +1725,7 @@ impl<'de> serde::Deserialize<'de> for Hash { } } -#[derive(Clone, Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error)] #[error(transparent)] pub struct LockError(Box); @@ -1651,7 +1744,7 @@ where /// For example, if there are two or more duplicative distributions given /// to `Lock::new`, then an error is returned. It's likely that the fault /// is with the caller somewhere in such cases. -#[derive(Clone, Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error)] enum LockErrorKind { /// An error that occurs when multiple distributions with the same /// ID were found. @@ -1732,7 +1825,7 @@ enum LockErrorKind { }, /// An error that occurs when a hash is expected (or not) for a particular /// artifact, but one was not found (or was). - #[error("since the distribution `{id}` comes from a {source} dependency, a hash was {expected} but one was not found for {artifact_type}", source = id.source.kind.name(), expected = if *expected { "expected" } else { "not expected" })] + #[error("since the distribution `{id}` comes from a {source} dependency, a hash was {expected} but one was not found for {artifact_type}", source = id.source.name(), expected = if *expected { "expected" } else { "not expected" })] Hash { /// The ID of the distribution that has a missing hash. id: DistributionId, @@ -1777,6 +1870,15 @@ enum LockErrorKind { /// The ID of the distribution that has a missing base. id: DistributionId, }, + /// An error that occurs when converting between URLs and paths. + #[error("found dependency `{id}` with no locked distribution")] + VerbatimUrl { + /// The ID of the distribution that has a missing base. + id: DistributionId, + /// The inner error we forward. + #[source] + err: VerbatimUrlError, + }, } /// An error that occurs when a source string could not be parsed. @@ -1827,7 +1929,7 @@ impl std::error::Error for HashParseError {} impl std::fmt::Display for HashParseError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - self.0.fmt(f) + Display::fmt(self.0, f) } } diff --git a/crates/uv-resolver/src/pubgrub/dependencies.rs b/crates/uv-resolver/src/pubgrub/dependencies.rs index 16ecfef81..475c8d192 100644 --- a/crates/uv-resolver/src/pubgrub/dependencies.rs +++ b/crates/uv-resolver/src/pubgrub/dependencies.rs @@ -334,7 +334,8 @@ impl PubGrubRequirement { RequirementSource::Path { editable, url, - path, + install_path, + lock_path, } => { let Some(expected) = urls.get(&requirement.name) else { return Err(ResolveError::DisallowedUrl( @@ -344,7 +345,8 @@ impl PubGrubRequirement { }; let parsed_url = ParsedUrl::Path(ParsedPathUrl::from_source( - path.clone(), + install_path.clone(), + lock_path.clone(), *editable, url.to_url(), )); diff --git a/crates/uv-resolver/src/resolver/locals.rs b/crates/uv-resolver/src/resolver/locals.rs index 4a2c06c46..bc32f4abe 100644 --- a/crates/uv-resolver/src/resolver/locals.rs +++ b/crates/uv-resolver/src/resolver/locals.rs @@ -177,7 +177,9 @@ fn iter_locals(source: &RequirementSource) -> Box + .filter(pep440_rs::Version::is_local), ), RequirementSource::Git { .. } => Box::new(iter::empty()), - RequirementSource::Path { path, .. } => Box::new( + RequirementSource::Path { + install_path: path, .. + } => Box::new( path.file_name() .and_then(|filename| { let filename = filename.to_string_lossy(); diff --git a/crates/uv-resolver/src/resolver/urls.rs b/crates/uv-resolver/src/resolver/urls.rs index cea0920e4..fa5500494 100644 --- a/crates/uv-resolver/src/resolver/urls.rs +++ b/crates/uv-resolver/src/resolver/urls.rs @@ -53,13 +53,15 @@ impl Urls { } } RequirementSource::Path { - path, + install_path, + lock_path, editable, url, } => { let url = VerbatimParsedUrl { parsed_url: ParsedUrl::Path(ParsedPathUrl::from_source( - path.clone(), + install_path.clone(), + lock_path.clone(), *editable, url.to_url(), )), @@ -140,7 +142,8 @@ impl Urls { a.subdirectory == b.subdirectory && git.same_ref(&a.url, &b.url) } (ParsedUrl::Path(a), ParsedUrl::Path(b)) => { - a.path == b.path || is_same_file(&a.path, &b.path).unwrap_or(false) + a.install_path == b.install_path + || is_same_file(&a.install_path, &b.install_path).unwrap_or(false) } _ => false, } diff --git a/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap index be0ea2b0b..38a0ff753 100644 --- a/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap +++ b/crates/uv-resolver/src/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap @@ -12,20 +12,9 @@ Ok( "anyio", ), version: "4.3.0", - source: Source { - kind: Path, - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/foo/bar", - query: None, - fragment: None, - }, - }, + source: Path( + "file:///foo/bar", + ), }, sdist: None, wheels: [ @@ -80,20 +69,9 @@ Ok( "anyio", ), version: "4.3.0", - source: Source { - kind: Path, - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/foo/bar", - query: None, - fragment: None, - }, - }, + source: Path( + "file:///foo/bar", + ), }: 0, }, }, diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 1ad85469b..e6a8eb7cc 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -91,6 +91,7 @@ pub(crate) async fn run( .await?; project::sync::do_sync( project.project_name(), + project.workspace().root(), &venv, &lock, &index_locations, diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 53185c395..1fab992a4 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use std::path::Path; use distribution_types::IndexLocations; use install_wheel_rs::linker::LinkMode; @@ -60,6 +61,7 @@ pub(crate) async fn sync( // Perform the sync operation. do_sync( project.project_name(), + project.workspace().root(), &venv, &lock, &index_locations, @@ -77,7 +79,8 @@ pub(crate) async fn sync( /// Sync a lockfile with an environment. #[allow(clippy::too_many_arguments)] pub(super) async fn do_sync( - project: &PackageName, + project_name: &PackageName, + workspace_root: &Path, venv: &PythonEnvironment, lock: &Lock, index_locations: &IndexLocations, @@ -108,7 +111,8 @@ pub(super) async fn do_sync( let tags = venv.interpreter().tags()?; // Read the lockfile. - let resolution = lock.to_resolution(markers, tags, project, &extras, &dev)?; + let resolution = + lock.to_resolution(workspace_root, markers, tags, project_name, &extras, &dev)?; // Initialize the registry client. // TODO(zanieb): Support client options e.g. offline, tls, etc. diff --git a/crates/uv/tests/lock.rs b/crates/uv/tests/lock.rs index 6200b1923..5968be859 100644 --- a/crates/uv/tests/lock.rs +++ b/crates/uv/tests/lock.rs @@ -90,8 +90,8 @@ fn lock_wheel_registry() -> Result<()> { [[distribution]] name = "project" version = "0.1.0" - source = "editable+file://[TEMP_DIR]/" - sdist = { url = "file://[TEMP_DIR]/" } + source = "editable+." + sdist = { path = "." } [[distribution.dependencies]] name = "anyio" @@ -173,8 +173,8 @@ fn lock_sdist_registry() -> Result<()> { [[distribution]] name = "project" version = "0.1.0" - source = "editable+file://[TEMP_DIR]/" - sdist = { url = "file://[TEMP_DIR]/" } + source = "editable+." + sdist = { path = "." } [[distribution.dependencies]] name = "source-distribution" @@ -232,8 +232,8 @@ fn lock_sdist_git() -> Result<()> { [[distribution]] name = "project" version = "0.1.0" - source = "editable+file://[TEMP_DIR]/" - sdist = { url = "file://[TEMP_DIR]/" } + source = "editable+." + sdist = { path = "." } [[distribution.dependencies]] name = "uv-public-pypackage" @@ -347,8 +347,8 @@ fn lock_wheel_url() -> Result<()> { [[distribution]] name = "project" version = "0.1.0" - source = "editable+file://[TEMP_DIR]/" - sdist = { url = "file://[TEMP_DIR]/" } + source = "editable+." + sdist = { path = "." } [[distribution.dependencies]] name = "anyio" @@ -472,8 +472,8 @@ fn lock_sdist_url() -> Result<()> { [[distribution]] name = "project" version = "0.1.0" - source = "editable+file://[TEMP_DIR]/" - sdist = { url = "file://[TEMP_DIR]/" } + source = "editable+." + sdist = { path = "." } [[distribution.dependencies]] name = "anyio" @@ -608,8 +608,8 @@ fn lock_project_extra() -> Result<()> { [[distribution]] name = "project" version = "0.1.0" - source = "editable+file://[TEMP_DIR]/" - sdist = { url = "file://[TEMP_DIR]/" } + source = "editable+." + sdist = { path = "." } [[distribution.dependencies]] name = "anyio" @@ -893,8 +893,8 @@ fn lock_dependency_extra() -> Result<()> { [[distribution]] name = "project" version = "0.1.0" - source = "editable+file://[TEMP_DIR]/" - sdist = { url = "file://[TEMP_DIR]/" } + source = "editable+." + sdist = { path = "." } [[distribution.dependencies]] name = "flask" @@ -1116,8 +1116,8 @@ fn lock_conditional_dependency_extra() -> Result<()> { [[distribution]] name = "project" version = "0.1.0" - source = "editable+file://[TEMP_DIR]/" - sdist = { url = "file://[TEMP_DIR]/" } + source = "editable+." + sdist = { path = "." } [[distribution.dependencies]] name = "requests" @@ -1210,7 +1210,7 @@ fn lock_conditional_dependency_extra() -> Result<()> { fs_err::copy(lockfile, context_38.temp_dir.join("uv.lock"))?; // Install from the lockfile. - uv_snapshot!(context.filters(), context_38.sync(), @r###" + uv_snapshot!(context_38.filters(), context_38.sync(), @r###" success: true exit_code: 0 ----- stdout ----- @@ -1277,8 +1277,8 @@ fn lock_preference() -> Result<()> { [[distribution]] name = "project" version = "0.1.0" - source = "editable+file://[TEMP_DIR]/" - sdist = { url = "file://[TEMP_DIR]/" } + source = "editable+." + sdist = { path = "." } [[distribution.dependencies]] name = "iniconfig" @@ -1330,8 +1330,8 @@ fn lock_preference() -> Result<()> { [[distribution]] name = "project" version = "0.1.0" - source = "editable+file://[TEMP_DIR]/" - sdist = { url = "file://[TEMP_DIR]/" } + source = "editable+." + sdist = { path = "." } [[distribution.dependencies]] name = "iniconfig" @@ -1372,8 +1372,8 @@ fn lock_preference() -> Result<()> { [[distribution]] name = "project" version = "0.1.0" - source = "editable+file://[TEMP_DIR]/" - sdist = { url = "file://[TEMP_DIR]/" } + source = "editable+." + sdist = { path = "." } [[distribution.dependencies]] name = "iniconfig" @@ -1426,8 +1426,8 @@ fn lock_git_sha() -> Result<()> { [[distribution]] name = "project" version = "0.1.0" - source = "editable+file://[TEMP_DIR]/" - sdist = { url = "file://[TEMP_DIR]/" } + source = "editable+." + sdist = { path = "." } [[distribution.dependencies]] name = "uv-public-pypackage" @@ -1484,8 +1484,8 @@ fn lock_git_sha() -> Result<()> { [[distribution]] name = "project" version = "0.1.0" - source = "editable+file://[TEMP_DIR]/" - sdist = { url = "file://[TEMP_DIR]/" } + source = "editable+." + sdist = { path = "." } [[distribution.dependencies]] name = "uv-public-pypackage" @@ -1527,8 +1527,8 @@ fn lock_git_sha() -> Result<()> { [[distribution]] name = "project" version = "0.1.0" - source = "editable+file://[TEMP_DIR]/" - sdist = { url = "file://[TEMP_DIR]/" } + source = "editable+." + sdist = { path = "." } [[distribution.dependencies]] name = "uv-public-pypackage" @@ -1704,8 +1704,8 @@ fn lock_requires_python() -> Result<()> { [[distribution]] name = "project" version = "0.1.0" - source = "editable+file://[TEMP_DIR]/" - sdist = { url = "file://[TEMP_DIR]/" } + source = "editable+." + sdist = { path = "." } [[distribution.dependencies]] name = "pygls" @@ -1869,8 +1869,8 @@ fn lock_requires_python() -> Result<()> { [[distribution]] name = "project" version = "0.1.0" - source = "editable+file://[TEMP_DIR]/" - sdist = { url = "file://[TEMP_DIR]/" } + source = "editable+." + sdist = { path = "." } [[distribution.dependencies]] name = "pygls" @@ -2022,8 +2022,8 @@ fn lock_requires_python() -> Result<()> { [[distribution]] name = "project" version = "0.1.0" - source = "editable+file://[TEMP_DIR]/" - sdist = { url = "file://[TEMP_DIR]/" } + source = "editable+." + sdist = { path = "." } [[distribution.dependencies]] name = "pygls" @@ -2224,8 +2224,8 @@ fn lock_requires_python_star() -> Result<()> { [[distribution]] name = "project" version = "0.1.0" - source = "editable+file://[TEMP_DIR]/" - sdist = { url = "file://[TEMP_DIR]/" } + source = "editable+." + sdist = { path = "." } [[distribution.dependencies]] name = "linehaul" @@ -2393,8 +2393,8 @@ fn lock_requires_python_pre() -> Result<()> { [[distribution]] name = "project" version = "0.1.0" - source = "editable+file://[TEMP_DIR]/" - sdist = { url = "file://[TEMP_DIR]/" } + source = "editable+." + sdist = { path = "." } [[distribution.dependencies]] name = "linehaul" @@ -2477,8 +2477,8 @@ fn lock_dev() -> Result<()> { [[distribution]] name = "project" version = "0.1.0" - source = "editable+file://[TEMP_DIR]/" - sdist = { url = "file://[TEMP_DIR]/" } + source = "editable+." + sdist = { path = "." } [[distribution.dependencies]] name = "iniconfig" @@ -2578,8 +2578,8 @@ fn lock_conditional_unconditional() -> Result<()> { [[distribution]] name = "project" version = "0.1.0" - source = "editable+file://[TEMP_DIR]/" - sdist = { url = "file://[TEMP_DIR]/" } + source = "editable+." + sdist = { path = "." } [[distribution.dependencies]] name = "iniconfig" @@ -2638,8 +2638,8 @@ fn lock_multiple_markers() -> Result<()> { [[distribution]] name = "project" version = "0.1.0" - source = "editable+file://[TEMP_DIR]/" - sdist = { url = "file://[TEMP_DIR]/" } + source = "editable+." + sdist = { path = "." } [[distribution.dependencies]] name = "iniconfig" diff --git a/crates/uv/tests/lock_scenarios.rs b/crates/uv/tests/lock_scenarios.rs index d39607099..b839626a0 100644 --- a/crates/uv/tests/lock_scenarios.rs +++ b/crates/uv/tests/lock_scenarios.rs @@ -92,8 +92,8 @@ fn fork_basic() -> Result<()> { [[distribution]] name = "project" version = "0.1.0" - source = "editable+file://[TEMP_DIR]/" - sdist = { url = "file://[TEMP_DIR]/" } + source = "editable+." + sdist = { path = "." } [[distribution.dependencies]] name = "package-a" @@ -220,8 +220,8 @@ fn fork_marker_accrue() -> Result<()> { [[distribution]] name = "project" version = "0.1.0" - source = "editable+file://[TEMP_DIR]/" - sdist = { url = "file://[TEMP_DIR]/" } + source = "editable+." + sdist = { path = "." } [[distribution.dependencies]] name = "package-a" @@ -414,8 +414,8 @@ fn fork_marker_selection() -> Result<()> { [[distribution]] name = "project" version = "0.1.0" - source = "editable+file://[TEMP_DIR]/" - sdist = { url = "file://[TEMP_DIR]/" } + source = "editable+." + sdist = { path = "." } [[distribution.dependencies]] name = "package-a" @@ -578,8 +578,8 @@ fn fork_marker_track() -> Result<()> { [[distribution]] name = "project" version = "0.1.0" - source = "editable+file://[TEMP_DIR]/" - sdist = { url = "file://[TEMP_DIR]/" } + source = "editable+." + sdist = { path = "." } [[distribution.dependencies]] name = "package-a" @@ -715,8 +715,8 @@ fn fork_non_fork_marker_transitive() -> Result<()> { [[distribution]] name = "project" version = "0.1.0" - source = "editable+file://[TEMP_DIR]/" - sdist = { url = "file://[TEMP_DIR]/" } + source = "editable+." + sdist = { path = "." } [[distribution.dependencies]] name = "package-a" diff --git a/crates/uv/tests/workspace.rs b/crates/uv/tests/workspace.rs index 551324663..3fc4486cd 100644 --- a/crates/uv/tests/workspace.rs +++ b/crates/uv/tests/workspace.rs @@ -4,7 +4,6 @@ use std::process::Command; use anyhow::Result; use assert_cmd::assert::OutputAssertExt; -use url::Url; use crate::common::{copy_dir_ignore, get_bin, uv_snapshot, TestContext, EXCLUDE_NEWER}; @@ -529,27 +528,12 @@ fn workspace_lock_idempotence(workspace: &str, subdirectories: &[&str]) -> Resul .assert() .success(); - let raw_lock = fs_err::read_to_string(work_dir.join("uv.lock"))?; - // Remove temp paths from lock. - // TODO(konsti): There shouldn't be absolute paths in the lock to begin with. - let redacted_lock = raw_lock - .replace( - Url::from_directory_path(&context.temp_dir) - .unwrap() - .as_str(), - "file:///tmp", - ) - .replace( - Url::from_directory_path(fs_err::canonicalize(&context.temp_dir)?) - .unwrap() - .as_str(), - "file:///tmp", - ); + let lock = fs_err::read_to_string(work_dir.join("uv.lock"))?; // Check the lockfile is the same for all resolutions. if let Some(shared_lock) = &shared_lock { - assert_eq!(shared_lock, &redacted_lock); + assert_eq!(shared_lock, &lock); } else { - shared_lock = Some(redacted_lock); + shared_lock = Some(lock); } } Ok(())