Add support for config_settings in PEP 517 hooks (#1833)

## Summary

Adds `--config-setting` / `-C` (with a `--config-settings` alias for
convenience) to the CLI.

Closes https://github.com/astral-sh/uv/issues/1460.
This commit is contained in:
Charlie Marsh 2024-02-22 19:53:45 -05:00 committed by GitHub
parent 1103298e6c
commit aa73a4f0ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 392 additions and 35 deletions

3
Cargo.lock generated
View file

@ -4600,9 +4600,12 @@ name = "uv-traits"
version = "0.0.1" version = "0.0.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap",
"distribution-types", "distribution-types",
"once-map", "once-map",
"pep508_rs", "pep508_rs",
"serde",
"serde_json",
"tokio", "tokio",
"uv-cache", "uv-cache",
"uv-interpreter", "uv-interpreter",

View file

@ -21,7 +21,7 @@ platform-host = { path = "../platform-host" }
uv-extract = { path = "../uv-extract" } uv-extract = { path = "../uv-extract" }
uv-fs = { path = "../uv-fs" } uv-fs = { path = "../uv-fs" }
uv-interpreter = { path = "../uv-interpreter" } uv-interpreter = { path = "../uv-interpreter" }
uv-traits = { path = "../uv-traits" } uv-traits = { path = "../uv-traits", features = ["serde"] }
pypi-types = { path = "../pypi-types" } pypi-types = { path = "../pypi-types" }
anyhow = { workspace = true } anyhow = { workspace = true }

View file

@ -29,7 +29,7 @@ use distribution_types::Resolution;
use pep508_rs::Requirement; use pep508_rs::Requirement;
use uv_fs::Normalized; use uv_fs::Normalized;
use uv_interpreter::{Interpreter, Virtualenv}; 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` /// e.g. `pygraphviz/graphviz_wrap.c:3020:10: fatal error: graphviz/cgraph.h: No such file or directory`
static MISSING_HEADER_RE: Lazy<Regex> = Lazy::new(|| { static MISSING_HEADER_RE: Lazy<Regex> = Lazy::new(|| {
@ -247,6 +247,7 @@ pub struct SourceBuildContext {
pub struct SourceBuild { pub struct SourceBuild {
temp_dir: TempDir, temp_dir: TempDir,
source_tree: PathBuf, source_tree: PathBuf,
config_settings: ConfigSettings,
/// If performing a PEP 517 build, the backend to use. /// If performing a PEP 517 build, the backend to use.
pep517_backend: Option<Pep517Backend>, pep517_backend: Option<Pep517Backend>,
/// The virtual environment in which to build the source distribution. /// The virtual environment in which to build the source distribution.
@ -281,6 +282,7 @@ impl SourceBuild {
source_build_context: SourceBuildContext, source_build_context: SourceBuildContext,
package_id: String, package_id: String,
setup_py: SetupPyStrategy, setup_py: SetupPyStrategy,
config_settings: ConfigSettings,
build_kind: BuildKind, build_kind: BuildKind,
) -> Result<SourceBuild, Error> { ) -> Result<SourceBuild, Error> {
let temp_dir = tempdir_in(build_context.cache().root())?; let temp_dir = tempdir_in(build_context.cache().root())?;
@ -354,6 +356,7 @@ impl SourceBuild {
build_context, build_context,
&package_id, &package_id,
build_kind, build_kind,
&config_settings,
) )
.await?; .await?;
} }
@ -364,6 +367,7 @@ impl SourceBuild {
pep517_backend, pep517_backend,
venv, venv,
build_kind, build_kind,
config_settings,
metadata_directory: None, metadata_directory: None,
package_id, package_id,
}) })
@ -492,10 +496,13 @@ impl SourceBuild {
prepare_metadata_for_build_wheel = getattr(backend, "prepare_metadata_for_build_wheel", None) prepare_metadata_for_build_wheel = getattr(backend, "prepare_metadata_for_build_wheel", None)
if prepare_metadata_for_build_wheel: if prepare_metadata_for_build_wheel:
print(prepare_metadata_for_build_wheel("{}")) print(prepare_metadata_for_build_wheel("{}", config_settings={}))
else: else:
print() 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!( let span = info_span!(
"run_python_script", "run_python_script",
@ -619,8 +626,13 @@ impl SourceBuild {
let escaped_wheel_dir = escape_path_for_python(wheel_dir); let escaped_wheel_dir = escape_path_for_python(wheel_dir);
let script = formatdoc! { let script = formatdoc! {
r#"{} r#"{}
print(backend.build_{}("{}", metadata_directory={})) print(backend.build_{}("{}", metadata_directory={}, config_settings={}))
"#, pep517_backend.backend_import(), self.build_kind, escaped_wheel_dir, metadata_directory "#,
pep517_backend.backend_import(),
self.build_kind,
escaped_wheel_dir,
metadata_directory,
self.config_settings.escape_for_python()
}; };
let span = info_span!( let span = info_span!(
"run_python_script", "run_python_script",
@ -682,6 +694,7 @@ async fn create_pep517_build_environment(
build_context: &impl BuildContext, build_context: &impl BuildContext,
package_id: &str, package_id: &str,
build_kind: BuildKind, build_kind: BuildKind,
config_settings: &ConfigSettings,
) -> Result<(), Error> { ) -> Result<(), Error> {
debug!( debug!(
"Calling `{}.get_requires_for_build_{}()`", "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) get_requires_for_build = getattr(backend, "get_requires_for_build_{}", None)
if get_requires_for_build: if get_requires_for_build:
requires = get_requires_for_build() requires = get_requires_for_build(config_settings={})
else: else:
requires = [] requires = []
print(json.dumps(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!( let span = info_span!(
"run_python_script", "run_python_script",

View file

@ -14,7 +14,7 @@ use uv_dispatch::BuildDispatch;
use uv_installer::NoBinary; use uv_installer::NoBinary;
use uv_interpreter::Virtualenv; use uv_interpreter::Virtualenv;
use uv_resolver::InMemoryIndex; use uv_resolver::InMemoryIndex;
use uv_traits::{BuildContext, BuildKind, InFlight, NoBuild, SetupPyStrategy}; use uv_traits::{BuildContext, BuildKind, ConfigSettings, InFlight, NoBuild, SetupPyStrategy};
#[derive(Parser)] #[derive(Parser)]
pub(crate) struct BuildArgs { pub(crate) struct BuildArgs {
@ -61,6 +61,7 @@ pub(crate) async fn build(args: BuildArgs) -> Result<PathBuf> {
let index = InMemoryIndex::default(); let index = InMemoryIndex::default();
let setup_py = SetupPyStrategy::default(); let setup_py = SetupPyStrategy::default();
let in_flight = InFlight::default(); let in_flight = InFlight::default();
let config_settings = ConfigSettings::default();
let build_dispatch = BuildDispatch::new( let build_dispatch = BuildDispatch::new(
&client, &client,
@ -72,6 +73,7 @@ pub(crate) async fn build(args: BuildArgs) -> Result<PathBuf> {
&in_flight, &in_flight,
venv.python_executable(), venv.python_executable(),
setup_py, setup_py,
&config_settings,
&NoBuild::None, &NoBuild::None,
&NoBinary::None, &NoBinary::None,
); );
@ -84,6 +86,7 @@ pub(crate) async fn build(args: BuildArgs) -> Result<PathBuf> {
SourceBuildContext::default(), SourceBuildContext::default(),
args.sdist.display().to_string(), args.sdist.display().to_string(),
setup_py, setup_py,
config_settings.clone(),
build_kind, build_kind,
) )
.await?; .await?;

View file

@ -25,7 +25,7 @@ use uv_installer::{Downloader, NoBinary};
use uv_interpreter::Virtualenv; use uv_interpreter::Virtualenv;
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_resolver::{DistFinder, InMemoryIndex}; use uv_resolver::{DistFinder, InMemoryIndex};
use uv_traits::{BuildContext, InFlight, NoBuild, SetupPyStrategy}; use uv_traits::{BuildContext, ConfigSettings, InFlight, NoBuild, SetupPyStrategy};
#[derive(Parser)] #[derive(Parser)]
pub(crate) struct InstallManyArgs { pub(crate) struct InstallManyArgs {
@ -65,12 +65,12 @@ pub(crate) async fn install_many(args: InstallManyArgs) -> Result<()> {
let setup_py = SetupPyStrategy::default(); let setup_py = SetupPyStrategy::default();
let in_flight = InFlight::default(); let in_flight = InFlight::default();
let tags = venv.interpreter().tags()?; let tags = venv.interpreter().tags()?;
let no_build = if args.no_build { let no_build = if args.no_build {
NoBuild::All NoBuild::All
} else { } else {
NoBuild::None NoBuild::None
}; };
let config_settings = ConfigSettings::default();
let build_dispatch = BuildDispatch::new( let build_dispatch = BuildDispatch::new(
&client, &client,
@ -82,6 +82,7 @@ pub(crate) async fn install_many(args: InstallManyArgs) -> Result<()> {
&in_flight, &in_flight,
venv.python_executable(), venv.python_executable(),
setup_py, setup_py,
&config_settings,
&no_build, &no_build,
&NoBinary::None, &NoBinary::None,
); );

View file

@ -18,7 +18,7 @@ use uv_dispatch::BuildDispatch;
use uv_installer::NoBinary; use uv_installer::NoBinary;
use uv_interpreter::Virtualenv; use uv_interpreter::Virtualenv;
use uv_resolver::{InMemoryIndex, Manifest, Options, Resolver}; use uv_resolver::{InMemoryIndex, Manifest, Options, Resolver};
use uv_traits::{InFlight, NoBuild, SetupPyStrategy}; use uv_traits::{ConfigSettings, InFlight, NoBuild, SetupPyStrategy};
#[derive(ValueEnum, Default, Clone)] #[derive(ValueEnum, Default, Clone)]
pub(crate) enum ResolveCliFormat { pub(crate) enum ResolveCliFormat {
@ -72,12 +72,12 @@ pub(crate) async fn resolve_cli(args: ResolveCliArgs) -> Result<()> {
}; };
let index = InMemoryIndex::default(); let index = InMemoryIndex::default();
let in_flight = InFlight::default(); let in_flight = InFlight::default();
let no_build = if args.no_build { let no_build = if args.no_build {
NoBuild::All NoBuild::All
} else { } else {
NoBuild::None NoBuild::None
}; };
let config_settings = ConfigSettings::default();
let build_dispatch = BuildDispatch::new( let build_dispatch = BuildDispatch::new(
&client, &client,
@ -89,6 +89,7 @@ pub(crate) async fn resolve_cli(args: ResolveCliArgs) -> Result<()> {
&in_flight, &in_flight,
venv.python_executable(), venv.python_executable(),
SetupPyStrategy::default(), SetupPyStrategy::default(),
&config_settings,
&no_build, &no_build,
&NoBinary::None, &NoBinary::None,
); );

View file

@ -21,7 +21,7 @@ use uv_installer::NoBinary;
use uv_interpreter::Virtualenv; use uv_interpreter::Virtualenv;
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_resolver::InMemoryIndex; use uv_resolver::InMemoryIndex;
use uv_traits::{BuildContext, InFlight, NoBuild, SetupPyStrategy}; use uv_traits::{BuildContext, ConfigSettings, InFlight, NoBuild, SetupPyStrategy};
#[derive(Parser)] #[derive(Parser)]
pub(crate) struct ResolveManyArgs { pub(crate) struct ResolveManyArgs {
@ -96,6 +96,7 @@ pub(crate) async fn resolve_many(args: ResolveManyArgs) -> Result<()> {
let index_locations = IndexLocations::default(); let index_locations = IndexLocations::default();
let setup_py = SetupPyStrategy::default(); let setup_py = SetupPyStrategy::default();
let flat_index = FlatIndex::default(); let flat_index = FlatIndex::default();
let config_settings = ConfigSettings::default();
// Create a `BuildDispatch` for each requirement. // Create a `BuildDispatch` for each requirement.
let build_dispatch = BuildDispatch::new( let build_dispatch = BuildDispatch::new(
@ -108,6 +109,7 @@ pub(crate) async fn resolve_many(args: ResolveManyArgs) -> Result<()> {
&in_flight, &in_flight,
venv.python_executable(), venv.python_executable(),
setup_py, setup_py,
&config_settings,
&no_build, &no_build,
&NoBinary::None, &NoBinary::None,
); );

View file

@ -18,7 +18,7 @@ use uv_client::{FlatIndex, RegistryClient};
use uv_installer::{Downloader, Installer, NoBinary, Plan, Planner, Reinstall, SitePackages}; use uv_installer::{Downloader, Installer, NoBinary, Plan, Planner, Reinstall, SitePackages};
use uv_interpreter::{Interpreter, Virtualenv}; use uv_interpreter::{Interpreter, Virtualenv};
use uv_resolver::{InMemoryIndex, Manifest, Options, Resolver}; 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`] /// The main implementation of [`BuildContext`], used by the CLI, see [`BuildContext`]
/// documentation. /// documentation.
@ -34,6 +34,7 @@ pub struct BuildDispatch<'a> {
setup_py: SetupPyStrategy, setup_py: SetupPyStrategy,
no_build: &'a NoBuild, no_build: &'a NoBuild,
no_binary: &'a NoBinary, no_binary: &'a NoBinary,
config_settings: &'a ConfigSettings,
source_build_context: SourceBuildContext, source_build_context: SourceBuildContext,
options: Options, options: Options,
} }
@ -50,6 +51,7 @@ impl<'a> BuildDispatch<'a> {
in_flight: &'a InFlight, in_flight: &'a InFlight,
base_python: PathBuf, base_python: PathBuf,
setup_py: SetupPyStrategy, setup_py: SetupPyStrategy,
config_settings: &'a ConfigSettings,
no_build: &'a NoBuild, no_build: &'a NoBuild,
no_binary: &'a NoBinary, no_binary: &'a NoBinary,
) -> Self { ) -> Self {
@ -63,6 +65,7 @@ impl<'a> BuildDispatch<'a> {
in_flight, in_flight,
base_python, base_python,
setup_py, setup_py,
config_settings,
no_build, no_build,
no_binary, no_binary,
source_build_context: SourceBuildContext::default(), source_build_context: SourceBuildContext::default(),
@ -279,6 +282,7 @@ impl<'a> BuildContext for BuildDispatch<'a> {
self.source_build_context.clone(), self.source_build_context.clone(),
package_id.to_string(), package_id.to_string(),
self.setup_py, self.setup_py,
self.config_settings.clone(),
build_kind, build_kind,
) )
.boxed() .boxed()

View file

@ -13,6 +13,7 @@ license = { workspace = true }
workspace = true workspace = true
[dependencies] [dependencies]
clap = { workspace = true, optional = true }
distribution-types = { path = "../distribution-types" } distribution-types = { path = "../distribution-types" }
once-map = { path = "../once-map" } once-map = { path = "../once-map" }
pep508_rs = { path = "../pep508-rs" } pep508_rs = { path = "../pep508-rs" }
@ -21,4 +22,10 @@ uv-interpreter = { path = "../uv-interpreter" }
uv-normalize = { path = "../uv-normalize" } uv-normalize = { path = "../uv-normalize" }
anyhow = { workspace = true } anyhow = { workspace = true }
serde = { workspace = true, optional = true }
serde_json = { workspace = true, optional = true }
tokio = { workspace = true, features = ["sync"] } tokio = { workspace = true, features = ["sync"] }
[features]
default = []
serde = ["dep:serde", "dep:serde_json"]

View file

@ -1,11 +1,14 @@
//! Avoid cyclic crate dependencies between resolver, installer and builder. //! 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::fmt::{Display, Formatter};
use std::future::Future; use std::future::Future;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::str::FromStr; use std::str::FromStr;
use anyhow::Result; use anyhow::Result;
use serde::ser::SerializeMap;
use distribution_types::{CachedDist, DistributionId, IndexLocations, Resolution, SourceDist}; use distribution_types::{CachedDist, DistributionId, IndexLocations, Resolution, SourceDist};
use once_map::OnceMap; 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<Self, Self::Err> {
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<String>),
}
/// Settings to pass to a PEP 517 build backend, structured as a map from (string) key to string or
/// list of strings.
///
/// See: <https://peps.python.org/pep-0517/#config-settings>
#[derive(Debug, Default, Clone)]
pub struct ConfigSettings(BTreeMap<String, ConfigSettingValue>);
impl FromIterator<ConfigSettingEntry> for ConfigSettings {
fn from_iter<T: IntoIterator<Item = ConfigSettingEntry>>(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<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
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)] #[cfg(test)]
mod tests { mod tests {
use anyhow::Error; use anyhow::Error;
@ -349,4 +440,81 @@ mod tests {
Ok(()) 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"}"#);
}
} }

