diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index e8c67bf43..0af691b6e 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -22,6 +22,7 @@ use uv_configuration::{ }; use uv_configuration::{BuildOutput, Concurrency}; use uv_distribution::DistributionDatabase; +use uv_distribution::ExtraBuildRequires; use uv_distribution_filename::DistFilename; use uv_distribution_types::{ CachedDist, DependencyMetadata, Identifier, IndexCapabilities, IndexLocations, @@ -40,7 +41,6 @@ use uv_types::{ HashStrategy, InFlight, }; use uv_workspace::WorkspaceCache; -use uv_distribution::ExtraBuildRequires; #[derive(Debug, Error)] pub enum BuildDispatchError { diff --git a/crates/uv-distribution/src/metadata/build_requires.rs b/crates/uv-distribution/src/metadata/build_requires.rs index 7a746b5de..af218b470 100644 --- a/crates/uv-distribution/src/metadata/build_requires.rs +++ b/crates/uv-distribution/src/metadata/build_requires.rs @@ -261,13 +261,15 @@ impl ExtraBuildRequires { workspace, None, ) - .map(move |requirement| match requirement { - Ok(requirement) => Ok(requirement.into_inner().into()), - Err(err) => Err(MetadataError::LoweringError( - requirement_name.clone(), - Box::new(err), - )), - }) + .map( + move |requirement| match requirement { + Ok(requirement) => Ok(requirement.into_inner().into()), + Err(err) => Err(MetadataError::LoweringError( + requirement_name.clone(), + Box::new(err), + )), + }, + ) }) .collect::, _>>()?; result.insert(package_name, lowered); diff --git a/crates/uv-scripts/src/lib.rs b/crates/uv-scripts/src/lib.rs index b80cdc219..b0b9fde2e 100644 --- a/crates/uv-scripts/src/lib.rs +++ b/crates/uv-scripts/src/lib.rs @@ -381,6 +381,8 @@ pub struct ToolUv { pub override_dependencies: Option>>, pub constraint_dependencies: Option>>, pub build_constraint_dependencies: Option>>, + pub extra_build_dependencies: + Option>>>, pub sources: Option>, } diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index 7c62a30f7..bd39bad18 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -564,9 +564,8 @@ async fn build_package( let workspace_cache = WorkspaceCache::default(); // Create a build dispatch. - let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( - extra_build_dependencies.clone(), - ); + let extra_build_requires = + uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone()); let build_dispatch = BuildDispatch::new( &client, cache, diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index ed54bb15c..7dbaaf428 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -477,9 +477,8 @@ pub(crate) async fn pip_compile( .map(|constraint| constraint.requirement.clone()), ); - let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( - extra_build_dependencies.clone(), - ); + let extra_build_requires = + uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone()); let build_dispatch = BuildDispatch::new( &client, &cache, diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs index 2a536f8ca..f202f3ab0 100644 --- a/crates/uv/src/commands/pip/install.rs +++ b/crates/uv/src/commands/pip/install.rs @@ -420,9 +420,8 @@ pub(crate) async fn pip_install( let state = SharedState::default(); // Create a build dispatch. - let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( - extra_build_dependencies.clone(), - ); + let extra_build_requires = + uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone()); let build_dispatch = BuildDispatch::new( &client, &cache, diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs index dbcb78fdd..be1161c8e 100644 --- a/crates/uv/src/commands/pip/sync.rs +++ b/crates/uv/src/commands/pip/sync.rs @@ -355,9 +355,8 @@ pub(crate) async fn pip_sync( let state = SharedState::default(); // Create a build dispatch. - let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( - extra_build_dependencies.clone(), - ); + let extra_build_requires = + uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone()); let build_dispatch = BuildDispatch::new( &client, &cache, diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 19b4d0d42..73b6a81ab 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -1298,6 +1298,7 @@ impl PythonTarget { /// Represents the destination where dependencies are added, either to a project or a script. #[derive(Debug, Clone)] +#[allow(clippy::large_enum_variant)] pub(super) enum AddTarget { /// A PEP 723 script, with inline metadata. Script(Pep723Script, Box), @@ -1398,6 +1399,7 @@ impl AddTarget { } #[derive(Debug, Clone)] +#[allow(clippy::large_enum_variant)] enum AddTargetSnapshot { Script(Pep723Script, Option>), Project(VirtualProject, Option>), diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index c14bfd904..b78fc8ce8 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -32,6 +32,7 @@ use crate::printer::Printer; use crate::settings::{NetworkSettings, ResolverSettings}; #[derive(Debug, Clone)] +#[allow(clippy::large_enum_variant)] enum ExportTarget { /// A PEP 723 script, with inline metadata. Script(Pep723Script), diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 73b89d084..ff6686691 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -42,7 +42,7 @@ use crate::commands::pip::loggers::{DefaultResolveLogger, ResolveLogger, Summary use crate::commands::project::lock_target::LockTarget; use crate::commands::project::{ ProjectError, ProjectInterpreter, ScriptInterpreter, UniversalState, - init_script_python_requirement, + init_script_python_requirement, script_specification, }; use crate::commands::reporters::{PythonDownloadReporter, ResolverReporter}; use crate::commands::{ExitStatus, ScriptPath, diagnostics, pip}; @@ -678,9 +678,16 @@ async fn do_lock( index_locations, *sources, )?, - LockTarget::Script(_) => uv_distribution::ExtraBuildRequires::from_lowered( - extra_build_dependencies.clone(), - ), + LockTarget::Script(script) => { + // Try to get extra build dependencies from the script metadata + script_specification(Pep723ItemRef::Script(script), settings)? + .map(|spec| spec.extra_build_requires) + .unwrap_or_else(|| { + uv_distribution::ExtraBuildRequires::from_lowered( + extra_build_dependencies.clone(), + ) + }) + } }; let build_dispatch = BuildDispatch::new( &client, diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index e91026db8..96860b911 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -46,6 +46,7 @@ use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy}; use uv_virtualenv::remove_virtualenv; use uv_warnings::{warn_user, warn_user_once}; use uv_workspace::dependency_groups::DependencyGroupError; +use uv_workspace::pyproject::ExtraBuildDependencies; use uv_workspace::pyproject::PyProjectToml; use uv_workspace::{RequiresPythonSources, Workspace, WorkspaceCache}; @@ -1741,9 +1742,8 @@ pub(crate) async fn resolve_names( let build_hasher = HashStrategy::default(); // Create a build dispatch. - let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( - extra_build_dependencies.clone(), - ); + let extra_build_requires = + uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone()); let build_dispatch = BuildDispatch::new( &client, cache, @@ -1954,9 +1954,8 @@ pub(crate) async fn resolve_environment( let workspace_cache = WorkspaceCache::default(); // Create a build dispatch. - let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( - extra_build_dependencies.clone(), - ); + let extra_build_requires = + uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone()); let resolve_dispatch = BuildDispatch::new( &client, cache, @@ -2097,9 +2096,8 @@ pub(crate) async fn sync_environment( }; // Create a build dispatch. - let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( - extra_build_dependencies.clone(), - ); + let extra_build_requires = + uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies.clone()); let build_dispatch = BuildDispatch::new( &client, cache, @@ -2157,6 +2155,15 @@ pub(crate) async fn sync_environment( Ok(venv) } +/// A script specification that includes both requirements and extra build dependencies. +#[derive(Debug)] +pub(crate) struct ScriptSpecification { + /// The requirements specification for the script. + pub(crate) requirements: RequirementsSpecification, + /// The extra build dependencies for the script. + pub(crate) extra_build_requires: uv_distribution::ExtraBuildRequires, +} + /// The result of updating a [`PythonEnvironment`] to satisfy a set of [`RequirementsSource`]s. #[derive(Debug)] pub(crate) struct EnvironmentUpdate { @@ -2179,6 +2186,7 @@ pub(crate) async fn update_environment( spec: RequirementsSpecification, modifications: Modifications, build_constraints: Constraints, + extra_build_requires: uv_distribution::ExtraBuildRequires, settings: &ResolverInstallerSettings, network_settings: &NetworkSettings, state: &SharedState, @@ -2209,7 +2217,7 @@ pub(crate) async fn update_environment( link_mode, no_build_isolation, no_build_isolation_package, - extra_build_dependencies, + extra_build_dependencies: _, prerelease, resolution, sources, @@ -2326,9 +2334,6 @@ pub(crate) async fn update_environment( }; // Create a build dispatch. - let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( - extra_build_dependencies.clone(), - ); let build_dispatch = BuildDispatch::new( &client, cache, @@ -2547,12 +2552,12 @@ pub(crate) fn detect_conflicts( Ok(()) } -/// Determine the [`RequirementsSpecification`] for a script. +/// Determine the [`ScriptSpecification`] for a script. #[allow(clippy::result_large_err)] pub(crate) fn script_specification( script: Pep723ItemRef<'_>, settings: &ResolverSettings, -) -> Result, ProjectError> { +) -> Result, ProjectError> { let Some(dependencies) = script.metadata().dependencies.as_ref() else { return Ok(None); }; @@ -2647,11 +2652,47 @@ pub(crate) fn script_specification( }) .collect::, _>>()?; - Ok(Some(RequirementsSpecification::from_overrides( - requirements, - constraints, - overrides, - ))) + // Collect any `tool.uv.extra-build-dependencies` from the script. + let empty = BTreeMap::default(); + let script_extra_build_dependencies = script + .metadata() + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.extra_build_dependencies.as_ref()) + .unwrap_or(&empty); + + // Lower the extra build dependencies + let mut extra_build_dependencies = ExtraBuildDependencies::default(); + for (name, requirements) in script_extra_build_dependencies { + let lowered_requirements: Vec<_> = requirements + .iter() + .cloned() + .flat_map(|requirement| { + LoweredRequirement::from_non_workspace_requirement( + requirement, + script_dir.as_ref(), + script_sources, + script_indexes, + &settings.index_locations, + ) + .map_ok(|req| req.into_inner().into()) + }) + .collect::, _>>()?; + extra_build_dependencies.insert(name.clone(), lowered_requirements); + } + + let extra_build_requires = + uv_distribution::ExtraBuildRequires::from_lowered(extra_build_dependencies); + + Ok(Some(ScriptSpecification { + requirements: RequirementsSpecification::from_overrides( + requirements, + constraints, + overrides, + ), + extra_build_requires, + })) } /// Warn if the user provides (e.g.) an `--index-url` in a requirements file. diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index 50615699e..cfda7327b 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -386,6 +386,7 @@ pub(crate) async fn remove( /// Represents the destination where dependencies are added, either to a project or a script. #[derive(Debug)] +#[allow(clippy::large_enum_variant)] enum RemoveTarget { /// A PEP 723 script, with inline metadata. Project(VirtualProject), diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index a1476c63c..8b863eb10 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -358,7 +358,9 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl } // Install the script requirements, if necessary. Otherwise, use an isolated environment. - if let Some(spec) = script_specification((&script).into(), &settings.resolver)? { + if let Some(script_spec) = script_specification((&script).into(), &settings.resolver)? { + let spec = script_spec.requirements; + let script_extra_build_requires = script_spec.extra_build_requires; let environment = ScriptEnvironment::get_or_init( (&script).into(), python.as_deref().map(PythonRequest::parse), @@ -407,6 +409,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl spec, modifications, build_constraints.unwrap_or_default(), + script_extra_build_requires, &settings, &network_settings, &sync_state, diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 9dbb47888..c93e1dc39 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -14,12 +14,11 @@ use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ Concurrency, Constraints, DependencyGroups, DependencyGroupsWithDefaults, DryRun, EditableMode, ExtrasSpecification, ExtrasSpecificationWithDefaults, HashCheckingMode, InstallOptions, - PreviewMode, TargetTriple, + PreviewMode, TargetTriple, Upgrade, }; use uv_dispatch::BuildDispatch; use uv_distribution_types::{ - DirectorySourceDist, Dist, Index, Requirement, Resolution, ResolvedDist, - SourceDist, + DirectorySourceDist, Dist, Index, Requirement, Resolution, ResolvedDist, SourceDist, }; use uv_fs::{PortablePathBuf, Simplified}; use uv_installer::SitePackages; @@ -27,15 +26,14 @@ use uv_normalize::{DefaultExtras, DefaultGroups, PackageName}; use uv_pep508::{MarkerTree, VersionOrUrl}; use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl, ParsedUrl}; use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest}; -use uv_resolver::{FlatIndex, Installable, Lock}; +use uv_requirements::RequirementsSpecification; +use uv_resolver::{FlatIndex, ForkStrategy, Installable, Lock, PrereleaseMode, ResolutionMode}; use uv_scripts::{Pep723ItemRef, Pep723Script}; use uv_settings::PythonInstallMirrors; use uv_types::{BuildIsolation, HashStrategy}; use uv_warnings::{warn_user, warn_user_once}; use uv_workspace::pyproject::{ExtraBuildDependencies, Source}; -use uv_workspace::{ - DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace, WorkspaceCache, -}; +use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace, WorkspaceCache}; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger}; use crate::commands::pip::operations::Modifications; @@ -50,7 +48,9 @@ use crate::commands::project::{ }; use crate::commands::{ExitStatus, diagnostics}; use crate::printer::Printer; -use crate::settings::{InstallerSettingsRef, NetworkSettings, ResolverInstallerSettings}; +use crate::settings::{ + InstallerSettingsRef, NetworkSettings, ResolverInstallerSettings, ResolverSettings, +}; /// Sync the project environment. #[allow(clippy::fn_params_excessive_bools)] @@ -223,8 +223,18 @@ pub(crate) async fn sync( } // Parse the requirements from the script. - let spec = script_specification(Pep723ItemRef::Script(script), &settings.resolver)? - .unwrap_or_default(); + let script_spec = + script_specification(Pep723ItemRef::Script(script), &settings.resolver)?; + let (spec, script_extra_build_requires) = if let Some(script_spec) = script_spec { + (script_spec.requirements, script_spec.extra_build_requires) + } else { + ( + RequirementsSpecification::default(), + uv_distribution::ExtraBuildRequires::from_lowered( + ExtraBuildDependencies::default(), + ), + ) + }; // Parse the build constraints from the script. let build_constraints = script @@ -249,6 +259,7 @@ pub(crate) async fn sync( spec, modifications, build_constraints.unwrap_or_default(), + script_extra_build_requires, &settings, &network_settings, &PlatformState::default(), @@ -495,6 +506,7 @@ fn identify_installation_target<'a>( } #[derive(Debug, Clone)] +#[allow(clippy::large_enum_variant)] enum SyncTarget { /// Sync a project environment. Project(VirtualProject), @@ -546,7 +558,6 @@ impl Deref for SyncEnvironment { } } - /// Sync a lockfile with an environment. #[allow(clippy::fn_params_excessive_bools)] pub(super) async fn do_sync( @@ -607,12 +618,35 @@ pub(super) async fn do_sync( sources, )? } - InstallTarget::Script { .. } => uv_distribution::ExtraBuildRequires { - extra_build_dependencies: ExtraBuildDependencies::default(), - }, + InstallTarget::Script { script, .. } => { + // Try to get extra build dependencies from the script metadata + let resolver_settings = ResolverSettings { + build_options: build_options.clone(), + config_setting: config_setting.clone(), + config_settings_package: config_settings_package.clone(), + dependency_metadata: dependency_metadata.clone(), + exclude_newer, + fork_strategy: ForkStrategy::default(), + index_locations: index_locations.clone(), + index_strategy, + keyring_provider, + link_mode, + no_build_isolation, + no_build_isolation_package: no_build_isolation_package.to_vec(), + extra_build_dependencies: extra_build_dependencies.clone(), + prerelease: PrereleaseMode::default(), + resolution: ResolutionMode::default(), + sources, + upgrade: Upgrade::default(), + }; + script_specification(Pep723ItemRef::Script(script), &resolver_settings)? + .map(|spec| spec.extra_build_requires) + .unwrap_or_else(|| uv_distribution::ExtraBuildRequires { + extra_build_dependencies: ExtraBuildDependencies::default(), + }) + } }; - let client_builder = BaseClientBuilder::new() .retries_from_env()? .connectivity(network_settings.connectivity) diff --git a/crates/uv/src/commands/tool/install.rs b/crates/uv/src/commands/tool/install.rs index 12de5fd1f..fa4ae243a 100644 --- a/crates/uv/src/commands/tool/install.rs +++ b/crates/uv/src/commands/tool/install.rs @@ -23,7 +23,7 @@ use uv_requirements::{RequirementsSource, RequirementsSpecification}; use uv_settings::{PythonInstallMirrors, ResolverInstallerOptions, ToolOptions}; use uv_tool::InstalledTools; use uv_warnings::warn_user; -use uv_workspace::WorkspaceCache; +use uv_workspace::{WorkspaceCache, pyproject::ExtraBuildDependencies}; use crate::commands::ExitStatus; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger}; @@ -439,6 +439,7 @@ pub(crate) async fn install( spec, Modifications::Exact, Constraints::from_requirements(build_constraints.iter().cloned()), + uv_distribution::ExtraBuildRequires::from_lowered(ExtraBuildDependencies::default()), &settings, &network_settings, &state, diff --git a/crates/uv/src/commands/tool/upgrade.rs b/crates/uv/src/commands/tool/upgrade.rs index 9d2d32a21..1c511524c 100644 --- a/crates/uv/src/commands/tool/upgrade.rs +++ b/crates/uv/src/commands/tool/upgrade.rs @@ -18,7 +18,7 @@ use uv_python::{ use uv_requirements::RequirementsSpecification; use uv_settings::{Combine, PythonInstallMirrors, ResolverInstallerOptions, ToolOptions}; use uv_tool::InstalledTools; -use uv_workspace::WorkspaceCache; +use uv_workspace::{WorkspaceCache, pyproject::ExtraBuildDependencies}; use crate::commands::pip::loggers::{ DefaultInstallLogger, SummaryResolveLogger, UpgradeInstallLogger, @@ -343,6 +343,7 @@ async fn upgrade_tool( spec, Modifications::Exact, build_constraints, + uv_distribution::ExtraBuildRequires::from_lowered(ExtraBuildDependencies::default()), &settings, network_settings, &state, diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index 77477afe8..c2395fc4f 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -275,9 +275,8 @@ pub(crate) async fn venv( // Do not allow builds let build_options = BuildOptions::new(NoBinary::None, NoBuild::All); - let extra_build_requires = uv_distribution::ExtraBuildRequires::from_lowered( - ExtraBuildDependencies::default(), - ); + let extra_build_requires = + uv_distribution::ExtraBuildRequires::from_lowered(ExtraBuildDependencies::default()); // Prep the build context. let build_dispatch = BuildDispatch::new( &client, diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index 00a8b5563..b30f7d6b2 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -3931,6 +3931,7 @@ fn resolve_both_special_fields() -> anyhow::Result<()> { torch_backend: None, no_build_isolation: false, no_build_isolation_package: [], + extra_build_dependencies: {}, build_options: BuildOptions { no_binary: None, no_build: None, diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 74451f35e..6c612fceb 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -1778,7 +1778,7 @@ fn sync_extra_build_dependencies() -> Result<()> { [tool.uv.sources] child = { path = "child" } bad_child = { path = "bad_child" } - + [tool.uv.extra-build-dependencies] child = ["anyio"] "#})?; @@ -1833,7 +1833,7 @@ fn sync_extra_build_dependencies_sources() -> Result<()> { except ModuleNotFoundError: print("Missing `anyio` module", file=sys.stderr) sys.exit(1) - + # Check that we got the local version of anyio by checking for the marker if not hasattr(anyio, 'LOCAL_ANYIO_MARKER'): print("Found system anyio instead of local anyio", file=sys.stderr) @@ -1915,7 +1915,7 @@ fn sync_extra_build_dependencies_sources_from_child() -> Result<()> { except ModuleNotFoundError: print("Missing `anyio` module", file=sys.stderr) sys.exit(1) - + # Check that we got the local version of anyio by checking for the marker if not hasattr(anyio, 'LOCAL_ANYIO_MARKER'): print("Found system anyio instead of local anyio", file=sys.stderr) @@ -4593,6 +4593,187 @@ fn no_install_project_no_build() -> Result<()> { Ok(()) } +#[test] +fn sync_extra_build_dependencies_script() -> Result<()> { + let context = TestContext::new("3.12").with_filtered_counts(); + + // Write a test package that arbitrarily requires `anyio` at build time + let child = context.temp_dir.child("child"); + child.create_dir_all()?; + let child_pyproject_toml = child.child("pyproject.toml"); + child_pyproject_toml.write_str(indoc! {r#" + [project] + name = "child" + version = "0.1.0" + requires-python = ">=3.9" + [build-system] + requires = ["hatchling"] + backend-path = ["."] + build-backend = "build_backend" + "#})?; + let build_backend = child.child("build_backend.py"); + build_backend.write_str(indoc! {r#" + import sys + from hatchling.build import * + try: + import anyio + except ModuleNotFoundError: + print("Missing `anyio` module", file=sys.stderr) + sys.exit(1) + "#})?; + child.child("src/child/__init__.py").touch()?; + + // Create a script that depends on the child package + let script = context.temp_dir.child("script.py"); + script.write_str(indoc! {r#" + # /// script + # requires-python = ">=3.12" + # dependencies = ["child"] + # + # [tool.uv.sources] + # child = { path = "child" } + # /// + "#})?; + + let filters = context + .filters() + .into_iter() + .chain(vec![( + r"environments-v2/script-[a-z0-9]+", + "environments-v2/script-[HASH]", + )]) + .collect::>(); + + // Running `uv sync` should fail due to missing build-dependencies + uv_snapshot!(filters, context.sync().arg("--script").arg("script.py"), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Creating script environment at: [CACHE_DIR]/environments-v2/script-[HASH] + Resolved [N] packages in [TIME] + × Failed to build `child @ file://[TEMP_DIR]/child` + ├─▶ The build backend returned an error + ╰─▶ Call to `build_backend.build_wheel` failed (exit status: 1) + + [stderr] + Missing `anyio` module + + hint: This usually indicates a problem with the package or the build environment. + "); + + // Add extra build dependencies to the script + script.write_str(indoc! {r#" + # /// script + # requires-python = ">=3.12" + # dependencies = ["child"] + # + # [tool.uv.sources] + # child = { path = "child" } + # + # [tool.uv.extra-build-dependencies] + # child = ["anyio"] + # /// + "#})?; + + // Running `uv sync` should now succeed due to extra build-dependencies + context.venv().arg("--clear").assert().success(); + uv_snapshot!(filters, context.sync().arg("--script").arg("script.py"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using script environment at: [CACHE_DIR]/environments-v2/script-[HASH] + Resolved [N] packages in [TIME] + Prepared [N] packages in [TIME] + Installed [N] packages in [TIME] + + child==0.1.0 (from file://[TEMP_DIR]/child) + "); + + Ok(()) +} + +#[test] +fn sync_extra_build_dependencies_script_sources() -> Result<()> { + let context = TestContext::new("3.12").with_filtered_counts(); + let anyio_local = context.workspace_root.join("scripts/packages/anyio_local"); + + // Write a test package that arbitrarily requires `anyio` at a specific _path_ at build time + let child = context.temp_dir.child("child"); + child.create_dir_all()?; + let child_pyproject_toml = child.child("pyproject.toml"); + child_pyproject_toml.write_str(indoc! {r#" + [project] + name = "child" + version = "0.1.0" + requires-python = ">=3.9" + [build-system] + requires = ["hatchling"] + backend-path = ["."] + build-backend = "build_backend" + "#})?; + let build_backend = child.child("build_backend.py"); + build_backend.write_str(&formatdoc! {r#" + import sys + from hatchling.build import * + try: + import anyio + except ModuleNotFoundError: + print("Missing `anyio` module", file=sys.stderr) + sys.exit(1) + + # Check that we got the local version of anyio by checking for the marker + if not hasattr(anyio, 'LOCAL_ANYIO_MARKER'): + print("Found system anyio instead of local anyio", file=sys.stderr) + sys.exit(1) + "#})?; + child.child("src/child/__init__.py").touch()?; + + // Create a script that depends on the child package + let script = context.temp_dir.child("script.py"); + script.write_str(&formatdoc! {r#" + # /// script + # requires-python = ">=3.12" + # dependencies = ["child"] + # + # [tool.uv.sources] + # anyio = {{ path = "{}" }} + # child = {{ path = "child" }} + # + # [tool.uv.extra-build-dependencies] + # child = ["anyio"] + # /// + "#, anyio_local.display() + })?; + + let filters = context + .filters() + .into_iter() + .chain(vec![( + r"environments-v2/script-[a-z0-9]+", + "environments-v2/script-[HASH]", + )]) + .collect::>(); + + // Running `uv sync` should succeed with the sources applied + uv_snapshot!(filters, context.sync().arg("--script").arg("script.py"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Creating script environment at: [CACHE_DIR]/environments-v2/script-[HASH] + Resolved [N] packages in [TIME] + Prepared [N] packages in [TIME] + Installed [N] packages in [TIME] + + child==0.1.0 (from file://[TEMP_DIR]/child) + "); + + Ok(()) +} + #[test] fn virtual_no_build() -> Result<()> { let context = TestContext::new("3.12");