From 74b49695eed1c1e9cae9e18fdfc6629356eb1a9d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 21 Feb 2024 16:23:32 -0500 Subject: [PATCH] Add --config-settings --- Cargo.lock | 3 + crates/uv-build/Cargo.toml | 2 +- crates/uv-build/src/lib.rs | 27 ++- crates/uv-dev/src/build.rs | 5 +- crates/uv-dev/src/install_many.rs | 5 +- crates/uv-dev/src/resolve_cli.rs | 5 +- crates/uv-dev/src/resolve_many.rs | 4 +- crates/uv-dispatch/src/lib.rs | 6 +- crates/uv-traits/Cargo.toml | 7 + crates/uv-traits/src/lib.rs | 168 ++++++++++++++++++ crates/uv/src/commands/pip_compile.rs | 4 +- crates/uv/src/commands/pip_install.rs | 5 +- crates/uv/src/commands/pip_sync.rs | 4 +- crates/uv/src/commands/venv.rs | 6 +- crates/uv/src/main.rs | 57 ++++-- crates/uv/tests/pip_install.rs | 102 +++++++++++ .../setuptools_editable/.gitignore | 2 + .../setuptools_editable/pyproject.toml | 13 ++ .../setuptools_editable/__init__.py | 2 + 19 files changed, 392 insertions(+), 35 deletions(-) create mode 100644 scripts/editable-installs/setuptools_editable/.gitignore create mode 100644 scripts/editable-installs/setuptools_editable/pyproject.toml create mode 100644 scripts/editable-installs/setuptools_editable/setuptools_editable/__init__.py diff --git a/Cargo.lock b/Cargo.lock index 936a54bc540b..ae7ddb042bb7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4600,9 +4600,12 @@ name = "uv-traits" version = "0.0.1" dependencies = [ "anyhow", + "clap", "distribution-types", "once-map", "pep508_rs", + "serde", + "serde_json", "tokio", "uv-cache", "uv-interpreter", diff --git a/crates/uv-build/Cargo.toml b/crates/uv-build/Cargo.toml index 8dabecfa82bc..0b7bd17702b1 100644 --- a/crates/uv-build/Cargo.toml +++ b/crates/uv-build/Cargo.toml @@ -21,7 +21,7 @@ platform-host = { path = "../platform-host" } uv-extract = { path = "../uv-extract" } uv-fs = { path = "../uv-fs" } uv-interpreter = { path = "../uv-interpreter" } -uv-traits = { path = "../uv-traits" } +uv-traits = { path = "../uv-traits", features = ["serde"] } pypi-types = { path = "../pypi-types" } anyhow = { workspace = true } diff --git a/crates/uv-build/src/lib.rs b/crates/uv-build/src/lib.rs index d63ba89f6749..7de745b1cebb 100644 --- a/crates/uv-build/src/lib.rs +++ b/crates/uv-build/src/lib.rs @@ -29,7 +29,7 @@ use distribution_types::Resolution; use pep508_rs::Requirement; use uv_fs::Normalized; use uv_interpreter::{Interpreter, Virtualenv}; -use uv_traits::{BuildContext, BuildKind, SetupPyStrategy, SourceBuildTrait}; +use uv_traits::{BuildContext, BuildKind, ConfigSettings, SetupPyStrategy, SourceBuildTrait}; /// e.g. `pygraphviz/graphviz_wrap.c:3020:10: fatal error: graphviz/cgraph.h: No such file or directory` static MISSING_HEADER_RE: Lazy = Lazy::new(|| { @@ -247,6 +247,7 @@ pub struct SourceBuildContext { pub struct SourceBuild { temp_dir: TempDir, source_tree: PathBuf, + config_settings: ConfigSettings, /// If performing a PEP 517 build, the backend to use. pep517_backend: Option, /// The virtual environment in which to build the source distribution. @@ -281,6 +282,7 @@ impl SourceBuild { source_build_context: SourceBuildContext, package_id: String, setup_py: SetupPyStrategy, + config_settings: ConfigSettings, build_kind: BuildKind, ) -> Result { let temp_dir = tempdir_in(build_context.cache().root())?; @@ -354,6 +356,7 @@ impl SourceBuild { build_context, &package_id, build_kind, + &config_settings, ) .await?; } @@ -364,6 +367,7 @@ impl SourceBuild { pep517_backend, venv, build_kind, + config_settings, metadata_directory: None, package_id, }) @@ -492,10 +496,13 @@ impl SourceBuild { prepare_metadata_for_build_wheel = getattr(backend, "prepare_metadata_for_build_wheel", None) if prepare_metadata_for_build_wheel: - print(prepare_metadata_for_build_wheel("{}")) + print(prepare_metadata_for_build_wheel("{}", config_settings={})) else: print() - "#, pep517_backend.backend_import(), escape_path_for_python(&metadata_directory) + "#, + pep517_backend.backend_import(), + escape_path_for_python(&metadata_directory), + self.config_settings.escape_for_python(), }; let span = info_span!( "run_python_script", @@ -619,8 +626,13 @@ impl SourceBuild { let escaped_wheel_dir = escape_path_for_python(wheel_dir); let script = formatdoc! { r#"{} - print(backend.build_{}("{}", metadata_directory={})) - "#, pep517_backend.backend_import(), self.build_kind, escaped_wheel_dir, metadata_directory + print(backend.build_{}("{}", metadata_directory={}, config_settings={})) + "#, + pep517_backend.backend_import(), + self.build_kind, + escaped_wheel_dir, + metadata_directory, + self.config_settings.escape_for_python() }; let span = info_span!( "run_python_script", @@ -682,6 +694,7 @@ async fn create_pep517_build_environment( build_context: &impl BuildContext, package_id: &str, build_kind: BuildKind, + config_settings: &ConfigSettings, ) -> Result<(), Error> { debug!( "Calling `{}.get_requires_for_build_{}()`", @@ -694,11 +707,11 @@ async fn create_pep517_build_environment( get_requires_for_build = getattr(backend, "get_requires_for_build_{}", None) if get_requires_for_build: - requires = get_requires_for_build() + requires = get_requires_for_build(config_settings={}) else: requires = [] print(json.dumps(requires)) - "#, pep517_backend.backend_import(), build_kind + "#, pep517_backend.backend_import(), build_kind, config_settings.escape_for_python() }; let span = info_span!( "run_python_script", diff --git a/crates/uv-dev/src/build.rs b/crates/uv-dev/src/build.rs index b8ad64f389b7..42ef947e7264 100644 --- a/crates/uv-dev/src/build.rs +++ b/crates/uv-dev/src/build.rs @@ -14,7 +14,7 @@ use uv_dispatch::BuildDispatch; use uv_installer::NoBinary; use uv_interpreter::Virtualenv; use uv_resolver::InMemoryIndex; -use uv_traits::{BuildContext, BuildKind, InFlight, NoBuild, SetupPyStrategy}; +use uv_traits::{BuildContext, BuildKind, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; #[derive(Parser)] pub(crate) struct BuildArgs { @@ -61,6 +61,7 @@ pub(crate) async fn build(args: BuildArgs) -> Result { let index = InMemoryIndex::default(); let setup_py = SetupPyStrategy::default(); let in_flight = InFlight::default(); + let config_settings = ConfigSettings::default(); let build_dispatch = BuildDispatch::new( &client, @@ -72,6 +73,7 @@ pub(crate) async fn build(args: BuildArgs) -> Result { &in_flight, venv.python_executable(), setup_py, + &config_settings, &NoBuild::None, &NoBinary::None, ); @@ -84,6 +86,7 @@ pub(crate) async fn build(args: BuildArgs) -> Result { SourceBuildContext::default(), args.sdist.display().to_string(), setup_py, + config_settings.clone(), build_kind, ) .await?; diff --git a/crates/uv-dev/src/install_many.rs b/crates/uv-dev/src/install_many.rs index 0c8f0e5ffad2..ef1f2a1738e4 100644 --- a/crates/uv-dev/src/install_many.rs +++ b/crates/uv-dev/src/install_many.rs @@ -25,7 +25,7 @@ use uv_installer::{Downloader, NoBinary}; use uv_interpreter::Virtualenv; use uv_normalize::PackageName; use uv_resolver::{DistFinder, InMemoryIndex}; -use uv_traits::{BuildContext, InFlight, NoBuild, SetupPyStrategy}; +use uv_traits::{BuildContext, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; #[derive(Parser)] pub(crate) struct InstallManyArgs { @@ -65,12 +65,12 @@ pub(crate) async fn install_many(args: InstallManyArgs) -> Result<()> { let setup_py = SetupPyStrategy::default(); let in_flight = InFlight::default(); let tags = venv.interpreter().tags()?; - let no_build = if args.no_build { NoBuild::All } else { NoBuild::None }; + let config_settings = ConfigSettings::default(); let build_dispatch = BuildDispatch::new( &client, @@ -82,6 +82,7 @@ pub(crate) async fn install_many(args: InstallManyArgs) -> Result<()> { &in_flight, venv.python_executable(), setup_py, + &config_settings, &no_build, &NoBinary::None, ); diff --git a/crates/uv-dev/src/resolve_cli.rs b/crates/uv-dev/src/resolve_cli.rs index de9801f2d5fe..eee9606cc905 100644 --- a/crates/uv-dev/src/resolve_cli.rs +++ b/crates/uv-dev/src/resolve_cli.rs @@ -18,7 +18,7 @@ use uv_dispatch::BuildDispatch; use uv_installer::NoBinary; use uv_interpreter::Virtualenv; use uv_resolver::{InMemoryIndex, Manifest, Options, Resolver}; -use uv_traits::{InFlight, NoBuild, SetupPyStrategy}; +use uv_traits::{ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; #[derive(ValueEnum, Default, Clone)] pub(crate) enum ResolveCliFormat { @@ -72,12 +72,12 @@ pub(crate) async fn resolve_cli(args: ResolveCliArgs) -> Result<()> { }; let index = InMemoryIndex::default(); let in_flight = InFlight::default(); - let no_build = if args.no_build { NoBuild::All } else { NoBuild::None }; + let config_settings = ConfigSettings::default(); let build_dispatch = BuildDispatch::new( &client, @@ -89,6 +89,7 @@ pub(crate) async fn resolve_cli(args: ResolveCliArgs) -> Result<()> { &in_flight, venv.python_executable(), SetupPyStrategy::default(), + &config_settings, &no_build, &NoBinary::None, ); diff --git a/crates/uv-dev/src/resolve_many.rs b/crates/uv-dev/src/resolve_many.rs index e5c3b3802159..96cb7085b315 100644 --- a/crates/uv-dev/src/resolve_many.rs +++ b/crates/uv-dev/src/resolve_many.rs @@ -21,7 +21,7 @@ use uv_installer::NoBinary; use uv_interpreter::Virtualenv; use uv_normalize::PackageName; use uv_resolver::InMemoryIndex; -use uv_traits::{BuildContext, InFlight, NoBuild, SetupPyStrategy}; +use uv_traits::{BuildContext, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; #[derive(Parser)] pub(crate) struct ResolveManyArgs { @@ -96,6 +96,7 @@ pub(crate) async fn resolve_many(args: ResolveManyArgs) -> Result<()> { let index_locations = IndexLocations::default(); let setup_py = SetupPyStrategy::default(); let flat_index = FlatIndex::default(); + let config_settings = ConfigSettings::default(); // Create a `BuildDispatch` for each requirement. let build_dispatch = BuildDispatch::new( @@ -108,6 +109,7 @@ pub(crate) async fn resolve_many(args: ResolveManyArgs) -> Result<()> { &in_flight, venv.python_executable(), setup_py, + &config_settings, &no_build, &NoBinary::None, ); diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index 320aae87b8b9..4601fc52bc05 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -18,7 +18,7 @@ use uv_client::{FlatIndex, RegistryClient}; use uv_installer::{Downloader, Installer, NoBinary, Plan, Planner, Reinstall, SitePackages}; use uv_interpreter::{Interpreter, Virtualenv}; use uv_resolver::{InMemoryIndex, Manifest, Options, Resolver}; -use uv_traits::{BuildContext, BuildKind, InFlight, NoBuild, SetupPyStrategy}; +use uv_traits::{BuildContext, BuildKind, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; /// The main implementation of [`BuildContext`], used by the CLI, see [`BuildContext`] /// documentation. @@ -34,6 +34,7 @@ pub struct BuildDispatch<'a> { setup_py: SetupPyStrategy, no_build: &'a NoBuild, no_binary: &'a NoBinary, + config_settings: &'a ConfigSettings, source_build_context: SourceBuildContext, options: Options, } @@ -50,6 +51,7 @@ impl<'a> BuildDispatch<'a> { in_flight: &'a InFlight, base_python: PathBuf, setup_py: SetupPyStrategy, + config_settings: &'a ConfigSettings, no_build: &'a NoBuild, no_binary: &'a NoBinary, ) -> Self { @@ -63,6 +65,7 @@ impl<'a> BuildDispatch<'a> { in_flight, base_python, setup_py, + config_settings, no_build, no_binary, source_build_context: SourceBuildContext::default(), @@ -279,6 +282,7 @@ impl<'a> BuildContext for BuildDispatch<'a> { self.source_build_context.clone(), package_id.to_string(), self.setup_py, + self.config_settings.clone(), build_kind, ) .boxed() diff --git a/crates/uv-traits/Cargo.toml b/crates/uv-traits/Cargo.toml index 7af63adc159c..3f671c1f6947 100644 --- a/crates/uv-traits/Cargo.toml +++ b/crates/uv-traits/Cargo.toml @@ -13,6 +13,7 @@ license = { workspace = true } workspace = true [dependencies] +clap = { workspace = true, optional = true } distribution-types = { path = "../distribution-types" } once-map = { path = "../once-map" } pep508_rs = { path = "../pep508-rs" } @@ -21,4 +22,10 @@ uv-interpreter = { path = "../uv-interpreter" } uv-normalize = { path = "../uv-normalize" } anyhow = { workspace = true } +serde = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } tokio = { workspace = true, features = ["sync"] } + +[features] +default = [] +serde = ["dep:serde", "dep:serde_json"] diff --git a/crates/uv-traits/src/lib.rs b/crates/uv-traits/src/lib.rs index 7df5480e5991..df66fda754cc 100644 --- a/crates/uv-traits/src/lib.rs +++ b/crates/uv-traits/src/lib.rs @@ -1,11 +1,14 @@ //! Avoid cyclic crate dependencies between resolver, installer and builder. +use std::collections::btree_map::Entry; +use std::collections::BTreeMap; use std::fmt::{Display, Formatter}; use std::future::Future; use std::path::{Path, PathBuf}; use std::str::FromStr; use anyhow::Result; +use serde::ser::SerializeMap; use distribution_types::{CachedDist, DistributionId, IndexLocations, Resolution, SourceDist}; use once_map::OnceMap; @@ -288,6 +291,94 @@ impl NoBuild { } } +#[derive(Debug, Clone)] +pub struct ConfigSettingEntry { + /// The key of the setting. For example, given `key=value`, this would be `key`. + key: String, + /// The value of the setting. For example, given `key=value`, this would be `value`. + value: String, +} + +impl FromStr for ConfigSettingEntry { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let Some((key, value)) = s.split_once('=') else { + return Err(anyhow::anyhow!( + "Invalid config setting: {s} (expected `KEY=VALUE`)" + )); + }; + Ok(Self { + key: key.trim().to_string(), + value: value.trim().to_string(), + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum ConfigSettingValue { + /// The value consists of a single string. + String(String), + /// The value consists of a list of strings. + List(Vec), +} + +/// Settings to pass to a PEP 517 build backend, structured as a map from (string) key to string or +/// list of strings. +/// +/// See: +#[derive(Debug, Default, Clone)] +pub struct ConfigSettings(BTreeMap); + +impl FromIterator for ConfigSettings { + fn from_iter>(iter: T) -> Self { + let mut config = BTreeMap::default(); + for entry in iter { + match config.entry(entry.key) { + Entry::Vacant(vacant) => { + vacant.insert(ConfigSettingValue::String(entry.value)); + } + Entry::Occupied(mut occupied) => match occupied.get_mut() { + ConfigSettingValue::String(existing) => { + let existing = existing.clone(); + occupied.insert(ConfigSettingValue::List(vec![existing, entry.value])); + } + ConfigSettingValue::List(existing) => { + existing.push(entry.value); + } + }, + } + } + Self(config) + } +} + +#[cfg(feature = "serde")] +impl ConfigSettings { + /// Convert the settings to a string that can be passed directly to a PEP 517 build backend. + pub fn escape_for_python(&self) -> String { + serde_json::to_string(self).expect("Failed to serialize config settings") + } +} + +#[cfg(feature = "serde")] +impl serde::Serialize for ConfigSettings { + fn serialize(&self, serializer: S) -> Result { + let mut map = serializer.serialize_map(Some(self.0.len()))?; + for (key, value) in &self.0 { + match value { + ConfigSettingValue::String(value) => { + map.serialize_entry(&key, &value)?; + } + ConfigSettingValue::List(values) => { + map.serialize_entry(&key, &values)?; + } + } + } + map.end() + } +} + #[cfg(test)] mod tests { use anyhow::Error; @@ -349,4 +440,81 @@ mod tests { Ok(()) } + + #[test] + fn collect_config_settings() { + let settings: ConfigSettings = vec![ + ConfigSettingEntry { + key: "key".to_string(), + value: "value".to_string(), + }, + ConfigSettingEntry { + key: "key".to_string(), + value: "value2".to_string(), + }, + ConfigSettingEntry { + key: "list".to_string(), + value: "value3".to_string(), + }, + ConfigSettingEntry { + key: "list".to_string(), + value: "value4".to_string(), + }, + ] + .into_iter() + .collect(); + assert_eq!( + settings.0.get("key"), + Some(&ConfigSettingValue::List(vec![ + "value".to_string(), + "value2".to_string() + ])) + ); + assert_eq!( + settings.0.get("list"), + Some(&ConfigSettingValue::List(vec![ + "value3".to_string(), + "value4".to_string() + ])) + ); + } + + #[test] + #[cfg(feature = "serde")] + fn escape_for_python() { + let mut settings = ConfigSettings::default(); + settings.0.insert( + "key".to_string(), + ConfigSettingValue::String("value".to_string()), + ); + settings.0.insert( + "list".to_string(), + ConfigSettingValue::List(vec!["value1".to_string(), "value2".to_string()]), + ); + assert_eq!( + settings.escape_for_python(), + r#"{"key":"value","list":["value1","value2"]}"# + ); + + let mut settings = ConfigSettings::default(); + settings.0.insert( + "key".to_string(), + ConfigSettingValue::String("Hello, \"world!\"".to_string()), + ); + settings.0.insert( + "list".to_string(), + ConfigSettingValue::List(vec!["'value1'".to_string()]), + ); + assert_eq!( + settings.escape_for_python(), + r#"{"key":"Hello, \"world!\"","list":["'value1'"]}"# + ); + + let mut settings = ConfigSettings::default(); + settings.0.insert( + "key".to_string(), + ConfigSettingValue::String("val\\1 {}ue".to_string()), + ); + assert_eq!(settings.escape_for_python(), r#"{"key":"val\\1 {}ue"}"#); + } } diff --git a/crates/uv/src/commands/pip_compile.rs b/crates/uv/src/commands/pip_compile.rs index 12f6a2415e7d..602fe2900947 100644 --- a/crates/uv/src/commands/pip_compile.rs +++ b/crates/uv/src/commands/pip_compile.rs @@ -31,7 +31,7 @@ use uv_resolver::{ AnnotationStyle, DependencyMode, DisplayResolutionGraph, InMemoryIndex, Manifest, OptionsBuilder, PreReleaseMode, ResolutionMode, Resolver, }; -use uv_traits::{InFlight, NoBuild, SetupPyStrategy}; +use uv_traits::{ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; use uv_warnings::warn_user; use crate::commands::reporters::{DownloadReporter, ResolverReporter}; @@ -58,6 +58,7 @@ pub(crate) async fn pip_compile( include_find_links: bool, index_locations: IndexLocations, setup_py: SetupPyStrategy, + config_settings: ConfigSettings, connectivity: Connectivity, no_build: &NoBuild, python_version: Option, @@ -219,6 +220,7 @@ pub(crate) async fn pip_compile( &in_flight, interpreter.sys_executable().to_path_buf(), setup_py, + &config_settings, no_build, &NoBinary::None, ) diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index 693abe2b6426..f9a931fde2ca 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -33,7 +33,7 @@ use uv_resolver::{ DependencyMode, InMemoryIndex, Manifest, Options, OptionsBuilder, PreReleaseMode, ResolutionGraph, ResolutionMode, Resolver, }; -use uv_traits::{InFlight, NoBuild, SetupPyStrategy}; +use uv_traits::{ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; use crate::commands::reporters::{DownloadReporter, InstallReporter, ResolverReporter}; use crate::commands::{elapsed, ChangeEvent, ChangeEventKind, ExitStatus}; @@ -58,6 +58,7 @@ pub(crate) async fn pip_install( link_mode: LinkMode, setup_py: SetupPyStrategy, connectivity: Connectivity, + config_settings: &ConfigSettings, no_build: &NoBuild, no_binary: &NoBinary, strict: bool, @@ -173,6 +174,7 @@ pub(crate) async fn pip_install( &in_flight, venv.python_executable(), setup_py, + config_settings, no_build, no_binary, ) @@ -255,6 +257,7 @@ pub(crate) async fn pip_install( &in_flight, venv.python_executable(), setup_py, + config_settings, no_build, no_binary, ) diff --git a/crates/uv/src/commands/pip_sync.rs b/crates/uv/src/commands/pip_sync.rs index 08a51b1c1282..39aa3523bbad 100644 --- a/crates/uv/src/commands/pip_sync.rs +++ b/crates/uv/src/commands/pip_sync.rs @@ -20,7 +20,7 @@ use uv_installer::{ }; use uv_interpreter::Virtualenv; use uv_resolver::InMemoryIndex; -use uv_traits::{InFlight, NoBuild, SetupPyStrategy}; +use uv_traits::{ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; use crate::commands::reporters::{DownloadReporter, FinderReporter, InstallReporter}; use crate::commands::{elapsed, ChangeEvent, ChangeEventKind, ExitStatus}; @@ -36,6 +36,7 @@ pub(crate) async fn pip_sync( index_locations: IndexLocations, setup_py: SetupPyStrategy, connectivity: Connectivity, + config_settings: &ConfigSettings, no_build: &NoBuild, no_binary: &NoBinary, strict: bool, @@ -112,6 +113,7 @@ pub(crate) async fn pip_sync( &in_flight, venv.python_executable(), setup_py, + config_settings, no_build, no_binary, ); diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index ba38edd36382..aa6f6e9f9a24 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -22,7 +22,7 @@ use uv_fs::Normalized; use uv_installer::NoBinary; use uv_interpreter::{find_default_python, find_requested_python, Error}; use uv_resolver::{InMemoryIndex, OptionsBuilder}; -use uv_traits::{BuildContext, InFlight, NoBuild, SetupPyStrategy}; +use uv_traits::{BuildContext, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; use crate::commands::ExitStatus; use crate::printer::Printer; @@ -154,6 +154,9 @@ async fn venv_impl( // Track in-flight downloads, builds, etc., across resolutions. let in_flight = InFlight::default(); + // For seed packages, assume the default settings are sufficient. + let config_settings = ConfigSettings::default(); + // Prep the build context. let build_dispatch = BuildDispatch::new( &client, @@ -165,6 +168,7 @@ async fn venv_impl( &in_flight, venv.python_executable(), SetupPyStrategy::default(), + &config_settings, &NoBuild::All, &NoBinary::None, ) diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index 218cdc4213ba..0aae1cff0458 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -20,7 +20,9 @@ use uv_installer::{NoBinary, Reinstall}; use uv_interpreter::PythonVersion; use uv_normalize::{ExtraName, PackageName}; use uv_resolver::{AnnotationStyle, DependencyMode, PreReleaseMode, ResolutionMode}; -use uv_traits::{NoBuild, PackageNameSpecifier, SetupPyStrategy}; +use uv_traits::{ + ConfigSettingEntry, ConfigSettings, NoBuild, PackageNameSpecifier, SetupPyStrategy, +}; use crate::commands::{extra_name_with_clap_error, ExitStatus, Upgrade}; use crate::compat::CompatArgs; @@ -331,6 +333,10 @@ struct PipCompileArgs { #[clap(long, conflicts_with = "no_build")] only_binary: Vec, + /// Settings to pass to the PEP 517 build backend, specified as `KEY=VALUE` pairs. + #[clap(long, short = 'C', alias = "config-settings")] + config_setting: Vec, + /// The minimum Python version that should be supported by the compiled requirements (e.g., /// `3.7` or `3.7.9`). /// @@ -456,6 +462,10 @@ struct PipSyncArgs { #[clap(long, conflicts_with = "no_build")] only_binary: Vec, + /// Settings to pass to the PEP 517 build backend, specified as `KEY=VALUE` pairs. + #[clap(long, short = 'C', alias = "config-settings")] + config_setting: Vec, + /// Validate the virtual environment after completing the installation, to detect packages with /// missing dependencies or other issues. #[clap(long)] @@ -621,6 +631,10 @@ struct PipInstallArgs { #[clap(long, conflicts_with = "no_build")] only_binary: Vec, + /// Settings to pass to the PEP 517 build backend, specified as `KEY=VALUE` pairs. + #[clap(long, short = 'C', alias = "config-settings")] + config_setting: Vec, + /// Validate the virtual environment after completing the installation, to detect packages with /// missing dependencies or other issues. #[clap(long)] @@ -873,6 +887,12 @@ async fn run() -> Result { } else { DependencyMode::Transitive }; + let setup_py = if args.legacy_setup_py { + SetupPyStrategy::Setuptools + } else { + SetupPyStrategy::Pep517 + }; + let config_settings = args.config_setting.into_iter().collect::(); commands::pip_compile( &requirements, &constraints, @@ -889,11 +909,8 @@ async fn run() -> Result { args.emit_index_url, args.emit_find_links, index_urls, - if args.legacy_setup_py { - SetupPyStrategy::Setuptools - } else { - SetupPyStrategy::Pep517 - }, + setup_py, + config_settings, if args.offline { Connectivity::Offline } else { @@ -928,21 +945,25 @@ async fn run() -> Result { let reinstall = Reinstall::from_args(args.reinstall, args.reinstall_package); let no_binary = NoBinary::from_args(args.no_binary); let no_build = NoBuild::from_args(args.only_binary, args.no_build); + let setup_py = if args.legacy_setup_py { + SetupPyStrategy::Setuptools + } else { + SetupPyStrategy::Pep517 + }; + let config_settings = args.config_setting.into_iter().collect::(); + commands::pip_sync( &sources, &reinstall, args.link_mode, index_urls, - if args.legacy_setup_py { - SetupPyStrategy::Setuptools - } else { - SetupPyStrategy::Pep517 - }, + setup_py, if args.offline { Connectivity::Offline } else { Connectivity::Online }, + &config_settings, &no_build, &no_binary, args.strict, @@ -998,6 +1019,13 @@ async fn run() -> Result { } else { DependencyMode::Transitive }; + let setup_py = if args.legacy_setup_py { + SetupPyStrategy::Setuptools + } else { + SetupPyStrategy::Pep517 + }; + let config_settings = args.config_setting.into_iter().collect::(); + commands::pip_install( &requirements, &constraints, @@ -1010,16 +1038,13 @@ async fn run() -> Result { index_urls, &reinstall, args.link_mode, - if args.legacy_setup_py { - SetupPyStrategy::Setuptools - } else { - SetupPyStrategy::Pep517 - }, + setup_py, if args.offline { Connectivity::Offline } else { Connectivity::Online }, + &config_settings, &no_build, &no_binary, args.strict, diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index 5d6c2d054540..6cc7a6b8eb78 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -1598,3 +1598,105 @@ fn launcher_with_symlink() -> Result<()> { Ok(()) } + +#[test] +#[cfg(unix)] +fn config_settings() -> Result<()> { + let context = TestContext::new("3.12"); + + let current_dir = std::env::current_dir()?; + let workspace_dir = regex::escape( + Url::from_directory_path(current_dir.join("..").join("..").canonicalize()?) + .unwrap() + .as_str(), + ); + + let filters = [(workspace_dir.as_str(), "file://[WORKSPACE_DIR]/")] + .into_iter() + .chain(INSTA_FILTERS.to_vec()) + .collect::>(); + + // Install the editable package. + uv_snapshot!(filters, Command::new(get_bin()) + .arg("pip") + .arg("install") + .arg("-e") + .arg("../../scripts/editable-installs/setuptools_editable") + .arg("--cache-dir") + .arg(context.cache_dir.path()) + .arg("--exclude-newer") + .arg(EXCLUDE_NEWER) + .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("CARGO_TARGET_DIR", "../../../target/target_install_editable"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Built 1 editable in [TIME] + Resolved 2 packages in [TIME] + Downloaded 1 package in [TIME] + Installed 2 packages in [TIME] + + iniconfig==2.0.0 + + setuptools-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/setuptools_editable) + "### + ); + + // When installed without `--editable_mode=compat`, the `finder.py` file should be present. + let finder = context + .venv + .join("lib/python3.12/site-packages") + .join("__editable___setuptools_editable_0_1_0_finder.py"); + assert!(finder.exists()); + + // Install the editable package with `--editable_mode=compat`. + let context = TestContext::new("3.12"); + + let current_dir = std::env::current_dir()?; + let workspace_dir = regex::escape( + Url::from_directory_path(current_dir.join("..").join("..").canonicalize()?) + .unwrap() + .as_str(), + ); + + let filters = [(workspace_dir.as_str(), "file://[WORKSPACE_DIR]/")] + .into_iter() + .chain(INSTA_FILTERS.to_vec()) + .collect::>(); + + uv_snapshot!(filters, Command::new(get_bin()) + .arg("pip") + .arg("install") + .arg("-e") + .arg("../../scripts/editable-installs/setuptools_editable") + .arg("-C") + .arg("editable_mode=compat") + .arg("--cache-dir") + .arg(context.cache_dir.path()) + .arg("--exclude-newer") + .arg(EXCLUDE_NEWER) + .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("CARGO_TARGET_DIR", "../../../target/target_install_editable"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Built 1 editable in [TIME] + Resolved 2 packages in [TIME] + Downloaded 1 package in [TIME] + Installed 2 packages in [TIME] + + iniconfig==2.0.0 + + setuptools-editable==0.1.0 (from file://[WORKSPACE_DIR]/scripts/editable-installs/setuptools_editable) + "### + ); + + // When installed without `--editable_mode=compat`, the `finder.py` file should _not_ be present. + let finder = context + .venv + .join("lib/python3.12/site-packages") + .join("__editable___setuptools_editable_0_1_0_finder.py"); + assert!(!finder.exists()); + + Ok(()) +} diff --git a/scripts/editable-installs/setuptools_editable/.gitignore b/scripts/editable-installs/setuptools_editable/.gitignore new file mode 100644 index 000000000000..eaa9f051bc40 --- /dev/null +++ b/scripts/editable-installs/setuptools_editable/.gitignore @@ -0,0 +1,2 @@ +# Artifacts from the build process. +*.egg-info/ diff --git a/scripts/editable-installs/setuptools_editable/pyproject.toml b/scripts/editable-installs/setuptools_editable/pyproject.toml new file mode 100644 index 000000000000..f23ea7c8c7e0 --- /dev/null +++ b/scripts/editable-installs/setuptools_editable/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "setuptools_editable" +version = "0.1.0" +description = "Default template for a setuptools project" +authors = [ + {name = "konstin", email = "konstin@mailbox.org"}, +] +dependencies = ["iniconfig"] +requires-python = ">=3.11,<3.13" +license = {text = "MIT"} + +[project.optional-dependencies] +anyio = ["anyio>=3.3.0"] diff --git a/scripts/editable-installs/setuptools_editable/setuptools_editable/__init__.py b/scripts/editable-installs/setuptools_editable/setuptools_editable/__init__.py new file mode 100644 index 000000000000..b2dde251b1b4 --- /dev/null +++ b/scripts/editable-installs/setuptools_editable/setuptools_editable/__init__.py @@ -0,0 +1,2 @@ +def a(): + pass