diff --git a/Cargo.lock b/Cargo.lock index e121040f645d..4d59b1a768a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4700,7 +4700,6 @@ dependencies = [ "anyhow", "cache-key", "clap", - "distribution-types", "either", "pep508_rs", "platform-tags", diff --git a/crates/uv-configuration/Cargo.toml b/crates/uv-configuration/Cargo.toml index 843c2afe7400..5c83bb7c3e33 100644 --- a/crates/uv-configuration/Cargo.toml +++ b/crates/uv-configuration/Cargo.toml @@ -14,7 +14,6 @@ workspace = true [dependencies] cache-key = { workspace = true } -distribution-types = { workspace = true } pep508_rs = { workspace = true, features = ["schemars"] } platform-tags = { workspace = true } pypi-types = { workspace = true } diff --git a/crates/uv-configuration/src/install_options.rs b/crates/uv-configuration/src/install_options.rs index fd5c8bd4373e..0de0f411eca0 100644 --- a/crates/uv-configuration/src/install_options.rs +++ b/crates/uv-configuration/src/install_options.rs @@ -1,15 +1,16 @@ use std::collections::BTreeSet; -use rustc_hash::FxHashSet; use tracing::debug; -use distribution_types::{Name, Resolution}; use pep508_rs::PackageName; #[derive(Debug, Clone, Default)] pub struct InstallOptions { + /// Omit the project itself from the resolution. pub no_install_project: bool, + /// Omit all workspace members (including the project itself) from the resolution. pub no_install_workspace: bool, + /// Omit the specified packages from the resolution. pub no_install_package: Vec, } @@ -26,81 +27,48 @@ impl InstallOptions { } } - pub fn filter_resolution( - &self, - resolution: Resolution, - project_name: Option<&PackageName>, - members: &BTreeSet, - ) -> Resolution { - // If `--no-install-project` is set, remove the project itself. - let resolution = self.apply_no_install_project(resolution, project_name); - - // If `--no-install-workspace` is set, remove the project and any workspace members. - let resolution = self.apply_no_install_workspace(resolution, members); - - // If `--no-install-package` is provided, remove the requested packages. - self.apply_no_install_package(resolution) - } - - fn apply_no_install_project( - &self, - resolution: Resolution, - project_name: Option<&PackageName>, - ) -> Resolution { - if !self.no_install_project { - return resolution; - } - - let Some(project_name) = project_name else { - debug!("Ignoring `--no-install-project` for virtual workspace"); - return resolution; - }; - - resolution.filter(|dist| dist.name() != project_name) - } - - fn apply_no_install_workspace( - &self, - resolution: Resolution, - members: &BTreeSet, - ) -> Resolution { - if !self.no_install_workspace { - return resolution; - } - - resolution.filter(|dist| !members.contains(dist.name())) - } - - fn apply_no_install_package(&self, resolution: Resolution) -> Resolution { - if self.no_install_package.is_empty() { - return resolution; - } - - let no_install_packages = self.no_install_package.iter().collect::>(); - - resolution.filter(|dist| !no_install_packages.contains(dist.name())) - } - /// Returns `true` if a package passes the install filters. pub fn include_package( &self, package: &PackageName, - project_name: &PackageName, + project_name: Option<&PackageName>, members: &BTreeSet, ) -> bool { - // If `--no-install-project` is set, remove the project itself. The project is always - // part of the workspace. - if (self.no_install_project || self.no_install_workspace) && package == project_name { - return false; + // If `--no-install-project` is set, remove the project itself. + if self.no_install_project { + if let Some(project_name) = project_name { + if package == project_name { + debug!("Omitting `{package}` from resolution due to `--no-install-project`"); + return false; + } + } } // If `--no-install-workspace` is set, remove the project and any workspace members. - if self.no_install_workspace && members.contains(package) { - return false; + if self.no_install_workspace { + // In some cases, the project root might be omitted from the list of workspace members + // encoded in the lockfile. (But we already checked this above if `--no-install-project` + // is set.) + if !self.no_install_project { + if let Some(project_name) = project_name { + if package == project_name { + debug!( + "Omitting `{package}` from resolution due to `--no-install-workspace`" + ); + return false; + } + } + } + + if members.contains(package) { + debug!("Omitting `{package}` from resolution due to `--no-install-workspace`"); + return false; + } } // If `--no-install-package` is provided, remove the requested packages. if self.no_install_package.contains(package) { + debug!("Omitting `{package}` from resolution due to `--no-install-package`"); return false; } diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index a99b768472cd..9efb6524b169 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -30,7 +30,7 @@ use pypi_types::{ redact_git_credentials, HashDigest, ParsedArchiveUrl, ParsedGitUrl, Requirement, RequirementSource, ResolverMarkerEnvironment, }; -use uv_configuration::{BuildOptions, ExtrasSpecification}; +use uv_configuration::{BuildOptions, ExtrasSpecification, InstallOptions}; use uv_distribution::DistributionDatabase; use uv_fs::{relative_to, PortablePath, PortablePathBuf}; use uv_git::{GitReference, GitSha, RepositoryReference, ResolvedRepositoryReference}; @@ -547,6 +547,7 @@ impl Lock { extras: &ExtrasSpecification, dev: &[GroupName], build_options: &BuildOptions, + install_options: &InstallOptions, ) -> Result { let mut queue: VecDeque<(&Package, Option<&ExtraName>)> = VecDeque::new(); let mut seen = FxHashSet::default(); @@ -633,15 +634,21 @@ impl Lock { } } } - map.insert( - dist.id.name.clone(), - ResolvedDist::Installable(dist.to_dist( - project.workspace().install_path(), - tags, - build_options, - )?), - ); - hashes.insert(dist.id.name.clone(), dist.hashes()); + if install_options.include_package( + &dist.id.name, + project.project_name(), + &self.manifest.members, + ) { + map.insert( + dist.id.name.clone(), + ResolvedDist::Installable(dist.to_dist( + project.workspace().install_path(), + tags, + build_options, + )?), + ); + hashes.insert(dist.id.name.clone(), dist.hashes()); + } } let diagnostics = vec![]; Ok(Resolution::new(map, hashes, diagnostics)) diff --git a/crates/uv-resolver/src/lock/requirements_txt.rs b/crates/uv-resolver/src/lock/requirements_txt.rs index dcad09ee4951..b7f25dba4b90 100644 --- a/crates/uv-resolver/src/lock/requirements_txt.rs +++ b/crates/uv-resolver/src/lock/requirements_txt.rs @@ -134,7 +134,7 @@ impl<'lock> RequirementsTxtExport<'lock> { let mut nodes: Vec = petgraph .node_references() .filter(|(_index, package)| { - install_options.include_package(&package.id.name, root_name, lock.members()) + install_options.include_package(&package.id.name, Some(root_name), lock.members()) }) .map(|(index, package)| Node { package, diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index a027cee44404..31ed1c1a88e8 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -219,15 +219,19 @@ pub(super) async fn do_sync( let tags = venv.interpreter().tags()?; // Read the lockfile. - let resolution = lock.to_resolution(target, &markers, tags, extras, &dev, build_options)?; + let resolution = lock.to_resolution( + target, + &markers, + tags, + extras, + &dev, + build_options, + &install_options, + )?; // Always skip virtual projects, which shouldn't be built or installed. let resolution = apply_no_virtual_project(resolution); - // Filter resolution based on install-specific options. - let resolution = - install_options.filter_resolution(resolution, target.project_name(), lock.members()); - // Add all authenticated sources to the cache. for url in index_locations.urls() { store_credentials_from_url(url); diff --git a/crates/uv/tests/sync.rs b/crates/uv/tests/sync.rs index 80093b99f151..ff48a5df81a3 100644 --- a/crates/uv/tests/sync.rs +++ b/crates/uv/tests/sync.rs @@ -1222,6 +1222,61 @@ fn no_install_package() -> Result<()> { Ok(()) } +/// Ensure that `--no-build` isn't enforced for projects that aren't installed in the first place. +#[test] +fn no_install_project_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"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + // Generate a lockfile. + context.lock().assert().success(); + + // `--no-build` should raise an error, since we try to install the project. + uv_snapshot!(context.filters(), context.sync().arg("--no-build"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + warning: Failed to validate existing lockfile: distribution project==0.1.0 @ editable+. can't be installed because it is marked as `--no-build` but has no binary distribution + Resolved 4 packages in [TIME] + error: distribution project==0.1.0 @ editable+. can't be installed because it is marked as `--no-build` but has no binary distribution + "###); + + // But it's fine to combine `--no-install-project` with `--no-build`. We shouldn't error, since + // we aren't building the project. + uv_snapshot!(context.filters(), context.sync().arg("--no-install-project").arg("--no-build"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: Failed to validate existing lockfile: distribution project==0.1.0 @ editable+. can't be installed because it is marked as `--no-build` but has no binary distribution + 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(()) +} + /// Convert from a package to a virtual project. #[test] fn convert_to_virtual() -> Result<()> {