View file

@ -31,7 +31,7 @@ use uv_resolver::{
AnnotationStyle, DependencyMode, DisplayResolutionGraph, InMemoryIndex, Manifest, AnnotationStyle, DependencyMode, DisplayResolutionGraph, InMemoryIndex, Manifest,
OptionsBuilder, PreReleaseMode, ResolutionMode, Resolver, OptionsBuilder, PreReleaseMode, ResolutionMode, Resolver,
}; };
use uv_traits::{InFlight, NoBuild, SetupPyStrategy}; use uv_traits::{ConfigSettings, InFlight, NoBuild, SetupPyStrategy};
use uv_warnings::warn_user; use uv_warnings::warn_user;
use crate::commands::reporters::{DownloadReporter, ResolverReporter}; use crate::commands::reporters::{DownloadReporter, ResolverReporter};
@ -58,6 +58,7 @@ pub(crate) async fn pip_compile(
include_find_links: bool, include_find_links: bool,
index_locations: IndexLocations, index_locations: IndexLocations,
setup_py: SetupPyStrategy, setup_py: SetupPyStrategy,
config_settings: ConfigSettings,
connectivity: Connectivity, connectivity: Connectivity,
no_build: &NoBuild, no_build: &NoBuild,
python_version: Option<PythonVersion>, python_version: Option<PythonVersion>,
@ -219,6 +220,7 @@ pub(crate) async fn pip_compile(
&in_flight, &in_flight,
interpreter.sys_executable().to_path_buf(), interpreter.sys_executable().to_path_buf(),
setup_py, setup_py,
&config_settings,
no_build, no_build,
&NoBinary::None, &NoBinary::None,
) )

