Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow virtual packages with --no-build #12314

Merged
merged 1 commit into from
Mar 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions crates/uv-distribution-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -637,22 +637,31 @@ impl SourceDist {
}
}

/// Returns the [`Version`] of the distribution, if it is known.
pub fn version(&self) -> Option<&Version> {
match self {
Self::Registry(source_dist) => Some(&source_dist.version),
Self::DirectUrl(_) | Self::Git(_) | Self::Path(_) | Self::Directory(_) => None,
}
}

/// Return true if the distribution is editable.
/// Returns `true` if the distribution is editable.
pub fn is_editable(&self) -> bool {
match self {
Self::Directory(DirectorySourceDist { editable, .. }) => *editable,
_ => false,
}
}

/// Return true if the distribution refers to a local file or directory.
/// Returns `true` if the distribution is virtual.
pub fn is_virtual(&self) -> bool {
match self {
Self::Directory(DirectorySourceDist { r#virtual, .. }) => *r#virtual,
_ => false,
}
}

/// Returns `true` if the distribution refers to a local file or directory.
pub fn is_local(&self) -> bool {
matches!(self, Self::Directory(_) | Self::Path(_))
}
Expand All @@ -666,7 +675,7 @@ impl SourceDist {
}
}

/// Return the source tree of the distribution, if available.
/// Returns the source tree of the distribution, if available.
pub fn source_tree(&self) -> Option<&Path> {
match self {
Self::Directory(dist) => Some(&dist.install_path),
Expand Down
7 changes: 5 additions & 2 deletions crates/uv-resolver/src/lock/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2196,8 +2196,11 @@ impl Package {
};
}

if !no_build {
if let Some(sdist) = self.to_source_dist(workspace_root)? {
if let Some(sdist) = self.to_source_dist(workspace_root)? {
// Even with `--no-build`, allow virtual packages. (In the future, we may want to allow
// any local source tree, or at least editable source trees, which we allow in
// `uv pip`.)
if !no_build || sdist.is_virtual() {
return Ok(Dist::Source(sdist));
}
}
Expand Down
12 changes: 7 additions & 5 deletions crates/uv/tests/it/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22844,14 +22844,16 @@ fn lock_no_build_static_metadata() -> Result<()> {
"###);

// Install from the lockfile.
uv_snapshot!(context.filters(), context.sync().arg("--no-build").arg("--frozen"), @r###"
success: false
exit_code: 2
uv_snapshot!(context.filters(), context.sync().arg("--no-build").arg("--frozen"), @r"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
error: Distribution `dummy==0.1.0 @ virtual+.` can't be installed because it is marked as `--no-build` but has no binary distribution
"###);
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
");

Ok(())
}
Expand Down
139 changes: 133 additions & 6 deletions crates/uv/tests/it/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3482,6 +3482,133 @@ fn no_install_project_no_build() -> Result<()> {
Ok(())
}

#[test]
fn virtual_no_build() -> Result<()> {
let context = TestContext::new("3.12");

let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["anyio==3.7.0"]
"#,
)?;

// Generate a lockfile.
context.lock().assert().success();

// Clear the cache.
fs_err::remove_dir_all(&context.cache_dir)?;

// `--no-build` should not raise an error, since we don't install virtual projects.
uv_snapshot!(context.filters(), context.sync().arg("--no-build"), @r"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Resolved 4 packages in [TIME]
Prepared 3 packages in [TIME]
Installed 3 packages in [TIME]
+ anyio==3.7.0
+ idna==3.6
+ sniffio==1.3.1
");

Ok(())
}

#[test]
fn virtual_no_build_dynamic_cached() -> Result<()> {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super confusing that this test succeeds but the next test fails. The difference is that in this test, the dynamic metadata is cached, so we don't need to run any Python.

let context = TestContext::new("3.12");

let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dynamic = ["dependencies"]

[tool.setuptools.dynamic]
dependencies = {file = ["requirements.txt"]}
"#,
)?;

context
.temp_dir
.child("requirements.txt")
.write_str("anyio==3.7.0")?;

// Generate a lockfile.
context.lock().assert().success();

// `--no-build` should not raise an error, since we don't build or install the project (given
// that it's virtual and the metadata is cached).
uv_snapshot!(context.filters(), context.sync().arg("--no-build"), @r"
success: true
exit_code: 0
----- stdout -----

----- stderr -----
Resolved 4 packages in [TIME]
Prepared 3 packages in [TIME]
Installed 3 packages in [TIME]
+ anyio==3.7.0
+ idna==3.6
+ sniffio==1.3.1
");

Ok(())
}

#[test]
fn virtual_no_build_dynamic_no_cache() -> Result<()> {
let context = TestContext::new("3.12");

let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dynamic = ["dependencies"]

[tool.setuptools.dynamic]
dependencies = {file = ["requirements.txt"]}
"#,
)?;

context
.temp_dir
.child("requirements.txt")
.write_str("anyio==3.7.0")?;

// Generate a lockfile.
context.lock().assert().success();

// Clear the cache.
fs_err::remove_dir_all(&context.cache_dir)?;

// `--no-build` should raise an error, since we need to build the project.
uv_snapshot!(context.filters(), context.sync().arg("--no-build"), @r"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
error: Failed to generate package metadata for `project==0.1.0 @ virtual+.`
Caused by: Building source distributions for `project` is disabled
");

Ok(())
}

/// Convert from a package to a virtual project.
#[test]
fn convert_to_virtual() -> Result<()> {
Expand Down Expand Up @@ -4753,25 +4880,25 @@ fn no_build_error() -> Result<()> {
error: Distribution `django-allauth==0.51.0 @ registry+https://pypi.org/simple` can't be installed because it is marked as `--no-build` but has no binary distribution
"###);

uv_snapshot!(context.filters(), context.sync().arg("--no-build"), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--no-build"), @r"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
Resolved 19 packages in [TIME]
error: Distribution `project==0.1.0 @ virtual+.` can't be installed because it is marked as `--no-build` but has no binary distribution
"###);
error: Distribution `django-allauth==0.51.0 @ registry+https://pypi.org/simple` can't be installed because it is marked as `--no-build` but has no binary distribution
");

uv_snapshot!(context.filters(), context.sync().arg("--reinstall").env("UV_NO_BUILD", "1"), @r###"
uv_snapshot!(context.filters(), context.sync().arg("--reinstall").env("UV_NO_BUILD", "1"), @r"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
Resolved 19 packages in [TIME]
error: Distribution `project==0.1.0 @ virtual+.` can't be installed because it is marked as `--no-build` but has no binary distribution
"###);
error: Distribution `django-allauth==0.51.0 @ registry+https://pypi.org/simple` can't be installed because it is marked as `--no-build` but has no binary distribution
");

uv_snapshot!(context.filters(), context.sync().arg("--reinstall").env("UV_NO_BUILD_PACKAGE", "django-allauth"), @r###"
success: false
Expand Down
Loading