diff --git a/crates/uv-configuration/src/dev.rs b/crates/uv-configuration/src/dev.rs index 80ae4f0640e4..cce967ba1f89 100644 --- a/crates/uv-configuration/src/dev.rs +++ b/crates/uv-configuration/src/dev.rs @@ -316,7 +316,7 @@ impl From for DevGroupsSpecification { /// The manifest of `dependency-groups` to include, taking into account the user-provided /// [`DevGroupsSpecification`] and the project-specific default groups. -#[derive(Debug, Clone)] +#[derive(Debug, Default, Clone)] pub struct DevGroupsManifest { /// The specification for the development dependencies. pub(crate) spec: DevGroupsSpecification, @@ -347,7 +347,7 @@ impl DevGroupsManifest { } /// Returns `true` if the group was enabled by default. - pub fn default(&self, group: &GroupName) -> bool { + pub fn is_default(&self, group: &GroupName) -> bool { if self.spec.contains(group) { // If the group was explicitly requested, then it wasn't enabled by default. false diff --git a/crates/uv-resolver/src/lock/installable.rs b/crates/uv-resolver/src/lock/installable.rs index 66dc1984262a..78d8c908ab77 100644 --- a/crates/uv-resolver/src/lock/installable.rs +++ b/crates/uv-resolver/src/lock/installable.rs @@ -27,6 +27,13 @@ pub trait Installable<'lock> { /// Return the [`PackageName`] of the root packages in the target. fn roots(&self) -> impl Iterator; + /// Return the [`InstallTarget`] requirements. + /// + /// Returns dependencies that apply to the workspace root, but not any of its members. As such, + /// only returns a non-empty iterator for scripts, which include packages directly (unlike + /// workspaces, in which each member has its own dependencies). + fn requirements(&self) -> Option<&[uv_pep508::Requirement]>; + /// Return the [`InstallTarget`] dependency groups. /// /// Returns dependencies that apply to the workspace root, but not any of its members. As such, diff --git a/crates/uv-scripts/src/lib.rs b/crates/uv-scripts/src/lib.rs index 8b46007e0be8..7bc52d0b028a 100644 --- a/crates/uv-scripts/src/lib.rs +++ b/crates/uv-scripts/src/lib.rs @@ -54,6 +54,14 @@ impl Pep723Item { Self::Remote(_) => None, } } + + /// Return the PEP 723 script, if any. + pub fn as_script(&self) -> Option<&Pep723Script> { + match self { + Self::Script(script) => Some(script), + _ => None, + } + } } /// A PEP 723 script, including its [`Pep723Metadata`]. @@ -193,6 +201,18 @@ impl Pep723Script { Ok(()) } + + /// Return the [`Sources`] defined in the PEP 723 metadata. + pub fn sources(&self) -> &BTreeMap { + static EMPTY: BTreeMap = BTreeMap::new(); + + self.metadata + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.sources.as_ref()) + .unwrap_or(&EMPTY) + } } /// PEP 723 metadata as parsed from a `script` comment block. diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index d06b98f1082c..d56f3dff6314 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -593,6 +593,7 @@ pub(crate) async fn add( Target::Project(project, environment) => (project, environment), // If `--script`, exit early. There's no reason to lock and sync. Target::Script(script, _) => { + // TODO(charlie): Lock the script, if a lockfile already exists. writeln!( printer.stderr(), "Updated `{}`", diff --git a/crates/uv/src/commands/project/environment.rs b/crates/uv/src/commands/project/environment.rs index e4dd28ec2c01..8eed9a12db57 100644 --- a/crates/uv/src/commands/project/environment.rs +++ b/crates/uv/src/commands/project/environment.rs @@ -1,6 +1,7 @@ use tracing::debug; use crate::commands::pip::loggers::{InstallLogger, ResolveLogger}; +use crate::commands::project::install_target::InstallTarget; use crate::commands::project::{ resolve_environment, sync_environment, EnvironmentSpecification, ProjectError, }; @@ -9,10 +10,13 @@ use crate::settings::ResolverInstallerSettings; use uv_cache::{Cache, CacheBucket}; use uv_cache_key::{cache_digest, hash_digest}; use uv_client::Connectivity; -use uv_configuration::{Concurrency, PreviewMode, TrustedHost}; +use uv_configuration::{ + Concurrency, DevGroupsManifest, ExtrasSpecification, InstallOptions, PreviewMode, TrustedHost, +}; use uv_dispatch::SharedState; use uv_distribution_types::{Name, Resolution}; use uv_python::{Interpreter, PythonEnvironment}; +use uv_resolver::Installable; /// A [`PythonEnvironment`] stored in the cache. #[derive(Debug)] @@ -25,9 +29,8 @@ impl From for PythonEnvironment { } impl CachedEnvironment { - /// Get or create an [`CachedEnvironment`] based on a given set of requirements and a base - /// interpreter. - pub(crate) async fn get_or_create( + /// Get or create an [`CachedEnvironment`] based on a given set of requirements. + pub(crate) async fn from_spec( spec: EnvironmentSpecification<'_>, interpreter: Interpreter, settings: &ResolverInstallerSettings, @@ -43,21 +46,7 @@ impl CachedEnvironment { printer: Printer, preview: PreviewMode, ) -> Result { - // When caching, always use the base interpreter, rather than that of the virtual - // environment. - let interpreter = if let Some(interpreter) = interpreter.to_base_interpreter(cache)? { - debug!( - "Caching via base interpreter: `{}`", - interpreter.sys_executable().display() - ); - interpreter - } else { - debug!( - "Caching via interpreter: `{}`", - interpreter.sys_executable().display() - ); - interpreter - }; + let interpreter = Self::base_interpreter(interpreter, cache)?; // Resolve the requirements with the interpreter. let resolution = Resolution::from( @@ -78,6 +67,93 @@ impl CachedEnvironment { .await?, ); + Self::from_resolution( + resolution, + interpreter, + settings, + state, + install, + installer_metadata, + connectivity, + concurrency, + native_tls, + allow_insecure_host, + cache, + printer, + preview, + ) + .await + } + + /// Get or create an [`CachedEnvironment`] based on a given [`InstallTarget`]. + pub(crate) async fn from_lock( + target: InstallTarget<'_>, + extras: &ExtrasSpecification, + dev: &DevGroupsManifest, + install_options: InstallOptions, + settings: &ResolverInstallerSettings, + interpreter: Interpreter, + state: &SharedState, + install: Box, + installer_metadata: bool, + connectivity: Connectivity, + concurrency: Concurrency, + native_tls: bool, + allow_insecure_host: &[TrustedHost], + cache: &Cache, + printer: Printer, + preview: PreviewMode, + ) -> Result { + let interpreter = Self::base_interpreter(interpreter, cache)?; + + // Determine the tags, markers, and interpreter to use for resolution. + let tags = interpreter.tags()?; + let marker_env = interpreter.resolver_marker_environment(); + + // Read the lockfile. + let resolution = target.to_resolution( + &marker_env, + tags, + extras, + dev, + &settings.build_options, + &install_options, + )?; + + Self::from_resolution( + resolution, + interpreter, + settings, + state, + install, + installer_metadata, + connectivity, + concurrency, + native_tls, + allow_insecure_host, + cache, + printer, + preview, + ) + .await + } + + /// Get or create an [`CachedEnvironment`] based on a given [`Resolution`]. + pub(crate) async fn from_resolution( + resolution: Resolution, + interpreter: Interpreter, + settings: &ResolverInstallerSettings, + state: &SharedState, + install: Box, + installer_metadata: bool, + connectivity: Connectivity, + concurrency: Concurrency, + native_tls: bool, + allow_insecure_host: &[TrustedHost], + cache: &Cache, + printer: Printer, + preview: PreviewMode, + ) -> Result { // Hash the resolution by hashing the generated lockfile. // TODO(charlie): If the resolution contains any mutable metadata (like a path or URL // dependency), skip this step. @@ -144,4 +220,28 @@ impl CachedEnvironment { pub(crate) fn into_interpreter(self) -> Interpreter { self.0.into_interpreter() } + + /// Return the [`Interpreter`] to use for the cached environment, based on a given + /// [`Interpreter`]. + /// + /// When caching, always use the base interpreter, rather than that of the virtual + /// environment. + fn base_interpreter( + interpreter: Interpreter, + cache: &Cache, + ) -> Result { + if let Some(interpreter) = interpreter.to_base_interpreter(cache)? { + debug!( + "Caching via base interpreter: `{}`", + interpreter.sys_executable().display() + ); + Ok(interpreter) + } else { + debug!( + "Caching via interpreter: `{}`", + interpreter.sys_executable().display() + ); + Ok(interpreter) + } + } } diff --git a/crates/uv/src/commands/project/install_target.rs b/crates/uv/src/commands/project/install_target.rs index cadc22ace909..36d22e151d5e 100644 --- a/crates/uv/src/commands/project/install_target.rs +++ b/crates/uv/src/commands/project/install_target.rs @@ -1,12 +1,16 @@ +use std::borrow::Cow; use std::collections::BTreeMap; use std::path::Path; +use std::str::FromStr; use itertools::Either; use uv_normalize::{GroupName, PackageName, DEV_DEPENDENCIES}; -use uv_pypi_types::VerbatimParsedUrl; +use uv_pypi_types::{LenientRequirement, VerbatimParsedUrl}; use uv_resolver::{Installable, Lock, Package}; +use uv_scripts::Pep723Script; use uv_workspace::dependency_groups::{DependencyGroupError, FlatDependencyGroups}; +use uv_workspace::pyproject::{DependencyGroupSpecifier, Source, Sources, ToolUvSources}; use uv_workspace::Workspace; /// A target that can be installed from a lockfile. @@ -28,6 +32,11 @@ pub(crate) enum InstallTarget<'lock> { workspace: &'lock Workspace, lock: &'lock Lock, }, + /// A PEP 723 script. + Script { + script: &'lock Pep723Script, + lock: &'lock Lock, + }, } impl<'lock> Installable<'lock> for InstallTarget<'lock> { @@ -36,6 +45,7 @@ impl<'lock> Installable<'lock> for InstallTarget<'lock> { Self::Project { workspace, .. } => workspace.install_path(), Self::Workspace { workspace, .. } => workspace.install_path(), Self::NonProjectWorkspace { workspace, .. } => workspace.install_path(), + Self::Script { script, .. } => script.path.parent().unwrap(), } } @@ -44,24 +54,37 @@ impl<'lock> Installable<'lock> for InstallTarget<'lock> { Self::Project { lock, .. } => lock, Self::Workspace { lock, .. } => lock, Self::NonProjectWorkspace { lock, .. } => lock, + Self::Script { lock, .. } => lock, } } fn roots(&self) -> impl Iterator { match self { - Self::Project { name, .. } => Either::Right(Either::Left(std::iter::once(*name))), - Self::NonProjectWorkspace { lock, .. } => Either::Left(lock.members().iter()), + Self::Project { name, .. } => Either::Left(Either::Left(std::iter::once(*name))), + Self::NonProjectWorkspace { lock, .. } => { + Either::Left(Either::Right(lock.members().iter())) + } Self::Workspace { lock, .. } => { // Identify the workspace members. // // The members are encoded directly in the lockfile, unless the workspace contains a // single member at the root, in which case, we identify it by its source. if lock.members().is_empty() { - Either::Right(Either::Right(lock.root().into_iter().map(Package::name))) + Either::Right(Either::Left(lock.root().into_iter().map(Package::name))) } else { - Either::Left(lock.members().iter()) + Either::Left(Either::Right(lock.members().iter())) } } + Self::Script { .. } => Either::Right(Either::Right(std::iter::empty())), + } + } + + fn requirements(&self) -> Option<&[uv_pep508::Requirement]> { + match self { + Self::Project { .. } => None, + Self::Workspace { .. } => None, + Self::NonProjectWorkspace { .. } => None, + Self::Script { script, .. } => script.metadata.dependencies.as_deref(), } } @@ -119,6 +142,7 @@ impl<'lock> Installable<'lock> for InstallTarget<'lock> { Ok(map) } + Self::Script { .. } => Ok(BTreeMap::default()), } } @@ -127,17 +151,117 @@ impl<'lock> Installable<'lock> for InstallTarget<'lock> { Self::Project { name, .. } => Some(name), Self::Workspace { .. } => None, Self::NonProjectWorkspace { .. } => None, + Self::Script { .. } => None, } } } impl<'lock> InstallTarget<'lock> { - /// Return the [`Workspace`] of the target. - pub(crate) fn workspace(&self) -> &'lock Workspace { + /// Return an iterator over all [`Sources`] defined by the target. + pub(crate) fn sources(&self) -> impl Iterator { + match self { + Self::Project { workspace, .. } + | Self::Workspace { workspace, .. } + | Self::NonProjectWorkspace { workspace, .. } => { + Either::Left(workspace.sources().values().flat_map(Sources::iter).chain( + workspace.packages().values().flat_map(|member| { + member + .pyproject_toml() + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.sources.as_ref()) + .map(ToolUvSources::inner) + .into_iter() + .flat_map(|sources| sources.values().flat_map(Sources::iter)) + }), + )) + } + Self::Script { script, .. } => { + Either::Right(script.sources().values().flat_map(Sources::iter)) + } + } + } + + /// Return an iterator over all requirements defined by the target. + pub(crate) fn requirements( + &self, + ) -> impl Iterator>> { match self { - Self::Project { workspace, .. } => workspace, - Self::Workspace { workspace, .. } => workspace, - Self::NonProjectWorkspace { workspace, .. } => workspace, + Self::Project { workspace, .. } + | Self::Workspace { workspace, .. } + | Self::NonProjectWorkspace { workspace, .. } => { + Either::Left( + // Iterate over the non-member requirements in the workspace. + workspace + .non_project_requirements() + .ok() + .into_iter() + .flatten() + .map(Cow::Owned) + .chain(workspace.packages().values().flat_map(|member| { + // Iterate over all dependencies in each member. + let dependencies = member + .pyproject_toml() + .project + .as_ref() + .and_then(|project| project.dependencies.as_ref()) + .into_iter() + .flatten(); + let optional_dependencies = member + .pyproject_toml() + .project + .as_ref() + .and_then(|project| project.optional_dependencies.as_ref()) + .into_iter() + .flat_map(|optional| optional.values()) + .flatten(); + let dependency_groups = member + .pyproject_toml() + .dependency_groups + .as_ref() + .into_iter() + .flatten() + .flat_map(|(_, dependencies)| { + dependencies.iter().filter_map(|specifier| { + if let DependencyGroupSpecifier::Requirement(requirement) = + specifier + { + Some(requirement) + } else { + None + } + }) + }); + let dev_dependencies = member + .pyproject_toml() + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.dev_dependencies.as_ref()) + .into_iter() + .flatten(); + dependencies + .chain(optional_dependencies) + .chain(dependency_groups) + .filter_map(|requires_dist| { + LenientRequirement::::from_str(requires_dist) + .map(uv_pep508::Requirement::from) + .map(Cow::Owned) + .ok() + }) + .chain(dev_dependencies.map(Cow::Borrowed)) + })), + ) + } + Self::Script { script, .. } => Either::Right( + script + .metadata + .dependencies + .iter() + .flatten() + .map(Cow::Borrowed), + ), } } } diff --git a/crates/uv/src/commands/project/lock_target.rs b/crates/uv/src/commands/project/lock_target.rs index 991b43f195db..dae8e14307bd 100644 --- a/crates/uv/src/commands/project/lock_target.rs +++ b/crates/uv/src/commands/project/lock_target.rs @@ -270,6 +270,11 @@ impl<'lock> LockTarget<'lock> { } } + /// Returns `true` if the lockfile exists. + pub(crate) fn exists(&self) -> bool { + self.lock_path().exists() + } + /// Read the lockfile from the workspace. /// /// Returns `Ok(None)` if the lockfile does not exist. diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 5cb85f3a452d..11a53bb0ecad 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -267,7 +267,7 @@ impl std::fmt::Display for ConflictError { self.conflicts .iter() .map(|conflict| match conflict { - ConflictPackage::Group(ref group) if self.dev.default(group) => + ConflictPackage::Group(ref group) if self.dev.is_default(group) => format!("`{group}` (enabled by default)"), ConflictPackage::Group(ref group) => format!("`{group}`"), ConflictPackage::Extra(..) => unreachable!(), @@ -286,7 +286,7 @@ impl std::fmt::Display for ConflictError { .map(|(i, conflict)| { let conflict = match conflict { ConflictPackage::Extra(ref extra) => format!("extra `{extra}`"), - ConflictPackage::Group(ref group) if self.dev.default(group) => { + ConflictPackage::Group(ref group) if self.dev.is_default(group) => { format!("group `{group}` (enabled by default)") } ConflictPackage::Group(ref group) => format!("group `{group}`"), diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 04f6cd9ac4b5..1dfe64ad4b43 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -17,8 +17,8 @@ use uv_cache::Cache; use uv_cli::ExternalCommand; use uv_client::{BaseClientBuilder, Connectivity}; use uv_configuration::{ - Concurrency, DevGroupsSpecification, EditableMode, ExtrasSpecification, GroupsSpecification, - InstallOptions, LowerBound, PreviewMode, SourceStrategy, TrustedHost, + Concurrency, DevGroupsManifest, DevGroupsSpecification, EditableMode, ExtrasSpecification, + GroupsSpecification, InstallOptions, LowerBound, PreviewMode, SourceStrategy, TrustedHost, }; use uv_dispatch::SharedState; use uv_distribution::LoweredRequirement; @@ -200,109 +200,57 @@ pub(crate) async fn run( .await? .into_interpreter(); - // Determine the working directory for the script. - let script_dir = match &script { - Pep723Item::Script(script) => std::path::absolute(&script.path)? - .parent() - .expect("script path has no parent") - .to_owned(), - Pep723Item::Stdin(..) | Pep723Item::Remote(..) => std::env::current_dir()?, - }; - let script = script.into_metadata(); - - // Install the script requirements, if necessary. Otherwise, use an isolated environment. - if let Some(dependencies) = script.dependencies { - // Collect any `tool.uv.index` from the script. - let empty = Vec::default(); - let script_indexes = match settings.sources { - SourceStrategy::Enabled => script - .tool - .as_ref() - .and_then(|tool| tool.uv.as_ref()) - .and_then(|uv| uv.top_level.index.as_deref()) - .unwrap_or(&empty), - SourceStrategy::Disabled => &empty, - }; + // If a lockfile already exists, lock the script. + if let Some(target) = script + .as_script() + .map(LockTarget::from) + .filter(LockTarget::exists) + { + debug!("Found existing lockfile for script"); - // Collect any `tool.uv.sources` from the script. - let empty = BTreeMap::default(); - let script_sources = match settings.sources { - SourceStrategy::Enabled => script - .tool - .as_ref() - .and_then(|tool| tool.uv.as_ref()) - .and_then(|uv| uv.sources.as_ref()) - .unwrap_or(&empty), - SourceStrategy::Disabled => &empty, + // Determine the lock mode. + let mode = if frozen { + LockMode::Frozen + } else if locked { + LockMode::Locked(&interpreter) + } else { + LockMode::Write(&interpreter) }; - let requirements = dependencies - .into_iter() - .flat_map(|requirement| { - LoweredRequirement::from_non_workspace_requirement( - requirement, - script_dir.as_ref(), - script_sources, - script_indexes, - &settings.index_locations, - LowerBound::Allow, - ) - .map_ok(LoweredRequirement::into_inner) - }) - .collect::>()?; - let constraints = script - .tool - .as_ref() - .and_then(|tool| tool.uv.as_ref()) - .and_then(|uv| uv.constraint_dependencies.as_ref()) - .into_iter() - .flatten() - .cloned() - .flat_map(|requirement| { - LoweredRequirement::from_non_workspace_requirement( - requirement, - script_dir.as_ref(), - script_sources, - script_indexes, - &settings.index_locations, - LowerBound::Allow, - ) - .map_ok(LoweredRequirement::into_inner) - }) - .collect::, _>>()?; - let overrides = script - .tool - .as_ref() - .and_then(|tool| tool.uv.as_ref()) - .and_then(|uv| uv.override_dependencies.as_ref()) - .into_iter() - .flatten() - .cloned() - .flat_map(|requirement| { - LoweredRequirement::from_non_workspace_requirement( - requirement, - script_dir.as_ref(), - script_sources, - script_indexes, - &settings.index_locations, - LowerBound::Allow, - ) - .map_ok(LoweredRequirement::into_inner) - }) - .collect::, _>>()?; - - let spec = - RequirementsSpecification::from_overrides(requirements, constraints, overrides); - let result = CachedEnvironment::get_or_create( - EnvironmentSpecification::from(spec), - interpreter, - &settings, + // Generate a lockfile. + let lock = project::lock::do_safe_lock( + mode, + target, + settings.as_ref().into(), + LowerBound::Allow, &state, if show_resolution { Box::new(DefaultResolveLogger) } else { Box::new(SummaryResolveLogger) }, + connectivity, + concurrency, + native_tls, + allow_insecure_host, + cache, + printer, + preview, + ) + .await? + .into_lock(); + + let result = CachedEnvironment::from_lock( + InstallTarget::Script { + script: script.as_script().unwrap(), + lock: &lock, + }, + &ExtrasSpecification::default(), + &DevGroupsManifest::default(), + InstallOptions::default(), + &settings, + interpreter, + &state, if show_resolution { Box::new(DefaultInstallLogger) } else { @@ -331,19 +279,151 @@ pub(crate) async fn run( Some(environment.into_interpreter()) } else { - // Create a virtual environment. - temp_dir = cache.venv_dir()?; - let environment = uv_virtualenv::create_venv( - temp_dir.path(), - interpreter, - uv_virtualenv::Prompt::None, - false, - false, - false, - false, - )?; + // Determine the working directory for the script. + let script_dir = match &script { + Pep723Item::Script(script) => std::path::absolute(&script.path)? + .parent() + .expect("script path has no parent") + .to_owned(), + Pep723Item::Stdin(..) | Pep723Item::Remote(..) => std::env::current_dir()?, + }; + let script = script.into_metadata(); + + // Install the script requirements, if necessary. Otherwise, use an isolated environment. + if let Some(dependencies) = script.dependencies { + // Collect any `tool.uv.index` from the script. + let empty = Vec::default(); + let script_indexes = match settings.sources { + SourceStrategy::Enabled => script + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.top_level.index.as_deref()) + .unwrap_or(&empty), + SourceStrategy::Disabled => &empty, + }; - Some(environment.into_interpreter()) + // Collect any `tool.uv.sources` from the script. + let empty = BTreeMap::default(); + let script_sources = match settings.sources { + SourceStrategy::Enabled => script + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.sources.as_ref()) + .unwrap_or(&empty), + SourceStrategy::Disabled => &empty, + }; + + let requirements = dependencies + .into_iter() + .flat_map(|requirement| { + LoweredRequirement::from_non_workspace_requirement( + requirement, + script_dir.as_ref(), + script_sources, + script_indexes, + &settings.index_locations, + LowerBound::Allow, + ) + .map_ok(LoweredRequirement::into_inner) + }) + .collect::>()?; + let constraints = script + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.constraint_dependencies.as_ref()) + .into_iter() + .flatten() + .cloned() + .flat_map(|requirement| { + LoweredRequirement::from_non_workspace_requirement( + requirement, + script_dir.as_ref(), + script_sources, + script_indexes, + &settings.index_locations, + LowerBound::Allow, + ) + .map_ok(LoweredRequirement::into_inner) + }) + .collect::, _>>()?; + let overrides = script + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.override_dependencies.as_ref()) + .into_iter() + .flatten() + .cloned() + .flat_map(|requirement| { + LoweredRequirement::from_non_workspace_requirement( + requirement, + script_dir.as_ref(), + script_sources, + script_indexes, + &settings.index_locations, + LowerBound::Allow, + ) + .map_ok(LoweredRequirement::into_inner) + }) + .collect::, _>>()?; + + let spec = + RequirementsSpecification::from_overrides(requirements, constraints, overrides); + let result = CachedEnvironment::from_spec( + EnvironmentSpecification::from(spec), + interpreter, + &settings, + &state, + if show_resolution { + Box::new(DefaultResolveLogger) + } else { + Box::new(SummaryResolveLogger) + }, + if show_resolution { + Box::new(DefaultInstallLogger) + } else { + Box::new(SummaryInstallLogger) + }, + installer_metadata, + connectivity, + concurrency, + native_tls, + allow_insecure_host, + cache, + printer, + preview, + ) + .await; + + let environment = match result { + Ok(resolution) => resolution, + Err(ProjectError::Operation(err)) => { + return diagnostics::OperationDiagnostic::with_context("script") + .report(err) + .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())) + } + Err(err) => return Err(err.into()), + }; + + Some(environment.into_interpreter()) + } else { + // Create a virtual environment. + temp_dir = cache.venv_dir()?; + let environment = uv_virtualenv::create_venv( + temp_dir.path(), + interpreter, + uv_virtualenv::Prompt::None, + false, + false, + false, + false, + )?; + + Some(environment.into_interpreter()) + } } } else { None @@ -846,7 +926,7 @@ pub(crate) async fn run( Some(spec) => { debug!("Syncing ephemeral requirements"); - let result = CachedEnvironment::get_or_create( + let result = CachedEnvironment::from_spec( EnvironmentSpecification::from(spec).with_lock( lock.as_ref() .map(|(lock, install_path)| (lock, install_path.as_ref())), diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index da546235b317..2941a954a5b9 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -1,6 +1,4 @@ -use std::borrow::Cow; use std::path::Path; -use std::str::FromStr; use anyhow::{Context, Result}; use itertools::Itertools; @@ -18,16 +16,14 @@ use uv_distribution_types::{ }; use uv_installer::SitePackages; use uv_normalize::PackageName; -use uv_pep508::{MarkerTree, Requirement, VersionOrUrl}; -use uv_pypi_types::{ - LenientRequirement, ParsedArchiveUrl, ParsedGitUrl, ParsedUrl, VerbatimParsedUrl, -}; +use uv_pep508::{MarkerTree, VersionOrUrl}; +use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl, ParsedUrl}; use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest}; use uv_resolver::{FlatIndex, Installable}; use uv_settings::PythonInstallMirrors; use uv_types::{BuildIsolation, HashStrategy}; use uv_warnings::warn_user; -use uv_workspace::pyproject::{DependencyGroupSpecifier, Source, Sources, ToolUvSources}; +use uv_workspace::pyproject::Source; use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace}; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger}; @@ -368,7 +364,7 @@ pub(super) async fn do_sync( } // Populate credentials from the workspace. - store_credentials_from_workspace(target.workspace()); + store_credentials_from_workspace(target); // Initialize the registry client. let client = RegistryClientBuilder::new(cache.clone()) @@ -526,9 +522,9 @@ fn apply_editable_mode(resolution: Resolution, editable: EditableMode) -> Resolu /// /// These credentials can come from any of `tool.uv.sources`, `tool.uv.dev-dependencies`, /// `project.dependencies`, and `project.optional-dependencies`. -fn store_credentials_from_workspace(workspace: &Workspace) { +fn store_credentials_from_workspace(target: InstallTarget<'_>) { // Iterate over any sources in the workspace root. - for source in workspace.sources().values().flat_map(Sources::iter) { + for source in target.sources() { match source { Source::Git { git, .. } => { uv_git::store_credentials_from_url(git); @@ -541,12 +537,7 @@ fn store_credentials_from_workspace(workspace: &Workspace) { } // Iterate over any dependencies defined in the workspace root. - for requirement in workspace - .non_project_requirements() - .ok() - .into_iter() - .flatten() - { + for requirement in target.requirements() { let Some(VersionOrUrl::Url(url)) = &requirement.version_or_url else { continue; }; @@ -560,94 +551,4 @@ fn store_credentials_from_workspace(workspace: &Workspace) { _ => {} } } - - // Iterate over each workspace member. - for member in workspace.packages().values() { - // Iterate over the `tool.uv.sources`. - for source in member - .pyproject_toml() - .tool - .as_ref() - .and_then(|tool| tool.uv.as_ref()) - .and_then(|uv| uv.sources.as_ref()) - .map(ToolUvSources::inner) - .iter() - .flat_map(|sources| sources.values().flat_map(Sources::iter)) - { - match source { - Source::Git { git, .. } => { - uv_git::store_credentials_from_url(git); - } - Source::Url { url, .. } => { - uv_auth::store_credentials_from_url(url); - } - _ => {} - } - } - - // Iterate over all dependencies. - let dependencies = member - .pyproject_toml() - .project - .as_ref() - .and_then(|project| project.dependencies.as_ref()) - .into_iter() - .flatten(); - let optional_dependencies = member - .pyproject_toml() - .project - .as_ref() - .and_then(|project| project.optional_dependencies.as_ref()) - .into_iter() - .flat_map(|optional| optional.values()) - .flatten(); - let dependency_groups = member - .pyproject_toml() - .dependency_groups - .as_ref() - .into_iter() - .flatten() - .flat_map(|(_, dependencies)| { - dependencies.iter().filter_map(|specifier| { - if let DependencyGroupSpecifier::Requirement(requirement) = specifier { - Some(requirement) - } else { - None - } - }) - }); - let dev_dependencies = member - .pyproject_toml() - .tool - .as_ref() - .and_then(|tool| tool.uv.as_ref()) - .and_then(|uv| uv.dev_dependencies.as_ref()) - .into_iter() - .flatten(); - - for requirement in dependencies - .chain(optional_dependencies) - .chain(dependency_groups) - .filter_map(|requires_dist| { - LenientRequirement::::from_str(requires_dist) - .map(Requirement::from) - .map(Cow::Owned) - .ok() - }) - .chain(dev_dependencies.map(Cow::Borrowed)) - { - let Some(VersionOrUrl::Url(url)) = &requirement.version_or_url else { - continue; - }; - match &url.parsed_url { - ParsedUrl::Git(ParsedGitUrl { url, .. }) => { - uv_git::store_credentials_from_url(url.repository()); - } - ParsedUrl::Archive(ParsedArchiveUrl { url, .. }) => { - uv_auth::store_credentials_from_url(url); - } - _ => {} - } - } - } } diff --git a/crates/uv/src/commands/tool/run.rs b/crates/uv/src/commands/tool/run.rs index 8d97bc10485d..c45a4ecddd44 100644 --- a/crates/uv/src/commands/tool/run.rs +++ b/crates/uv/src/commands/tool/run.rs @@ -621,7 +621,7 @@ async fn get_or_create_environment( // TODO(zanieb): When implementing project-level tools, discover the project and check if it has the tool. // TODO(zanieb): Determine if we should layer on top of the project environment if it is present. - let environment = CachedEnvironment::get_or_create( + let environment = CachedEnvironment::from_spec( EnvironmentSpecification::from(spec), interpreter, settings,