View file

@ -33,7 +33,7 @@ use uv_resolver::{
DependencyMode, InMemoryIndex, Manifest, Options, OptionsBuilder, PreReleaseMode, DependencyMode, InMemoryIndex, Manifest, Options, OptionsBuilder, PreReleaseMode,
ResolutionGraph, ResolutionMode, Resolver, 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::reporters::{DownloadReporter, InstallReporter, ResolverReporter};
use crate::commands::{elapsed, ChangeEvent, ChangeEventKind, ExitStatus}; use crate::commands::{elapsed, ChangeEvent, ChangeEventKind, ExitStatus};
@ -58,6 +58,7 @@ pub(crate) async fn pip_install(
link_mode: LinkMode, link_mode: LinkMode,
setup_py: SetupPyStrategy, setup_py: SetupPyStrategy,
connectivity: Connectivity, connectivity: Connectivity,
config_settings: &ConfigSettings,
no_build: &NoBuild, no_build: &NoBuild,
no_binary: &NoBinary, no_binary: &NoBinary,
strict: bool, strict: bool,
@ -173,6 +174,7 @@ pub(crate) async fn pip_install(
&in_flight, &in_flight,
venv.python_executable(), venv.python_executable(),
setup_py, setup_py,
config_settings,
no_build, no_build,
no_binary, no_binary,
) )
@ -255,6 +257,7 @@ pub(crate) async fn pip_install(
&in_flight, &in_flight,
venv.python_executable(), venv.python_executable(),
setup_py, setup_py,
config_settings,
no_build, no_build,
no_binary, no_binary,
) )

View file

@ -20,7 +20,7 @@ use uv_installer::{
}; };
use uv_interpreter::Virtualenv; use uv_interpreter::Virtualenv;
use uv_resolver::InMemoryIndex; 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::reporters::{DownloadReporter, FinderReporter, InstallReporter};
use crate::commands::{elapsed, ChangeEvent, ChangeEventKind, ExitStatus}; use crate::commands::{elapsed, ChangeEvent, ChangeEventKind, ExitStatus};
@ -36,6 +36,7 @@ pub(crate) async fn pip_sync(
index_locations: IndexLocations, index_locations: IndexLocations,
setup_py: SetupPyStrategy, setup_py: SetupPyStrategy,
connectivity: Connectivity, connectivity: Connectivity,
config_settings: &ConfigSettings,
no_build: &NoBuild, no_build: &NoBuild,
no_binary: &NoBinary, no_binary: &NoBinary,
strict: bool, strict: bool,
@ -112,6 +113,7 @@ pub(crate) async fn pip_sync(
&in_flight, &in_flight,
venv.python_executable(), venv.python_executable(),
setup_py, setup_py,
config_settings,
no_build, no_build,
no_binary, no_binary,
); );

View file

@ -22,7 +22,7 @@ use uv_fs::Normalized;
use uv_installer::NoBinary; use uv_installer::NoBinary;
use uv_interpreter::{find_default_python, find_requested_python, Error}; use uv_interpreter::{find_default_python, find_requested_python, Error};
use uv_resolver::{InMemoryIndex, OptionsBuilder}; 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::commands::ExitStatus;
use crate::printer::Printer; use crate::printer::Printer;
@ -154,6 +154,9 @@ async fn venv_impl(
// Track in-flight downloads, builds, etc., across resolutions. // Track in-flight downloads, builds, etc., across resolutions.
let in_flight = InFlight::default(); let in_flight = InFlight::default();
// For seed packages, assume the default settings are sufficient.
let config_settings = ConfigSettings::default();
// Prep the build context. // Prep the build context.
let build_dispatch = BuildDispatch::new( let build_dispatch = BuildDispatch::new(
&client, &client,
@ -165,6 +168,7 @@ async fn venv_impl(
&in_flight, &in_flight,
venv.python_executable(), venv.python_executable(),
SetupPyStrategy::default(), SetupPyStrategy::default(),
&config_settings,
&NoBuild::All, &NoBuild::All,
&NoBinary::None, &NoBinary::None,
) )

View file

@ -20,7 +20,9 @@ use uv_installer::{NoBinary, Reinstall};
use uv_interpreter::PythonVersion; use uv_interpreter::PythonVersion;
use uv_normalize::{ExtraName, PackageName}; use uv_normalize::{ExtraName, PackageName};
use uv_resolver::{AnnotationStyle, DependencyMode, PreReleaseMode, ResolutionMode}; 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::commands::{extra_name_with_clap_error, ExitStatus, Upgrade};
use crate::compat::CompatArgs; use crate::compat::CompatArgs;
@ -331,6 +333,10 @@ struct PipCompileArgs {
#[clap(long, conflicts_with = "no_build")] #[clap(long, conflicts_with = "no_build")]
only_binary: Vec<PackageNameSpecifier>, only_binary: Vec<PackageNameSpecifier>,
/// Settings to pass to the PEP 517 build backend, specified as `KEY=VALUE` pairs.
#[clap(long, short = 'C', alias = "config-settings")]
config_setting: Vec<ConfigSettingEntry>,
/// The minimum Python version that should be supported by the compiled requirements (e.g., /// The minimum Python version that should be supported by the compiled requirements (e.g.,
/// `3.7` or `3.7.9`). /// `3.7` or `3.7.9`).
/// ///
@ -456,6 +462,10 @@ struct PipSyncArgs {
#[clap(long, conflicts_with = "no_build")] #[clap(long, conflicts_with = "no_build")]
only_binary: Vec<PackageNameSpecifier>, only_binary: Vec<PackageNameSpecifier>,
/// Settings to pass to the PEP 517 build backend, specified as `KEY=VALUE` pairs.
#[clap(long, short = 'C', alias = "config-settings")]
config_setting: Vec<ConfigSettingEntry>,
/// Validate the virtual environment after completing the installation, to detect packages with /// Validate the virtual environment after completing the installation, to detect packages with
/// missing dependencies or other issues. /// missing dependencies or other issues.
#[clap(long)] #[clap(long)]
@ -621,6 +631,10 @@ struct PipInstallArgs {
#[clap(long, conflicts_with = "no_build")] #[clap(long, conflicts_with = "no_build")]
only_binary: Vec<PackageNameSpecifier>, only_binary: Vec<PackageNameSpecifier>,
/// Settings to pass to the PEP 517 build backend, specified as `KEY=VALUE` pairs.
#[clap(long, short = 'C', alias = "config-settings")]
config_setting: Vec<ConfigSettingEntry>,
/// Validate the virtual environment after completing the installation, to detect packages with /// Validate the virtual environment after completing the installation, to detect packages with
/// missing dependencies or other issues. /// missing dependencies or other issues.
#[clap(long)] #[clap(long)]
@ -873,6 +887,12 @@ async fn run() -> Result<ExitStatus> {
} else { } else {
DependencyMode::Transitive DependencyMode::Transitive
}; };
let setup_py = if args.legacy_setup_py {
SetupPyStrategy::Setuptools
} else {
SetupPyStrategy::Pep517
};
let config_settings = args.config_setting.into_iter().collect::<ConfigSettings>();
commands::pip_compile( commands::pip_compile(
&requirements, &requirements,
&constraints, &constraints,
@ -889,11 +909,8 @@ async fn run() -> Result<ExitStatus> {
args.emit_index_url, args.emit_index_url,
args.emit_find_links, args.emit_find_links,
index_urls, index_urls,
if args.legacy_setup_py { setup_py,
SetupPyStrategy::Setuptools config_settings,
} else {
SetupPyStrategy::Pep517
},
if args.offline { if args.offline {
Connectivity::Offline Connectivity::Offline
} else { } else {
@ -928,21 +945,25 @@ async fn run() -> Result<ExitStatus> {
let reinstall = Reinstall::from_args(args.reinstall, args.reinstall_package); let reinstall = Reinstall::from_args(args.reinstall, args.reinstall_package);
let no_binary = NoBinary::from_args(args.no_binary); let no_binary = NoBinary::from_args(args.no_binary);
let no_build = NoBuild::from_args(args.only_binary, args.no_build); 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::<ConfigSettings>();
commands::pip_sync( commands::pip_sync(
&sources, &sources,
&reinstall, &reinstall,
args.link_mode, args.link_mode,
index_urls, index_urls,
if args.legacy_setup_py { setup_py,
SetupPyStrategy::Setuptools
} else {
SetupPyStrategy::Pep517
},
if args.offline { if args.offline {
Connectivity::Offline Connectivity::Offline
} else { } else {
Connectivity::Online Connectivity::Online
}, },
&config_settings,
&no_build, &no_build,
&no_binary, &no_binary,
args.strict, args.strict,
@ -998,6 +1019,13 @@ async fn run() -> Result<ExitStatus> {
} else { } else {
DependencyMode::Transitive DependencyMode::Transitive
}; };
let setup_py = if args.legacy_setup_py {
SetupPyStrategy::Setuptools
} else {
SetupPyStrategy::Pep517
};
let config_settings = args.config_setting.into_iter().collect::<ConfigSettings>();
commands::pip_install( commands::pip_install(
&requirements, &requirements,
&constraints, &constraints,
@ -1010,16 +1038,13 @@ async fn run() -> Result<ExitStatus> {
index_urls, index_urls,
&reinstall, &reinstall,
args.link_mode, args.link_mode,
if args.legacy_setup_py { setup_py,
SetupPyStrategy::Setuptools
} else {
SetupPyStrategy::Pep517
},
if args.offline { if args.offline {
Connectivity::Offline Connectivity::Offline
} else { } else {
Connectivity::Online Connectivity::Online
}, },
&config_settings,
&no_build, &no_build,
&no_binary, &no_binary,
args.strict, args.strict,

View file

@ -1598,3 +1598,105 @@ fn launcher_with_symlink() -> Result<()> {
Ok(()) 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::<Vec<_>>();
// 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::<Vec<_>>();
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(())
}

View file

@ -0,0 +1,2 @@
# Artifacts from the build process.
*.egg-info/

View file

@ -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"]

View file

@ -0,0 +1,2 @@
def a():
pass