diff --git a/crates/uv-toolchain/src/interpreter.rs b/crates/uv-toolchain/src/interpreter.rs index 9f772c2e8..1cb700c2c 100644 --- a/crates/uv-toolchain/src/interpreter.rs +++ b/crates/uv-toolchain/src/interpreter.rs @@ -471,7 +471,6 @@ pub enum Error { stderr: String, path: PathBuf, }, - #[error("Querying Python at `{}` failed with exit status {code}\n--- stdout:\n{stdout}\n--- stderr:\n{stderr}\n---", path.display())] StatusCode { code: ExitStatus, diff --git a/crates/uv-workspace/src/combine.rs b/crates/uv-workspace/src/combine.rs index 82a68e24e..ea36715e3 100644 --- a/crates/uv-workspace/src/combine.rs +++ b/crates/uv-workspace/src/combine.rs @@ -7,7 +7,7 @@ use uv_configuration::{ConfigSettings, IndexStrategy, KeyringProviderType, Targe use uv_resolver::{AnnotationStyle, ExcludeNewer, PreReleaseMode, ResolutionMode}; use uv_toolchain::PythonVersion; -use crate::{Options, PipOptions, Workspace}; +use crate::{GlobalOptions, Options, PipOptions, ResolverInstallerOptions, Workspace}; pub trait Combine { /// Combine two values, preferring the values in `self`. @@ -41,11 +41,8 @@ impl Combine for Option { impl Combine for Options { fn combine(self, other: Options) -> Options { Options { - native_tls: self.native_tls.combine(other.native_tls), - offline: self.offline.combine(other.offline), - no_cache: self.no_cache.combine(other.no_cache), - preview: self.preview.combine(other.preview), - cache_dir: self.cache_dir.combine(other.cache_dir), + globals: self.globals.combine(other.globals), + top_level: self.top_level.combine(other.top_level), pip: self.pip.combine(other.pip), override_dependencies: self .override_dependencies @@ -54,6 +51,37 @@ impl Combine for Options { } } +impl Combine for GlobalOptions { + fn combine(self, other: GlobalOptions) -> GlobalOptions { + GlobalOptions { + native_tls: self.native_tls.combine(other.native_tls), + offline: self.offline.combine(other.offline), + no_cache: self.no_cache.combine(other.no_cache), + cache_dir: self.cache_dir.combine(other.cache_dir), + preview: self.preview.combine(other.preview), + } + } +} + +impl Combine for ResolverInstallerOptions { + fn combine(self, other: ResolverInstallerOptions) -> ResolverInstallerOptions { + ResolverInstallerOptions { + index_url: self.index_url.combine(other.index_url), + extra_index_url: self.extra_index_url.combine(other.extra_index_url), + no_index: self.no_index.combine(other.no_index), + find_links: self.find_links.combine(other.find_links), + index_strategy: self.index_strategy.combine(other.index_strategy), + keyring_provider: self.keyring_provider.combine(other.keyring_provider), + resolution: self.resolution.combine(other.resolution), + prerelease: self.prerelease.combine(other.prerelease), + config_settings: self.config_settings.combine(other.config_settings), + exclude_newer: self.exclude_newer.combine(other.exclude_newer), + link_mode: self.link_mode.combine(other.link_mode), + compile_bytecode: self.compile_bytecode.combine(other.compile_bytecode), + } + } +} + impl Combine for Option { fn combine(self, other: Option) -> Option { match (self, other) { diff --git a/crates/uv-workspace/src/settings.rs b/crates/uv-workspace/src/settings.rs index 13d6839d2..762a8cfe0 100644 --- a/crates/uv-workspace/src/settings.rs +++ b/crates/uv-workspace/src/settings.rs @@ -32,11 +32,10 @@ pub(crate) struct Tools { #[serde(rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct Options { - pub native_tls: Option, - pub offline: Option, - pub no_cache: Option, - pub preview: Option, - pub cache_dir: Option, + #[serde(flatten)] + pub globals: GlobalOptions, + #[serde(flatten)] + pub top_level: ResolverInstallerOptions, pub pip: Option, #[cfg_attr( feature = "schemars", @@ -48,6 +47,76 @@ pub struct Options { pub override_dependencies: Option>>, } +/// Global settings, relevant to all invocations. +#[allow(dead_code)] +#[derive(Debug, Clone, Default, Deserialize)] +#[serde(rename_all = "kebab-case")] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct GlobalOptions { + pub native_tls: Option, + pub offline: Option, + pub no_cache: Option, + pub cache_dir: Option, + pub preview: Option, +} + +/// Settings relevant to all installer operations. +#[allow(dead_code)] +#[derive(Debug, Clone, Default, Deserialize)] +#[serde(rename_all = "kebab-case")] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct InstallerOptions { + pub index_url: Option, + pub extra_index_url: Option>, + pub no_index: Option, + pub find_links: Option>, + pub index_strategy: Option, + pub keyring_provider: Option, + pub config_settings: Option, + pub link_mode: Option, + pub compile_bytecode: Option, +} + +/// Settings relevant to all resolver operations. +#[allow(dead_code)] +#[derive(Debug, Clone, Default, Deserialize)] +#[serde(rename_all = "kebab-case")] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct ResolverOptions { + pub index_url: Option, + pub extra_index_url: Option>, + pub no_index: Option, + pub find_links: Option>, + pub index_strategy: Option, + pub keyring_provider: Option, + pub resolution: Option, + pub prerelease: Option, + pub config_settings: Option, + pub exclude_newer: Option, + pub link_mode: Option, +} + +/// Shared settings, relevant to all operations that must resolve and install dependencies. The +/// union of [`InstallerOptions`] and [`ResolverOptions`]. +#[allow(dead_code)] +#[derive(Debug, Clone, Default, Deserialize)] +#[serde(rename_all = "kebab-case")] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct ResolverInstallerOptions { + pub index_url: Option, + pub extra_index_url: Option>, + pub no_index: Option, + pub find_links: Option>, + pub index_strategy: Option, + pub keyring_provider: Option, + pub resolution: Option, + pub prerelease: Option, + pub config_settings: Option, + pub exclude_newer: Option, + pub link_mode: Option, + pub compile_bytecode: Option, +} + /// A `[tool.uv.pip]` section. #[allow(dead_code)] #[derive(Debug, Clone, Default, Deserialize)] @@ -99,3 +168,120 @@ pub struct PipOptions { pub concurrent_builds: Option, pub concurrent_installs: Option, } + +impl Options { + /// Return the `pip` section, with any top-level options merged in. If options are repeated + /// between the top-level and the `pip` section, the `pip` options are preferred. + /// + /// For example, prefers `tool.uv.pip.index-url` over `tool.uv.index-url`. + pub fn pip(self) -> PipOptions { + let PipOptions { + python, + system, + break_system_packages, + target, + prefix, + index_url, + extra_index_url, + no_index, + find_links, + index_strategy, + keyring_provider, + no_build, + no_binary, + only_binary, + no_build_isolation, + strict, + extra, + all_extras, + no_deps, + resolution, + prerelease, + output_file, + no_strip_extras, + no_annotate, + no_header, + custom_compile_command, + generate_hashes, + legacy_setup_py, + config_settings, + python_version, + python_platform, + exclude_newer, + no_emit_package, + emit_index_url, + emit_find_links, + emit_marker_expression, + emit_index_annotation, + annotation_style, + link_mode, + compile_bytecode, + require_hashes, + concurrent_builds, + concurrent_downloads, + concurrent_installs, + } = self.pip.unwrap_or_default(); + + let ResolverInstallerOptions { + index_url: top_level_index_url, + extra_index_url: top_level_extra_index_url, + no_index: top_level_no_index, + find_links: top_level_find_links, + index_strategy: top_level_index_strategy, + keyring_provider: top_level_keyring_provider, + resolution: top_level_resolution, + prerelease: top_level_prerelease, + config_settings: top_level_config_settings, + exclude_newer: top_level_exclude_newer, + link_mode: top_level_link_mode, + compile_bytecode: top_level_compile_bytecode, + } = self.top_level; + + PipOptions { + python, + system, + break_system_packages, + target, + prefix, + index_url: index_url.or(top_level_index_url), + extra_index_url: extra_index_url.or(top_level_extra_index_url), + no_index: no_index.or(top_level_no_index), + find_links: find_links.or(top_level_find_links), + index_strategy: index_strategy.or(top_level_index_strategy), + keyring_provider: keyring_provider.or(top_level_keyring_provider), + no_build, + no_binary, + only_binary, + no_build_isolation, + strict, + extra, + all_extras, + no_deps, + resolution: resolution.or(top_level_resolution), + prerelease: prerelease.or(top_level_prerelease), + output_file, + no_strip_extras, + no_annotate, + no_header, + custom_compile_command, + generate_hashes, + legacy_setup_py, + config_settings: config_settings.or(top_level_config_settings), + python_version, + python_platform, + exclude_newer: exclude_newer.or(top_level_exclude_newer), + no_emit_package, + emit_index_url, + emit_find_links, + emit_marker_expression, + emit_index_annotation, + annotation_style, + link_mode: link_mode.or(top_level_link_mode), + compile_bytecode: compile_bytecode.or(top_level_compile_bytecode), + require_hashes, + concurrent_builds, + concurrent_downloads, + concurrent_installs, + } + } +} diff --git a/crates/uv/src/cli.rs b/crates/uv/src/cli.rs index 4d4aebb2a..34bf5a0d9 100644 --- a/crates/uv/src/cli.rs +++ b/crates/uv/src/cli.rs @@ -342,6 +342,9 @@ pub(crate) struct PipCompileArgs { #[arg(long, overrides_with("all_extras"), hide = true)] pub(crate) no_all_extras: bool, + #[command(flatten)] + pub(crate) resolver: ResolverArgs, + /// Ignore package dependencies, instead only add those packages explicitly listed /// on the command line to the resulting the requirements file. #[arg(long)] @@ -350,24 +353,6 @@ pub(crate) struct PipCompileArgs { #[arg(long, overrides_with("no_deps"), hide = true)] pub(crate) deps: bool, - /// The strategy to use when selecting between the different compatible versions for a given - /// package requirement. - /// - /// By default, `uv` will use the latest compatible version of each package (`highest`). - #[arg(long, value_enum, env = "UV_RESOLUTION")] - pub(crate) resolution: Option, - - /// The strategy to use when considering pre-release versions. - /// - /// By default, `uv` will accept pre-releases for packages that _only_ publish pre-releases, - /// along with first-party requirements that contain an explicit pre-release marker in the - /// declared specifiers (`if-necessary-or-explicit`). - #[arg(long, value_enum, env = "UV_PRERELEASE")] - pub(crate) prerelease: Option, - - #[arg(long, hide = true)] - pub(crate) pre: bool, - /// Write the compiled requirements to the given `requirements.txt` file. #[arg(long, short)] pub(crate) output_file: Option, @@ -423,36 +408,6 @@ pub(crate) struct PipCompileArgs { #[arg(long)] pub(crate) refresh_package: Vec, - /// The method to use when installing packages from the global cache. - /// - /// This option is only used when building source distributions. - /// - /// Defaults to `clone` (also known as Copy-on-Write) on macOS, and `hardlink` on Linux and - /// Windows. - #[arg(long, value_enum, env = "UV_LINK_MODE")] - pub(crate) link_mode: Option, - - #[command(flatten)] - pub(crate) index_args: IndexArgs, - - /// The strategy to use when resolving against multiple index URLs. - /// - /// By default, `uv` will stop at the first index on which a given package is available, and - /// limit resolutions to those present on that first index (`first-match`. This prevents - /// "dependency confusion" attacks, whereby an attack can upload a malicious package under the - /// same name to a secondary - #[arg(long, value_enum, env = "UV_INDEX_STRATEGY")] - pub(crate) index_strategy: Option, - - /// Attempt to use `keyring` for authentication for index URLs. - /// - /// At present, only `--keyring-provider subprocess` is supported, which configures `uv` to - /// use the `keyring` CLI to handle authentication. - /// - /// Defaults to `disabled`. - #[arg(long, value_enum, env = "UV_KEYRING_PROVIDER")] - pub(crate) keyring_provider: Option, - /// The Python interpreter against which to compile the requirements. /// /// By default, `uv` uses the virtual environment in the current working directory or any parent @@ -570,10 +525,6 @@ pub(crate) struct PipCompileArgs { #[arg(long, conflicts_with = "no_build")] pub(crate) only_binary: Option>, - /// Settings to pass to the PEP 517 build backend, specified as `KEY=VALUE` pairs. - #[arg(long, short = 'C', alias = "config-settings")] - pub(crate) config_setting: Option>, - /// The minimum Python version that should be supported by the compiled requirements (e.g., /// `3.7` or `3.7.9`). /// @@ -590,13 +541,6 @@ pub(crate) struct PipCompileArgs { #[arg(long)] pub(crate) python_platform: Option, - /// Limit candidate packages to those that were uploaded prior to the given date. - /// - /// Accepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and UTC dates in the same - /// format (e.g., `2006-12-02`). - #[arg(long, env = "UV_EXCLUDE_NEWER")] - pub(crate) exclude_newer: Option, - /// Specify a package to omit from the output resolution. Its dependencies will still be /// included in the resolution. Equivalent to pip-compile's `--unsafe-package` option. #[arg(long, alias = "unsafe-package")] @@ -662,6 +606,16 @@ pub(crate) struct PipSyncArgs { #[arg(long, short, env = "UV_CONSTRAINT", value_delimiter = ' ', value_parser = parse_maybe_file_path)] pub(crate) constraint: Vec>, + #[command(flatten)] + pub(crate) installer: InstallerArgs, + + /// Limit candidate packages to those that were uploaded prior to the given date. + /// + /// Accepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and UTC dates in the same + /// format (e.g., `2006-12-02`). + #[arg(long, env = "UV_EXCLUDE_NEWER")] + pub(crate) exclude_newer: Option, + /// Reinstall all packages, regardless of whether they're already installed. #[arg(long, alias = "force-reinstall", overrides_with("no_reinstall"))] pub(crate) reinstall: bool, @@ -689,25 +643,6 @@ pub(crate) struct PipSyncArgs { #[arg(long)] pub(crate) refresh_package: Vec, - /// The method to use when installing packages from the global cache. - /// - /// Defaults to `clone` (also known as Copy-on-Write) on macOS, and `hardlink` on Linux and - /// Windows. - #[arg(long, value_enum, env = "UV_LINK_MODE")] - pub(crate) link_mode: Option, - - #[command(flatten)] - pub(crate) index_args: IndexArgs, - - /// The strategy to use when resolving against multiple index URLs. - /// - /// By default, `uv` will stop at the first index on which a given package is available, and - /// limit resolutions to those present on that first index (`first-match`. This prevents - /// "dependency confusion" attacks, whereby an attack can upload a malicious package under the - /// same name to a secondary - #[arg(long, value_enum, env = "UV_INDEX_STRATEGY")] - pub(crate) index_strategy: Option, - /// Require a matching hash for each requirement. /// /// Hash-checking mode is all or nothing. If enabled, _all_ requirements must be provided @@ -726,15 +661,6 @@ pub(crate) struct PipSyncArgs { #[arg(long, overrides_with("require_hashes"), hide = true)] pub(crate) no_require_hashes: bool, - /// Attempt to use `keyring` for authentication for index URLs. - /// - /// Function's similar to `pip`'s `--keyring-provider subprocess` argument, - /// `uv` will try to use `keyring` via CLI when this flag is used. - /// - /// Defaults to `disabled`. - #[arg(long, value_enum, env = "UV_KEYRING_PROVIDER")] - pub(crate) keyring_provider: Option, - /// The Python interpreter into which packages should be installed. /// /// By default, `uv` installs into the virtual environment in the current working directory or @@ -869,30 +795,6 @@ pub(crate) struct PipSyncArgs { #[arg(long, conflicts_with = "no_build")] pub(crate) only_binary: Option>, - /// Compile Python files to bytecode. - /// - /// By default, does not compile Python (`.py`) files to bytecode (`__pycache__/*.pyc`), instead - /// Python lazily does the compilation the first time a module is imported. In cases where the - /// first start time matters, such as CLI applications and docker containers, this option can - /// trade longer install time for faster startup. - /// - /// The compile option will process the entire site-packages directory for consistency and - /// (like pip) ignore all errors. - #[arg(long, alias = "compile", overrides_with("no_compile_bytecode"))] - pub(crate) compile_bytecode: bool, - - #[arg( - long, - alias = "no_compile", - overrides_with("compile_bytecode"), - hide = true - )] - pub(crate) no_compile_bytecode: bool, - - /// Settings to pass to the PEP 517 build backend, specified as `KEY=VALUE` pairs. - #[arg(long, short = 'C', alias = "config-settings")] - pub(crate) config_setting: Option>, - /// The minimum Python version that should be supported by the requirements (e.g., /// `3.7` or `3.7.9`). /// @@ -923,13 +825,6 @@ pub(crate) struct PipSyncArgs { #[arg(long, overrides_with("strict"), hide = true)] pub(crate) no_strict: bool, - /// Limit candidate packages to those that were uploaded prior to the given date. - /// - /// Accepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and UTC dates in the same - /// format (e.g., `2006-12-02`). - #[arg(long, env = "UV_EXCLUDE_NEWER")] - pub(crate) exclude_newer: Option, - /// Perform a dry run, i.e., don't actually install anything but resolve the dependencies and /// print the resulting plan. #[arg(long)] @@ -995,6 +890,9 @@ pub(crate) struct PipInstallArgs { #[arg(long, overrides_with("all_extras"), hide = true)] pub(crate) no_all_extras: bool, + #[command(flatten)] + pub(crate) installer: ResolverInstallerArgs, + /// Allow package upgrades. #[arg(long, short = 'U', overrides_with("no_upgrade"))] pub(crate) upgrade: bool, @@ -1041,43 +939,6 @@ pub(crate) struct PipInstallArgs { #[arg(long, overrides_with("no_deps"), hide = true)] pub(crate) deps: bool, - /// The method to use when installing packages from the global cache. - /// - /// Defaults to `clone` (also known as Copy-on-Write) on macOS, and `hardlink` on Linux and - /// Windows. - #[arg(long, value_enum, env = "UV_LINK_MODE")] - pub(crate) link_mode: Option, - - /// The strategy to use when selecting between the different compatible versions for a given - /// package requirement. - /// - /// By default, `uv` will use the latest compatible version of each package (`highest`). - #[arg(long, value_enum, env = "UV_RESOLUTION")] - pub(crate) resolution: Option, - - /// The strategy to use when considering pre-release versions. - /// - /// By default, `uv` will accept pre-releases for packages that _only_ publish pre-releases, - /// along with first-party requirements that contain an explicit pre-release marker in the - /// declared specifiers (`if-necessary-or-explicit`). - #[arg(long, value_enum, env = "UV_PRERELEASE")] - pub(crate) prerelease: Option, - - #[arg(long, hide = true)] - pub(crate) pre: bool, - - #[command(flatten)] - pub(crate) index_args: IndexArgs, - - /// The strategy to use when resolving against multiple index URLs. - /// - /// By default, `uv` will stop at the first index on which a given package is available, and - /// limit resolutions to those present on that first index (`first-match`. This prevents - /// "dependency confusion" attacks, whereby an attack can upload a malicious package under the - /// same name to a secondary - #[arg(long, value_enum, env = "UV_INDEX_STRATEGY")] - pub(crate) index_strategy: Option, - /// Require a matching hash for each requirement. /// /// Hash-checking mode is all or nothing. If enabled, _all_ requirements must be provided @@ -1100,15 +961,6 @@ pub(crate) struct PipInstallArgs { #[arg(long, overrides_with("require_hashes"), hide = true)] pub(crate) no_require_hashes: bool, - /// Attempt to use `keyring` for authentication for index URLs. - /// - /// At present, only `--keyring-provider subprocess` is supported, which configures `uv` to - /// use the `keyring` CLI to handle authentication. - /// - /// Defaults to `disabled`. - #[arg(long, value_enum, env = "UV_KEYRING_PROVIDER")] - pub(crate) keyring_provider: Option, - /// The Python interpreter into which packages should be installed. /// /// By default, `uv` installs into the virtual environment in the current working directory or @@ -1243,30 +1095,6 @@ pub(crate) struct PipInstallArgs { #[arg(long, conflicts_with = "no_build")] pub(crate) only_binary: Option>, - /// Compile Python files to bytecode. - /// - /// By default, does not compile Python (`.py`) files to bytecode (`__pycache__/*.pyc`), instead - /// Python lazily does the compilation the first time a module is imported. In cases where the - /// first start time matters, such as CLI applications and docker containers, this option can - /// trade longer install time for faster startup. - /// - /// The compile option will process the entire site-packages directory for consistency and - /// (like pip) ignore all errors. - #[arg(long, alias = "compile", overrides_with("no_compile_bytecode"))] - pub(crate) compile_bytecode: bool, - - #[arg( - long, - alias = "no_compile", - overrides_with("compile_bytecode"), - hide = true - )] - pub(crate) no_compile_bytecode: bool, - - /// Settings to pass to the PEP 517 build backend, specified as `KEY=VALUE` pairs. - #[arg(long, short = 'C', alias = "config-settings")] - pub(crate) config_setting: Option>, - /// The minimum Python version that should be supported by the requirements (e.g., /// `3.7` or `3.7.9`). /// @@ -1297,13 +1125,6 @@ pub(crate) struct PipInstallArgs { #[arg(long, overrides_with("strict"), hide = true)] pub(crate) no_strict: bool, - /// Limit candidate packages to those that were uploaded prior to the given date. - /// - /// Accepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and UTC dates in the same - /// format (e.g., `2006-12-02`). - #[arg(long, env = "UV_EXCLUDE_NEWER")] - pub(crate) exclude_newer: Option, - /// Perform a dry run, i.e., don't actually install anything but resolve the dependencies and /// print the resulting plan. #[arg(long)] @@ -1673,48 +1494,8 @@ pub(crate) struct VenvArgs { #[arg(long)] pub(crate) system_site_packages: bool, - /// The method to use when installing packages from the global cache. - /// - /// This option is only used for installing seed packages. - /// - /// Defaults to `clone` (also known as Copy-on-Write) on macOS, and `hardlink` on Linux and - /// Windows. - #[arg(long, value_enum, env = "UV_LINK_MODE")] - pub(crate) link_mode: Option, - - /// The URL of the Python package index (by default: ). - /// - /// Accepts either a repository compliant with PEP 503 (the simple repository API), or a local - /// directory laid out in the same format. - /// - /// The index given by this flag is given lower priority than all other - /// indexes specified via the `--extra-index-url` flag. - /// - /// Unlike `pip`, `uv` will stop looking for versions of a package as soon - /// as it finds it in an index. That is, it isn't possible for `uv` to - /// consider versions of the same package across multiple indexes. - #[arg(long, short, env = "UV_INDEX_URL", value_parser = parse_index_url)] - pub(crate) index_url: Option>, - - /// Extra URLs of package indexes to use, in addition to `--index-url`. - /// - /// Accepts either a repository compliant with PEP 503 (the simple repository API), or a local - /// directory laid out in the same format. - /// - /// All indexes given via this flag take priority over the index - /// in `--index-url` (which defaults to PyPI). And when multiple - /// `--extra-index-url` flags are given, earlier values take priority. - /// - /// Unlike `pip`, `uv` will stop looking for versions of a package as soon - /// as it finds it in an index. That is, it isn't possible for `uv` to - /// consider versions of the same package across multiple indexes. - #[arg(long, env = "UV_EXTRA_INDEX_URL", value_delimiter = ' ', value_parser = parse_index_url)] - pub(crate) extra_index_url: Option>>, - - /// Ignore the registry index (e.g., PyPI), instead relying on direct URL dependencies and those - /// discovered via `--find-links`. - #[arg(long)] - pub(crate) no_index: bool, + #[command(flatten)] + pub(crate) index_args: IndexArgs, /// The strategy to use when resolving against multiple index URLs. /// @@ -1741,6 +1522,15 @@ pub(crate) struct VenvArgs { #[arg(long, env = "UV_EXCLUDE_NEWER")] pub(crate) exclude_newer: Option, + /// The method to use when installing packages from the global cache. + /// + /// This option is only used for installing seed packages. + /// + /// Defaults to `clone` (also known as Copy-on-Write) on macOS, and `hardlink` on Linux and + /// Windows. + #[arg(long, value_enum, env = "UV_LINK_MODE")] + pub(crate) link_mode: Option, + #[command(flatten)] pub(crate) compat_args: compat::VenvCompatArgs, } @@ -1808,7 +1598,7 @@ pub(crate) struct RunArgs { pub(crate) upgrade_package: Vec, #[command(flatten)] - pub(crate) index_args: IndexArgs, + pub(crate) installer: ResolverInstallerArgs, /// The Python interpreter to use to build the run environment. /// @@ -1824,13 +1614,6 @@ pub(crate) struct RunArgs { #[arg(long, short, env = "UV_PYTHON", verbatim_doc_comment)] pub(crate) python: Option, - /// Limit candidate packages to those that were uploaded prior to the given date. - /// - /// Accepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and UTC dates in the same - /// format (e.g., `2006-12-02`). - #[arg(long, env = "UV_EXCLUDE_NEWER")] - pub(crate) exclude_newer: Option, - /// Run the command in a different package in the workspace. #[arg(long, conflicts_with = "isolated")] pub(crate) package: Option, @@ -1877,7 +1660,7 @@ pub(crate) struct SyncArgs { pub(crate) refresh_package: Vec, #[command(flatten)] - pub(crate) index_args: IndexArgs, + pub(crate) installer: InstallerArgs, /// The Python interpreter to use to build the run environment. /// @@ -1925,7 +1708,7 @@ pub(crate) struct LockArgs { pub(crate) upgrade_package: Vec, #[command(flatten)] - pub(crate) index_args: IndexArgs, + pub(crate) resolver: ResolverArgs, /// The Python interpreter to use to build the run environment. /// @@ -1940,13 +1723,6 @@ pub(crate) struct LockArgs { /// - `/home/ferris/.local/bin/python3.10` uses the exact Python at the given path. #[arg(long, short, env = "UV_PYTHON", verbatim_doc_comment)] pub(crate) python: Option, - - /// Limit candidate packages to those that were uploaded prior to the given date. - /// - /// Accepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and UTC dates in the same - /// format (e.g., `2006-12-02`). - #[arg(long, env = "UV_EXCLUDE_NEWER")] - pub(crate) exclude_newer: Option, } #[derive(Args)] @@ -2028,7 +1804,7 @@ pub(crate) struct ToolRunArgs { pub(crate) with: Vec, #[command(flatten)] - pub(crate) index_args: IndexArgs, + pub(crate) installer: ResolverInstallerArgs, /// The Python interpreter to use to build the run environment. /// @@ -2098,10 +1874,6 @@ pub(crate) struct IndexArgs { /// /// The index given by this flag is given lower priority than all other /// indexes specified via the `--extra-index-url` flag. - /// - /// Unlike `pip`, `uv` will stop looking for versions of a package as soon - /// as it finds it in an index. That is, it isn't possible for `uv` to - /// consider versions of the same package across multiple indexes. #[arg(long, short, env = "UV_INDEX_URL", value_parser = parse_index_url)] pub(crate) index_url: Option>, @@ -2113,10 +1885,6 @@ pub(crate) struct IndexArgs { /// All indexes given via this flag take priority over the index /// in `--index-url` (which defaults to PyPI). And when multiple /// `--extra-index-url` flags are given, earlier values take priority. - /// - /// Unlike `pip`, `uv` will stop looking for versions of a package as soon - /// as it finds it in an index. That is, it isn't possible for `uv` to - /// consider versions of the same package across multiple indexes. #[arg(long, env = "UV_EXTRA_INDEX_URL", value_delimiter = ' ', value_parser = parse_index_url)] pub(crate) extra_index_url: Option>>, @@ -2134,3 +1902,203 @@ pub(crate) struct IndexArgs { #[arg(long)] pub(crate) no_index: bool, } + +/// Arguments that are used by commands that need to install (but not resolve) packages. +#[derive(Args)] +pub(crate) struct InstallerArgs { + #[command(flatten)] + pub(crate) index_args: IndexArgs, + + /// The strategy to use when resolving against multiple index URLs. + /// + /// By default, `uv` will stop at the first index on which a given package is available, and + /// limit resolutions to those present on that first index (`first-match`. This prevents + /// "dependency confusion" attacks, whereby an attack can upload a malicious package under the + /// same name to a secondary + #[arg(long, value_enum, env = "UV_INDEX_STRATEGY")] + pub(crate) index_strategy: Option, + + /// Attempt to use `keyring` for authentication for index URLs. + /// + /// At present, only `--keyring-provider subprocess` is supported, which configures `uv` to + /// use the `keyring` CLI to handle authentication. + /// + /// Defaults to `disabled`. + #[arg(long, value_enum, env = "UV_KEYRING_PROVIDER")] + pub(crate) keyring_provider: Option, + + /// Settings to pass to the PEP 517 build backend, specified as `KEY=VALUE` pairs. + #[arg(long, short = 'C', alias = "config-settings")] + pub(crate) config_setting: Option>, + + /// The method to use when installing packages from the global cache. + /// + /// Defaults to `clone` (also known as Copy-on-Write) on macOS, and `hardlink` on Linux and + /// Windows. + #[arg(long, value_enum, env = "UV_LINK_MODE")] + pub(crate) link_mode: Option, + + /// Compile Python files to bytecode. + /// + /// By default, does not compile Python (`.py`) files to bytecode (`__pycache__/*.pyc`), instead + /// Python lazily does the compilation the first time a module is imported. In cases where the + /// first start time matters, such as CLI applications and docker containers, this option can + /// trade longer install time for faster startup. + /// + /// The compile option will process the entire site-packages directory for consistency and + /// (like pip) ignore all errors. + #[arg(long, alias = "compile", overrides_with("no_compile_bytecode"))] + pub(crate) compile_bytecode: bool, + + #[arg( + long, + alias = "no_compile", + overrides_with("compile_bytecode"), + hide = true + )] + pub(crate) no_compile_bytecode: bool, +} + +/// Arguments that are used by commands that need to resolve (but not install) packages. +#[derive(Args)] +pub(crate) struct ResolverArgs { + #[command(flatten)] + pub(crate) index_args: IndexArgs, + + /// The strategy to use when resolving against multiple index URLs. + /// + /// By default, `uv` will stop at the first index on which a given package is available, and + /// limit resolutions to those present on that first index (`first-match`. This prevents + /// "dependency confusion" attacks, whereby an attack can upload a malicious package under the + /// same name to a secondary + #[arg(long, value_enum, env = "UV_INDEX_STRATEGY")] + pub(crate) index_strategy: Option, + + /// Attempt to use `keyring` for authentication for index URLs. + /// + /// At present, only `--keyring-provider subprocess` is supported, which configures `uv` to + /// use the `keyring` CLI to handle authentication. + /// + /// Defaults to `disabled`. + #[arg(long, value_enum, env = "UV_KEYRING_PROVIDER")] + pub(crate) keyring_provider: Option, + + /// The strategy to use when selecting between the different compatible versions for a given + /// package requirement. + /// + /// By default, `uv` will use the latest compatible version of each package (`highest`). + #[arg(long, value_enum, env = "UV_RESOLUTION")] + pub(crate) resolution: Option, + + /// The strategy to use when considering pre-release versions. + /// + /// By default, `uv` will accept pre-releases for packages that _only_ publish pre-releases, + /// along with first-party requirements that contain an explicit pre-release marker in the + /// declared specifiers (`if-necessary-or-explicit`). + #[arg(long, value_enum, env = "UV_PRERELEASE")] + pub(crate) prerelease: Option, + + #[arg(long, hide = true)] + pub(crate) pre: bool, + + /// Settings to pass to the PEP 517 build backend, specified as `KEY=VALUE` pairs. + #[arg(long, short = 'C', alias = "config-settings")] + pub(crate) config_setting: Option>, + + /// Limit candidate packages to those that were uploaded prior to the given date. + /// + /// Accepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and UTC dates in the same + /// format (e.g., `2006-12-02`). + #[arg(long, env = "UV_EXCLUDE_NEWER")] + pub(crate) exclude_newer: Option, + + /// The method to use when installing packages from the global cache. + /// + /// This option is only used when building source distributions. + /// + /// Defaults to `clone` (also known as Copy-on-Write) on macOS, and `hardlink` on Linux and + /// Windows. + #[arg(long, value_enum, env = "UV_LINK_MODE")] + pub(crate) link_mode: Option, +} + +/// Arguments that are used by commands that need to resolve and install packages. +#[derive(Args)] +pub(crate) struct ResolverInstallerArgs { + #[command(flatten)] + pub(crate) index_args: IndexArgs, + + /// The strategy to use when resolving against multiple index URLs. + /// + /// By default, `uv` will stop at the first index on which a given package is available, and + /// limit resolutions to those present on that first index (`first-match`. This prevents + /// "dependency confusion" attacks, whereby an attack can upload a malicious package under the + /// same name to a secondary + #[arg(long, value_enum, env = "UV_INDEX_STRATEGY")] + pub(crate) index_strategy: Option, + + /// Attempt to use `keyring` for authentication for index URLs. + /// + /// At present, only `--keyring-provider subprocess` is supported, which configures `uv` to + /// use the `keyring` CLI to handle authentication. + /// + /// Defaults to `disabled`. + #[arg(long, value_enum, env = "UV_KEYRING_PROVIDER")] + pub(crate) keyring_provider: Option, + + /// The strategy to use when selecting between the different compatible versions for a given + /// package requirement. + /// + /// By default, `uv` will use the latest compatible version of each package (`highest`). + #[arg(long, value_enum, env = "UV_RESOLUTION")] + pub(crate) resolution: Option, + + /// The strategy to use when considering pre-release versions. + /// + /// By default, `uv` will accept pre-releases for packages that _only_ publish pre-releases, + /// along with first-party requirements that contain an explicit pre-release marker in the + /// declared specifiers (`if-necessary-or-explicit`). + #[arg(long, value_enum, env = "UV_PRERELEASE")] + pub(crate) prerelease: Option, + + #[arg(long, hide = true)] + pub(crate) pre: bool, + + /// Settings to pass to the PEP 517 build backend, specified as `KEY=VALUE` pairs. + #[arg(long, short = 'C', alias = "config-settings")] + pub(crate) config_setting: Option>, + + /// Limit candidate packages to those that were uploaded prior to the given date. + /// + /// Accepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and UTC dates in the same + /// format (e.g., `2006-12-02`). + #[arg(long, env = "UV_EXCLUDE_NEWER")] + pub(crate) exclude_newer: Option, + + /// The method to use when installing packages from the global cache. + /// + /// Defaults to `clone` (also known as Copy-on-Write) on macOS, and `hardlink` on Linux and + /// Windows. + #[arg(long, value_enum, env = "UV_LINK_MODE")] + pub(crate) link_mode: Option, + + /// Compile Python files to bytecode. + /// + /// By default, does not compile Python (`.py`) files to bytecode (`__pycache__/*.pyc`), instead + /// Python lazily does the compilation the first time a module is imported. In cases where the + /// first start time matters, such as CLI applications and docker containers, this option can + /// trade longer install time for faster startup. + /// + /// The compile option will process the entire site-packages directory for consistency and + /// (like pip) ignore all errors. + #[arg(long, alias = "compile", overrides_with("no_compile_bytecode"))] + pub(crate) compile_bytecode: bool, + + #[arg( + long, + alias = "no_compile", + overrides_with("compile_bytecode"), + hide = true + )] + pub(crate) no_compile_bytecode: bool, +} diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index e6d11534d..47f929ec0 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -1,16 +1,18 @@ -use anyhow::Result; use std::str::FromStr; -use uv_distribution::pyproject_mut::PyProjectTomlMut; -use distribution_types::IndexLocations; +use anyhow::Result; + use pep508_rs::Requirement; use uv_cache::Cache; -use uv_configuration::{ExtrasSpecification, PreviewMode, Upgrade}; +use uv_client::Connectivity; +use uv_configuration::{Concurrency, ExtrasSpecification, PreviewMode, Upgrade}; +use uv_distribution::pyproject_mut::PyProjectTomlMut; use uv_distribution::ProjectWorkspace; use uv_warnings::warn_user; use crate::commands::{project, ExitStatus}; use crate::printer::Printer; +use crate::settings::{InstallerSettings, ResolverSettings}; /// Add one or more packages to the project requirements. #[allow(clippy::too_many_arguments)] @@ -18,6 +20,9 @@ pub(crate) async fn add( requirements: Vec, python: Option, preview: PreviewMode, + connectivity: Connectivity, + concurrency: Concurrency, + native_tls: bool, cache: &Cache, printer: Printer, ) -> Result { @@ -43,9 +48,9 @@ pub(crate) async fn add( // Discover or create the virtual environment. let venv = project::init_environment(project.workspace(), python.as_deref(), cache, printer)?; - let index_locations = IndexLocations::default(); + // Use the default settings. + let settings = ResolverSettings::default(); let upgrade = Upgrade::default(); - let exclude_newer = None; // Lock and sync the environment. let root_project_name = project @@ -59,10 +64,19 @@ pub(crate) async fn add( root_project_name, project.workspace(), venv.interpreter(), - &index_locations, upgrade, - exclude_newer, + &settings.index_locations, + &settings.index_strategy, + &settings.keyring_provider, + &settings.resolution, + &settings.prerelease, + &settings.config_setting, + settings.exclude_newer.as_ref(), + &settings.link_mode, preview, + connectivity, + concurrency, + native_tls, cache, printer, ) @@ -70,6 +84,7 @@ pub(crate) async fn add( // Perform a full sync, because we don't know what exactly is affected by the removal. // TODO(ibraheem): Should we accept CLI overrides for this? Should we even sync here? + let settings = InstallerSettings::default(); let extras = ExtrasSpecification::All; let dev = true; @@ -78,10 +93,18 @@ pub(crate) async fn add( project.workspace().root(), &venv, &lock, - &index_locations, extras, dev, + &settings.index_locations, + &settings.index_strategy, + &settings.keyring_provider, + &settings.config_setting, + &settings.link_mode, + &settings.compile_bytecode, preview, + connectivity, + concurrency, + native_tls, cache, printer, ) diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 92db59668..45550bbbf 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -5,17 +5,20 @@ use anstream::eprint; use distribution_types::{IndexLocations, UnresolvedRequirementSpecification}; use install_wheel_rs::linker::LinkMode; use uv_cache::Cache; -use uv_client::{FlatIndexClient, RegistryClientBuilder}; +use uv_client::{Connectivity, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ - BuildOptions, Concurrency, ConfigSettings, ExtrasSpecification, PreviewMode, Reinstall, - SetupPyStrategy, Upgrade, + BuildOptions, Concurrency, ConfigSettings, ExtrasSpecification, IndexStrategy, + KeyringProviderType, PreviewMode, Reinstall, SetupPyStrategy, Upgrade, }; use uv_dispatch::BuildDispatch; use uv_distribution::{Workspace, DEV_DEPENDENCIES}; use uv_git::GitResolver; use uv_normalize::PackageName; use uv_requirements::upgrade::{read_lockfile, LockedRequirements}; -use uv_resolver::{ExcludeNewer, FlatIndex, InMemoryIndex, Lock, OptionsBuilder, RequiresPython}; +use uv_resolver::{ + ExcludeNewer, FlatIndex, InMemoryIndex, Lock, OptionsBuilder, PreReleaseMode, RequiresPython, + ResolutionMode, +}; use uv_toolchain::Interpreter; use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight}; use uv_warnings::warn_user; @@ -23,15 +26,18 @@ use uv_warnings::warn_user; use crate::commands::project::{find_requires_python, ProjectError}; use crate::commands::{pip, project, ExitStatus}; use crate::printer::Printer; +use crate::settings::ResolverSettings; /// Resolve the project requirements into a lockfile. #[allow(clippy::too_many_arguments)] pub(crate) async fn lock( - index_locations: IndexLocations, upgrade: Upgrade, - exclude_newer: Option, python: Option, + settings: ResolverSettings, preview: PreviewMode, + connectivity: Connectivity, + concurrency: Concurrency, + native_tls: bool, cache: &Cache, printer: Printer, ) -> anyhow::Result { @@ -57,10 +63,19 @@ pub(crate) async fn lock( root_project_name, &workspace, &interpreter, - &index_locations, upgrade, - exclude_newer, + &settings.index_locations, + &settings.index_strategy, + &settings.keyring_provider, + &settings.resolution, + &settings.prerelease, + &settings.config_setting, + settings.exclude_newer.as_ref(), + &settings.link_mode, preview, + connectivity, + concurrency, + native_tls, cache, printer, ) @@ -85,10 +100,19 @@ pub(super) async fn do_lock( root_project_name: Option, workspace: &Workspace, interpreter: &Interpreter, - index_locations: &IndexLocations, upgrade: Upgrade, - exclude_newer: Option, + index_locations: &IndexLocations, + index_strategy: &IndexStrategy, + keyring_provider: &KeyringProviderType, + resolution: &ResolutionMode, + prerelease: &PreReleaseMode, + config_setting: &ConfigSettings, + exclude_newer: Option<&ExcludeNewer>, + link_mode: &LinkMode, preview: PreviewMode, + connectivity: Connectivity, + concurrency: Concurrency, + native_tls: bool, cache: &Cache, printer: Printer, ) -> Result { @@ -137,28 +161,36 @@ pub(super) async fn do_lock( }; // Initialize the registry client. - // TODO(zanieb): Support client options e.g. offline, tls, etc. let client = RegistryClientBuilder::new(cache.clone()) + .native_tls(native_tls) + .connectivity(connectivity) .index_urls(index_locations.index_urls()) + .index_strategy(*index_strategy) + .keyring(*keyring_provider) .markers(interpreter.markers()) .platform(interpreter.platform()) .build(); - // TODO(charlie): Respect project configuration. - let build_isolation = BuildIsolation::default(); - let concurrency = Concurrency::default(); - let config_settings = ConfigSettings::default(); - let extras = ExtrasSpecification::default(); + let options = OptionsBuilder::new() + .resolution_mode(*resolution) + .prerelease_mode(*prerelease) + .exclude_newer(exclude_newer.copied()) + .index_strategy(*index_strategy) + .build(); + let hasher = HashStrategy::Generate; + + // Initialize any shared state. let in_flight = InFlight::default(); let index = InMemoryIndex::default(); - let link_mode = LinkMode::default(); + + // TODO(charlie): These are all default values. We should consider whether we want to make them + // optional on the downstream APIs. + let build_isolation = BuildIsolation::default(); let build_options = BuildOptions::default(); + let extras = ExtrasSpecification::default(); let reinstall = Reinstall::default(); let setup_py = SetupPyStrategy::default(); - let hasher = HashStrategy::Generate; - let options = OptionsBuilder::new().exclude_newer(exclude_newer).build(); - // Resolve the flat indexes from `--find-links`. let flat_index = { let client = FlatIndexClient::new(&client, cache); @@ -183,9 +215,9 @@ pub(super) async fn do_lock( &git, &in_flight, setup_py, - &config_settings, + config_setting, build_isolation, - link_mode, + *link_mode, &build_options, concurrency, preview, diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index f3cecb0ec..8822652e2 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -5,14 +5,13 @@ use itertools::Itertools; use owo_colors::OwoColorize; use tracing::debug; -use distribution_types::{IndexLocations, Resolution}; -use install_wheel_rs::linker::LinkMode; +use distribution_types::Resolution; use pep440_rs::Version; use uv_cache::Cache; -use uv_client::{BaseClientBuilder, Connectivity, RegistryClientBuilder}; +use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ - BuildOptions, Concurrency, ConfigSettings, ExtrasSpecification, PreviewMode, Reinstall, - SetupPyStrategy, Upgrade, + BuildOptions, Concurrency, ExtrasSpecification, PreviewMode, Reinstall, SetupPyStrategy, + Upgrade, }; use uv_dispatch::BuildDispatch; use uv_distribution::Workspace; @@ -20,7 +19,7 @@ use uv_fs::Simplified; use uv_git::GitResolver; use uv_installer::{SatisfiesResult, SitePackages}; use uv_requirements::{RequirementsSource, RequirementsSpecification}; -use uv_resolver::{FlatIndex, InMemoryIndex, Options, RequiresPython}; +use uv_resolver::{FlatIndex, InMemoryIndex, OptionsBuilder, RequiresPython}; use uv_toolchain::{ Interpreter, PythonEnvironment, SystemPython, Toolchain, ToolchainRequest, VersionRequest, }; @@ -29,6 +28,7 @@ use uv_warnings::warn_user; use crate::commands::pip; use crate::printer::Printer; +use crate::settings::ResolverInstallerSettings; pub(crate) mod add; pub(crate) mod lock; @@ -256,17 +256,35 @@ pub(crate) fn init_environment( } /// Update a [`PythonEnvironment`] to satisfy a set of [`RequirementsSource`]s. +#[allow(clippy::too_many_arguments)] pub(crate) async fn update_environment( venv: PythonEnvironment, requirements: &[RequirementsSource], - index_locations: &IndexLocations, + settings: &ResolverInstallerSettings, + preview: PreviewMode, connectivity: Connectivity, + concurrency: Concurrency, + native_tls: bool, cache: &Cache, printer: Printer, - preview: PreviewMode, ) -> Result { - // TODO(zanieb): Support client configuration - let client_builder = BaseClientBuilder::default().connectivity(connectivity); + // Extract the project settings. + let ResolverInstallerSettings { + index_locations, + index_strategy, + keyring_provider, + resolution, + prerelease, + config_setting, + exclude_newer, + link_mode, + compile_bytecode, + } = settings; + + let client_builder = BaseClientBuilder::new() + .connectivity(connectivity) + .native_tls(native_tls) + .keyring(*keyring_provider); // Read all requirements from the provided sources. // TODO(zanieb): Consider allowing constraints and extras @@ -304,35 +322,48 @@ pub(crate) async fn update_environment( let markers = venv.interpreter().markers(); // Initialize the registry client. - // TODO(zanieb): Support client options e.g. offline, tls, etc. let client = RegistryClientBuilder::new(cache.clone()) + .native_tls(native_tls) .connectivity(connectivity) .index_urls(index_locations.index_urls()) + .index_strategy(*index_strategy) + .keyring(*keyring_provider) .markers(markers) - .platform(venv.interpreter().platform()) + .platform(interpreter.platform()) .build(); - // TODO(charlie): Respect project configuration. - let build_isolation = BuildIsolation::default(); - let compile = false; - let concurrency = Concurrency::default(); - let config_settings = ConfigSettings::default(); - let dry_run = false; - let extras = ExtrasSpecification::default(); - let flat_index = FlatIndex::default(); + let options = OptionsBuilder::new() + .resolution_mode(*resolution) + .prerelease_mode(*prerelease) + .exclude_newer(*exclude_newer) + .index_strategy(*index_strategy) + .build(); + + // Initialize any shared state. let git = GitResolver::default(); - let dev = Vec::default(); - let hasher = HashStrategy::default(); let in_flight = InFlight::default(); let index = InMemoryIndex::default(); - let link_mode = LinkMode::default(); + + // TODO(charlie): These are all default values. We should consider whether we want to make them + // optional on the downstream APIs. + let build_isolation = BuildIsolation::default(); let build_options = BuildOptions::default(); - let options = Options::default(); + let dev = Vec::default(); + let dry_run = false; + let extras = ExtrasSpecification::default(); + let hasher = HashStrategy::default(); let preferences = Vec::default(); let reinstall = Reinstall::default(); let setup_py = SetupPyStrategy::default(); let upgrade = Upgrade::default(); + // Resolve the flat indexes from `--find-links`. + let flat_index = { + let client = FlatIndexClient::new(&client, cache); + let entries = client.fetch(index_locations.flat_index()).await?; + FlatIndex::from_entries(entries, Some(tags), &hasher, &build_options) + }; + // Create a build dispatch. let resolve_dispatch = BuildDispatch::new( &client, @@ -344,9 +375,9 @@ pub(crate) async fn update_environment( &git, &in_flight, setup_py, - &config_settings, + config_setting, build_isolation, - link_mode, + *link_mode, &build_options, concurrency, preview, @@ -403,9 +434,9 @@ pub(crate) async fn update_environment( &git, &in_flight, setup_py, - &config_settings, + config_setting, build_isolation, - link_mode, + *link_mode, &build_options, concurrency, preview, @@ -419,8 +450,8 @@ pub(crate) async fn update_environment( pip::operations::Modifications::Sufficient, &reinstall, &build_options, - link_mode, - compile, + *link_mode, + *compile_bytecode, index_locations, &hasher, tags, diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index e04e2edcc..6fe8f8774 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -2,14 +2,15 @@ use anyhow::Result; use pep508_rs::PackageName; use uv_distribution::pyproject_mut::PyProjectTomlMut; -use distribution_types::IndexLocations; use uv_cache::Cache; -use uv_configuration::{ExtrasSpecification, PreviewMode, Upgrade}; +use uv_client::Connectivity; +use uv_configuration::{Concurrency, ExtrasSpecification, PreviewMode, Upgrade}; use uv_distribution::ProjectWorkspace; use uv_warnings::warn_user; use crate::commands::{project, ExitStatus}; use crate::printer::Printer; +use crate::settings::{InstallerSettings, ResolverSettings}; /// Remove one or more packages from the project requirements. #[allow(clippy::too_many_arguments)] @@ -17,6 +18,9 @@ pub(crate) async fn remove( requirements: Vec, python: Option, preview: PreviewMode, + connectivity: Connectivity, + concurrency: Concurrency, + native_tls: bool, cache: &Cache, printer: Printer, ) -> Result { @@ -46,9 +50,9 @@ pub(crate) async fn remove( // Discover or create the virtual environment. let venv = project::init_environment(project.workspace(), python.as_deref(), cache, printer)?; - let index_locations = IndexLocations::default(); - let upgrade = Upgrade::None; - let exclude_newer = None; + // Use the default settings. + let settings = ResolverSettings::default(); + let upgrade = Upgrade::default(); // Lock and sync the environment. let root_project_name = project @@ -62,10 +66,19 @@ pub(crate) async fn remove( root_project_name, project.workspace(), venv.interpreter(), - &index_locations, upgrade, - exclude_newer, + &settings.index_locations, + &settings.index_strategy, + &settings.keyring_provider, + &settings.resolution, + &settings.prerelease, + &settings.config_setting, + settings.exclude_newer.as_ref(), + &settings.link_mode, preview, + connectivity, + concurrency, + native_tls, cache, printer, ) @@ -73,6 +86,7 @@ pub(crate) async fn remove( // Perform a full sync, because we don't know what exactly is affected by the removal. // TODO(ibraheem): Should we accept CLI overrides for this? Should we even sync here? + let settings = InstallerSettings::default(); let extras = ExtrasSpecification::All; let dev = true; @@ -81,10 +95,18 @@ pub(crate) async fn remove( project.workspace().root(), &venv, &lock, - &index_locations, extras, dev, + &settings.index_locations, + &settings.index_strategy, + &settings.keyring_provider, + &settings.config_setting, + &settings.link_mode, + &settings.compile_bytecode, preview, + connectivity, + concurrency, + native_tls, cache, printer, ) diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index d0ef4b28a..5709393c7 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -2,28 +2,26 @@ use std::ffi::OsString; use std::path::PathBuf; use anyhow::{Context, Result}; -use distribution_types::IndexLocations; use itertools::Itertools; use tokio::process::Command; use tracing::debug; use uv_cache::Cache; use uv_client::{BaseClientBuilder, Connectivity}; -use uv_configuration::{ExtrasSpecification, PreviewMode, Upgrade}; +use uv_configuration::{Concurrency, ExtrasSpecification, PreviewMode, Upgrade}; use uv_distribution::{ProjectWorkspace, Workspace}; use uv_normalize::PackageName; use uv_requirements::RequirementsSource; -use uv_resolver::ExcludeNewer; use uv_toolchain::{PythonEnvironment, SystemPython, Toolchain}; use uv_warnings::warn_user; use crate::commands::{project, ExitStatus}; use crate::printer::Printer; +use crate::settings::ResolverInstallerSettings; /// Run a command. #[allow(clippy::too_many_arguments)] pub(crate) async fn run( - index_locations: IndexLocations, extras: ExtrasSpecification, dev: bool, target: Option, @@ -31,16 +29,16 @@ pub(crate) async fn run( requirements: Vec, python: Option, upgrade: Upgrade, - exclude_newer: Option, package: Option, + settings: ResolverInstallerSettings, isolated: bool, preview: PreviewMode, connectivity: Connectivity, + concurrency: Concurrency, + native_tls: bool, cache: &Cache, printer: Printer, ) -> Result { - let client_builder = BaseClientBuilder::new().connectivity(connectivity); - if preview.is_disabled() { warn_user!("`uv run` is experimental and may change without warning."); } @@ -76,10 +74,19 @@ pub(crate) async fn run( root_project_name, project.workspace(), venv.interpreter(), - &index_locations, upgrade, - exclude_newer, + &settings.index_locations, + &settings.index_strategy, + &settings.keyring_provider, + &settings.resolution, + &settings.prerelease, + &settings.config_setting, + settings.exclude_newer.as_ref(), + &settings.link_mode, preview, + connectivity, + concurrency, + native_tls, cache, printer, ) @@ -89,10 +96,18 @@ pub(crate) async fn run( project.workspace().root(), &venv, &lock, - &index_locations, extras, dev, + &settings.index_locations, + &settings.index_strategy, + &settings.keyring_provider, + &settings.config_setting, + &settings.link_mode, + &settings.compile_bytecode, preview, + connectivity, + concurrency, + native_tls, cache, printer, ) @@ -108,6 +123,10 @@ pub(crate) async fn run( } else { debug!("Syncing ephemeral environment."); + let client_builder = BaseClientBuilder::new() + .connectivity(connectivity) + .native_tls(native_tls); + // Discover an interpreter. let interpreter = if let Some(project_env) = &project_env { project_env.interpreter().clone() @@ -144,11 +163,13 @@ pub(crate) async fn run( project::update_environment( venv, &requirements, - &index_locations, + &settings, + preview, connectivity, + concurrency, + native_tls, cache, printer, - preview, ) .await?, ) diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index ebabe9017..1f83b613e 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -1,13 +1,14 @@ -use anyhow::Result; use std::path::Path; +use anyhow::Result; use distribution_types::IndexLocations; use install_wheel_rs::linker::LinkMode; + use uv_cache::Cache; -use uv_client::{FlatIndexClient, RegistryClientBuilder}; +use uv_client::{Connectivity, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ - BuildOptions, Concurrency, ConfigSettings, ExtrasSpecification, PreviewMode, Reinstall, - SetupPyStrategy, + BuildOptions, Concurrency, ConfigSettings, ExtrasSpecification, IndexStrategy, + KeyringProviderType, PreviewMode, Reinstall, SetupPyStrategy, }; use uv_dispatch::BuildDispatch; use uv_distribution::{ProjectWorkspace, DEV_DEPENDENCIES}; @@ -23,15 +24,19 @@ use crate::commands::pip::operations::Modifications; use crate::commands::project::ProjectError; use crate::commands::{pip, project, ExitStatus}; use crate::printer::Printer; +use crate::settings::InstallerSettings; /// Sync the project environment. #[allow(clippy::too_many_arguments)] pub(crate) async fn sync( - index_locations: IndexLocations, extras: ExtrasSpecification, dev: bool, python: Option, + settings: InstallerSettings, preview: PreviewMode, + connectivity: Connectivity, + concurrency: Concurrency, + native_tls: bool, cache: &Cache, printer: Printer, ) -> Result { @@ -58,10 +63,18 @@ pub(crate) async fn sync( project.workspace().root(), &venv, &lock, - &index_locations, extras, dev, + &settings.index_locations, + &settings.index_strategy, + &settings.keyring_provider, + &settings.config_setting, + &settings.link_mode, + &settings.compile_bytecode, preview, + connectivity, + concurrency, + native_tls, cache, printer, ) @@ -77,10 +90,18 @@ pub(super) async fn do_sync( workspace_root: &Path, venv: &PythonEnvironment, lock: &Lock, - index_locations: &IndexLocations, extras: ExtrasSpecification, dev: bool, + index_locations: &IndexLocations, + index_strategy: &IndexStrategy, + keyring_provider: &KeyringProviderType, + config_setting: &ConfigSettings, + link_mode: &LinkMode, + compile_bytecode: &bool, preview: PreviewMode, + connectivity: Connectivity, + concurrency: Concurrency, + native_tls: bool, cache: &Cache, printer: Printer, ) -> Result<(), ProjectError> { @@ -109,25 +130,27 @@ pub(super) async fn do_sync( lock.to_resolution(workspace_root, markers, tags, project_name, &extras, &dev)?; // Initialize the registry client. - // TODO(zanieb): Support client options e.g. offline, tls, etc. let client = RegistryClientBuilder::new(cache.clone()) + .native_tls(native_tls) + .connectivity(connectivity) .index_urls(index_locations.index_urls()) + .index_strategy(*index_strategy) + .keyring(*keyring_provider) .markers(markers) .platform(venv.interpreter().platform()) .build(); - // TODO(charlie): Respect project configuration. - let build_isolation = BuildIsolation::default(); - let compile = false; - let concurrency = Concurrency::default(); - let config_settings = ConfigSettings::default(); - let dry_run = false; + // Initialize any shared state. let git = GitResolver::default(); - let hasher = HashStrategy::default(); let in_flight = InFlight::default(); let index = InMemoryIndex::default(); - let link_mode = LinkMode::default(); + + // TODO(charlie): These are all default values. We should consider whether we want to make them + // optional on the downstream APIs. + let build_isolation = BuildIsolation::default(); let build_options = BuildOptions::default(); + let dry_run = false; + let hasher = HashStrategy::default(); let reinstall = Reinstall::default(); let setup_py = SetupPyStrategy::default(); @@ -149,9 +172,9 @@ pub(super) async fn do_sync( &git, &in_flight, setup_py, - &config_settings, + config_setting, build_isolation, - link_mode, + *link_mode, &build_options, concurrency, preview, @@ -166,8 +189,8 @@ pub(super) async fn do_sync( Modifications::Sufficient, &reinstall, &build_options, - link_mode, - compile, + *link_mode, + *compile_bytecode, index_locations, &hasher, tags, diff --git a/crates/uv/src/commands/tool/run.rs b/crates/uv/src/commands/tool/run.rs index 23cd61302..8a858d3d6 100644 --- a/crates/uv/src/commands/tool/run.rs +++ b/crates/uv/src/commands/tool/run.rs @@ -6,10 +6,9 @@ use itertools::Itertools; use tokio::process::Command; use tracing::debug; -use distribution_types::IndexLocations; use uv_cache::Cache; use uv_client::Connectivity; -use uv_configuration::PreviewMode; +use uv_configuration::{Concurrency, PreviewMode}; use uv_requirements::RequirementsSource; use uv_toolchain::{PythonEnvironment, SystemPython, Toolchain}; use uv_warnings::warn_user; @@ -17,6 +16,7 @@ use uv_warnings::warn_user; use crate::commands::project::update_environment; use crate::commands::ExitStatus; use crate::printer::Printer; +use crate::settings::ResolverInstallerSettings; /// Run a command. #[allow(clippy::too_many_arguments)] @@ -26,10 +26,12 @@ pub(crate) async fn run( python: Option, from: Option, with: Vec, + settings: ResolverInstallerSettings, _isolated: bool, preview: PreviewMode, - index_locations: IndexLocations, connectivity: Connectivity, + concurrency: Concurrency, + native_tls: bool, cache: &Cache, printer: Printer, ) -> Result { @@ -75,11 +77,13 @@ pub(crate) async fn run( update_environment( venv, &requirements, - &index_locations, + &settings, + preview, connectivity, + concurrency, + native_tls, cache, printer, - preview, ) .await?, ); diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index c95d0da5c..e659879fb 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -12,6 +12,7 @@ use tracing::{debug, instrument}; use cli::{ToolCommand, ToolNamespace, ToolchainCommand, ToolchainNamespace}; use uv_cache::Cache; +use uv_configuration::Concurrency; use uv_requirements::RequirementsSource; use uv_workspace::Combine; @@ -123,6 +124,8 @@ async fn run() -> Result { } else if cli.global_args.isolated { None } else { + // TODO(charlie): This needs to discover settings from the workspace _root_. Right now, it + // discovers the closest `pyproject.toml`, which could be a workspace _member_. let project = uv_workspace::Workspace::find(env::current_dir()?)?; let user = uv_workspace::Workspace::user()?; project.combine(user) @@ -189,7 +192,7 @@ async fn run() -> Result { // Resolve the settings from the command-line arguments and workspace configuration. let args = PipCompileSettings::resolve(args, workspace); rayon::ThreadPoolBuilder::new() - .num_threads(args.pip.concurrency.installs) + .num_threads(args.settings.concurrency.installs) .build_global() .expect("failed to initialize global rayon pool"); @@ -217,39 +220,39 @@ async fn run() -> Result { &constraints, &overrides, args.overrides_from_workspace, - args.pip.extras, - args.pip.output_file.as_deref(), - args.pip.resolution, - args.pip.prerelease, - args.pip.dependency_mode, + args.settings.extras, + args.settings.output_file.as_deref(), + args.settings.resolution, + args.settings.prerelease, + args.settings.dependency_mode, args.upgrade, - args.pip.generate_hashes, - args.pip.no_emit_package, - args.pip.no_strip_extras, - !args.pip.no_annotate, - !args.pip.no_header, - args.pip.custom_compile_command, - args.pip.emit_index_url, - args.pip.emit_find_links, - args.pip.emit_marker_expression, - args.pip.emit_index_annotation, - args.pip.index_locations, - args.pip.index_strategy, - args.pip.keyring_provider, - args.pip.setup_py, - args.pip.config_setting, + args.settings.generate_hashes, + args.settings.no_emit_package, + args.settings.no_strip_extras, + !args.settings.no_annotate, + !args.settings.no_header, + args.settings.custom_compile_command, + args.settings.emit_index_url, + args.settings.emit_find_links, + args.settings.emit_marker_expression, + args.settings.emit_index_annotation, + args.settings.index_locations, + args.settings.index_strategy, + args.settings.keyring_provider, + args.settings.setup_py, + args.settings.config_setting, globals.connectivity, - args.pip.no_build_isolation, - args.pip.no_build, - args.pip.no_binary, - args.pip.python_version, - args.pip.python_platform, - args.pip.exclude_newer, - args.pip.annotation_style, - args.pip.link_mode, - args.pip.python, - args.pip.system, - args.pip.concurrency, + args.settings.no_build_isolation, + args.settings.no_build, + args.settings.no_binary, + args.settings.python_version, + args.settings.python_platform, + args.settings.exclude_newer, + args.settings.annotation_style, + args.settings.link_mode, + args.settings.python, + args.settings.system, + args.settings.concurrency, globals.native_tls, globals.quiet, globals.preview, @@ -266,7 +269,7 @@ async fn run() -> Result { // Resolve the settings from the command-line arguments and workspace configuration. let args = PipSyncSettings::resolve(args, workspace); rayon::ThreadPoolBuilder::new() - .num_threads(args.pip.concurrency.installs) + .num_threads(args.settings.concurrency.installs) .build_global() .expect("failed to initialize global rayon pool"); @@ -288,28 +291,28 @@ async fn run() -> Result { &requirements, &constraints, &args.reinstall, - args.pip.link_mode, - args.pip.compile_bytecode, - args.pip.require_hashes, - args.pip.index_locations, - args.pip.index_strategy, - args.pip.keyring_provider, - args.pip.setup_py, + args.settings.link_mode, + args.settings.compile_bytecode, + args.settings.require_hashes, + args.settings.index_locations, + args.settings.index_strategy, + args.settings.keyring_provider, + args.settings.setup_py, globals.connectivity, - &args.pip.config_setting, - args.pip.no_build_isolation, - args.pip.no_build, - args.pip.no_binary, - args.pip.python_version, - args.pip.python_platform, - args.pip.strict, - args.pip.exclude_newer, - args.pip.python, - args.pip.system, - args.pip.break_system_packages, - args.pip.target, - args.pip.prefix, - args.pip.concurrency, + &args.settings.config_setting, + args.settings.no_build_isolation, + args.settings.no_build, + args.settings.no_binary, + args.settings.python_version, + args.settings.python_platform, + args.settings.strict, + args.settings.exclude_newer, + args.settings.python, + args.settings.system, + args.settings.break_system_packages, + args.settings.target, + args.settings.prefix, + args.settings.concurrency, globals.native_tls, globals.preview, cache, @@ -326,7 +329,7 @@ async fn run() -> Result { // Resolve the settings from the command-line arguments and workspace configuration. let args = PipInstallSettings::resolve(args, workspace); rayon::ThreadPoolBuilder::new() - .num_threads(args.pip.concurrency.installs) + .num_threads(args.settings.concurrency.installs) .build_global() .expect("failed to initialize global rayon pool"); @@ -359,34 +362,34 @@ async fn run() -> Result { &constraints, &overrides, args.overrides_from_workspace, - &args.pip.extras, - args.pip.resolution, - args.pip.prerelease, - args.pip.dependency_mode, + &args.settings.extras, + args.settings.resolution, + args.settings.prerelease, + args.settings.dependency_mode, args.upgrade, - args.pip.index_locations, - args.pip.index_strategy, - args.pip.keyring_provider, + args.settings.index_locations, + args.settings.index_strategy, + args.settings.keyring_provider, args.reinstall, - args.pip.link_mode, - args.pip.compile_bytecode, - args.pip.require_hashes, - args.pip.setup_py, + args.settings.link_mode, + args.settings.compile_bytecode, + args.settings.require_hashes, + args.settings.setup_py, globals.connectivity, - &args.pip.config_setting, - args.pip.no_build_isolation, - args.pip.no_build, - args.pip.no_binary, - args.pip.python_version, - args.pip.python_platform, - args.pip.strict, - args.pip.exclude_newer, - args.pip.python, - args.pip.system, - args.pip.break_system_packages, - args.pip.target, - args.pip.prefix, - args.pip.concurrency, + &args.settings.config_setting, + args.settings.no_build_isolation, + args.settings.no_build, + args.settings.no_binary, + args.settings.python_version, + args.settings.python_platform, + args.settings.strict, + args.settings.exclude_newer, + args.settings.python, + args.settings.system, + args.settings.break_system_packages, + args.settings.target, + args.settings.prefix, + args.settings.concurrency, globals.native_tls, globals.preview, cache, @@ -416,16 +419,16 @@ async fn run() -> Result { .collect::>(); commands::pip_uninstall( &sources, - args.pip.python, - args.pip.system, - args.pip.break_system_packages, - args.pip.target, - args.pip.prefix, + args.settings.python, + args.settings.system, + args.settings.break_system_packages, + args.settings.target, + args.settings.prefix, cache, globals.connectivity, globals.native_tls, globals.preview, - args.pip.keyring_provider, + args.settings.keyring_provider, printer, ) .await @@ -441,9 +444,9 @@ async fn run() -> Result { commands::pip_freeze( args.exclude_editable, - args.pip.strict, - args.pip.python.as_deref(), - args.pip.system, + args.settings.strict, + args.settings.python.as_deref(), + args.settings.system, globals.preview, &cache, printer, @@ -465,9 +468,9 @@ async fn run() -> Result { args.exclude_editable, &args.exclude, &args.format, - args.pip.strict, - args.pip.python.as_deref(), - args.pip.system, + args.settings.strict, + args.settings.python.as_deref(), + args.settings.system, globals.preview, &cache, printer, @@ -484,9 +487,9 @@ async fn run() -> Result { commands::pip_show( args.package, - args.pip.strict, - args.pip.python.as_deref(), - args.pip.system, + args.settings.strict, + args.settings.python.as_deref(), + args.settings.system, globals.preview, &cache, printer, @@ -502,8 +505,8 @@ async fn run() -> Result { let cache = cache.init()?; commands::pip_check( - args.shared.python.as_deref(), - args.shared.system, + args.settings.python.as_deref(), + args.settings.system, globals.preview, &cache, printer, @@ -542,17 +545,17 @@ async fn run() -> Result { commands::venv( &args.name, - args.pip.python.as_deref(), - args.pip.link_mode, - &args.pip.index_locations, - args.pip.index_strategy, - args.pip.keyring_provider, + args.settings.python.as_deref(), + args.settings.link_mode, + &args.settings.index_locations, + args.settings.index_strategy, + args.settings.keyring_provider, uv_virtualenv::Prompt::from_args(prompt), args.system_site_packages, globals.connectivity, args.seed, args.allow_existing, - args.pip.exclude_newer, + args.settings.exclude_newer, globals.native_tls, globals.preview, &cache, @@ -588,7 +591,6 @@ async fn run() -> Result { .collect::>(); commands::run( - args.index_locations, args.extras, args.dev, args.target, @@ -596,11 +598,13 @@ async fn run() -> Result { requirements, args.python, args.upgrade, - args.exclude_newer, args.package, + args.settings, globals.isolated, globals.preview, globals.connectivity, + Concurrency::default(), + globals.native_tls, &cache, printer, ) @@ -614,11 +618,14 @@ async fn run() -> Result { let cache = cache.init()?.with_refresh(args.refresh); commands::sync( - args.index_locations, args.extras, args.dev, args.python, + args.settings, globals.preview, + globals.connectivity, + Concurrency::default(), + globals.native_tls, &cache, printer, ) @@ -632,11 +639,13 @@ async fn run() -> Result { let cache = cache.init()?.with_refresh(args.refresh); commands::lock( - args.index_locations, args.upgrade, - args.exclude_newer, args.python, + args.settings, globals.preview, + globals.connectivity, + Concurrency::default(), + globals.native_tls, &cache, printer, ) @@ -653,6 +662,9 @@ async fn run() -> Result { args.requirements, args.python, globals.preview, + globals.connectivity, + Concurrency::default(), + globals.native_tls, &cache, printer, ) @@ -669,6 +681,9 @@ async fn run() -> Result { args.requirements, args.python, globals.preview, + globals.connectivity, + Concurrency::default(), + globals.native_tls, &cache, printer, ) @@ -701,10 +716,12 @@ async fn run() -> Result { args.python, args.from, args.with, + args.settings, globals.isolated, globals.preview, - args.index_locations, globals.connectivity, + Concurrency::default(), + globals.native_tls, &cache, printer, ) diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index a1e5d6bbd..1bddca8e7 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -18,12 +18,15 @@ use uv_configuration::{ use uv_normalize::PackageName; use uv_resolver::{AnnotationStyle, DependencyMode, ExcludeNewer, PreReleaseMode, ResolutionMode}; use uv_toolchain::{Prefix, PythonVersion, Target}; -use uv_workspace::{Combine, PipOptions, Workspace}; +use uv_workspace::{ + Combine, InstallerOptions, PipOptions, ResolverInstallerOptions, ResolverOptions, Workspace, +}; use crate::cli::{ - AddArgs, ColorChoice, GlobalArgs, IndexArgs, LockArgs, Maybe, PipCheckArgs, PipCompileArgs, - PipFreezeArgs, PipInstallArgs, PipListArgs, PipShowArgs, PipSyncArgs, PipUninstallArgs, - RemoveArgs, RunArgs, SyncArgs, ToolRunArgs, ToolchainInstallArgs, ToolchainListArgs, VenvArgs, + AddArgs, ColorChoice, GlobalArgs, IndexArgs, InstallerArgs, LockArgs, Maybe, PipCheckArgs, + PipCompileArgs, PipFreezeArgs, PipInstallArgs, PipListArgs, PipShowArgs, PipSyncArgs, + PipUninstallArgs, RemoveArgs, ResolverArgs, ResolverInstallerArgs, RunArgs, SyncArgs, + ToolRunArgs, ToolchainInstallArgs, ToolchainListArgs, VenvArgs, }; use crate::commands::ListFormat; @@ -64,10 +67,10 @@ impl GlobalSettings { args.color }, native_tls: flag(args.native_tls, args.no_native_tls) - .combine(workspace.and_then(|workspace| workspace.options.native_tls)) + .combine(workspace.and_then(|workspace| workspace.options.globals.native_tls)) .unwrap_or(false), connectivity: if flag(args.offline, args.no_offline) - .combine(workspace.and_then(|workspace| workspace.options.offline)) + .combine(workspace.and_then(|workspace| workspace.options.globals.offline)) .unwrap_or(false) { Connectivity::Offline @@ -77,7 +80,7 @@ impl GlobalSettings { isolated: args.isolated, preview: PreviewMode::from( flag(args.preview, args.no_preview) - .combine(workspace.and_then(|workspace| workspace.options.preview)) + .combine(workspace.and_then(|workspace| workspace.options.globals.preview)) .unwrap_or(false), ), } @@ -98,11 +101,11 @@ impl CacheSettings { Self { no_cache: args.no_cache || workspace - .and_then(|workspace| workspace.options.no_cache) + .and_then(|workspace| workspace.options.globals.no_cache) .unwrap_or(false), - cache_dir: args - .cache_dir - .or_else(|| workspace.and_then(|workspace| workspace.options.cache_dir.clone())), + cache_dir: args.cache_dir.or_else(|| { + workspace.and_then(|workspace| workspace.options.globals.cache_dir.clone()) + }), } } } @@ -111,7 +114,6 @@ impl CacheSettings { #[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct RunSettings { - pub(crate) index_locations: IndexLocations, pub(crate) extras: ExtrasSpecification, pub(crate) dev: bool, pub(crate) target: Option, @@ -120,14 +122,14 @@ pub(crate) struct RunSettings { pub(crate) python: Option, pub(crate) refresh: Refresh, pub(crate) upgrade: Upgrade, - pub(crate) exclude_newer: Option, pub(crate) package: Option, + pub(crate) settings: ResolverInstallerSettings, } impl RunSettings { /// Resolve the [`RunSettings`] from the CLI and workspace configuration. #[allow(clippy::needless_pass_by_value)] - pub(crate) fn resolve(args: RunArgs, _workspace: Option) -> Self { + pub(crate) fn resolve(args: RunArgs, workspace: Option) -> Self { let RunArgs { extra, all_extras, @@ -143,14 +145,12 @@ impl RunSettings { upgrade, no_upgrade, upgrade_package, - index_args, + installer, python, - exclude_newer, package, } = args; Self { - index_locations: IndexLocations::from(index_args), refresh: Refresh::from_args(flag(refresh, no_refresh), refresh_package), upgrade: Upgrade::from_args(flag(upgrade, no_upgrade), upgrade_package), extras: ExtrasSpecification::from_args( @@ -162,8 +162,11 @@ impl RunSettings { args, with, python, - exclude_newer, package, + settings: ResolverInstallerSettings::combine( + ResolverInstallerOptions::from(installer), + workspace, + ), } } } @@ -172,34 +175,37 @@ impl RunSettings { #[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct ToolRunSettings { - pub(crate) index_locations: IndexLocations, pub(crate) target: String, pub(crate) args: Vec, pub(crate) from: Option, pub(crate) with: Vec, pub(crate) python: Option, + pub(crate) settings: ResolverInstallerSettings, } impl ToolRunSettings { /// Resolve the [`ToolRunSettings`] from the CLI and workspace configuration. #[allow(clippy::needless_pass_by_value)] - pub(crate) fn resolve(args: ToolRunArgs, _workspace: Option) -> Self { + pub(crate) fn resolve(args: ToolRunArgs, workspace: Option) -> Self { let ToolRunArgs { target, args, from, with, - index_args, + installer, python, } = args; Self { - index_locations: IndexLocations::from(index_args), target, args, from, with, python, + settings: ResolverInstallerSettings::combine( + ResolverInstallerOptions::from(installer), + workspace, + ), } } } @@ -266,17 +272,17 @@ impl ToolchainInstallSettings { #[allow(clippy::struct_excessive_bools, dead_code)] #[derive(Debug, Clone)] pub(crate) struct SyncSettings { - pub(crate) index_locations: IndexLocations, pub(crate) refresh: Refresh, pub(crate) extras: ExtrasSpecification, pub(crate) dev: bool, pub(crate) python: Option, + pub(crate) settings: InstallerSettings, } impl SyncSettings { /// Resolve the [`SyncSettings`] from the CLI and workspace configuration. #[allow(clippy::needless_pass_by_value)] - pub(crate) fn resolve(args: SyncArgs, _workspace: Option) -> Self { + pub(crate) fn resolve(args: SyncArgs, workspace: Option) -> Self { let SyncArgs { extra, all_extras, @@ -286,12 +292,11 @@ impl SyncSettings { refresh, no_refresh, refresh_package, - index_args, + installer, python, } = args; Self { - index_locations: IndexLocations::from(index_args), refresh: Refresh::from_args(flag(refresh, no_refresh), refresh_package), extras: ExtrasSpecification::from_args( flag(all_extras, no_all_extras).unwrap_or_default(), @@ -299,6 +304,7 @@ impl SyncSettings { ), dev: flag(dev, no_dev).unwrap_or(true), python, + settings: InstallerSettings::combine(InstallerOptions::from(installer), workspace), } } } @@ -307,17 +313,16 @@ impl SyncSettings { #[allow(clippy::struct_excessive_bools, dead_code)] #[derive(Debug, Clone)] pub(crate) struct LockSettings { - pub(crate) index_locations: IndexLocations, pub(crate) refresh: Refresh, pub(crate) upgrade: Upgrade, - pub(crate) exclude_newer: Option, pub(crate) python: Option, + pub(crate) settings: ResolverSettings, } impl LockSettings { /// Resolve the [`LockSettings`] from the CLI and workspace configuration. #[allow(clippy::needless_pass_by_value)] - pub(crate) fn resolve(args: LockArgs, _workspace: Option) -> Self { + pub(crate) fn resolve(args: LockArgs, workspace: Option) -> Self { let LockArgs { refresh, no_refresh, @@ -325,17 +330,15 @@ impl LockSettings { upgrade, no_upgrade, upgrade_package, - index_args, - exclude_newer, + resolver, python, } = args; Self { - index_locations: IndexLocations::from(index_args), refresh: Refresh::from_args(flag(refresh, no_refresh), refresh_package), upgrade: Upgrade::from_args(flag(upgrade, no_upgrade), upgrade_package), - exclude_newer, python, + settings: ResolverSettings::combine(ResolverOptions::from(resolver), workspace), } } } @@ -397,8 +400,8 @@ pub(crate) struct PipCompileSettings { pub(crate) r#override: Vec, pub(crate) refresh: Refresh, pub(crate) upgrade: Upgrade, - pub(crate) pip: PipSettings, pub(crate) overrides_from_workspace: Vec, + pub(crate) settings: PipSettings, } impl PipCompileSettings { @@ -413,9 +416,6 @@ impl PipCompileSettings { no_all_extras, no_deps, deps, - resolution, - prerelease, - pre, output_file, no_strip_extras, strip_extras, @@ -428,10 +428,7 @@ impl PipCompileSettings { refresh, no_refresh, refresh_package, - link_mode, - index_args, - index_strategy, - keyring_provider, + resolver, python, system, no_system, @@ -448,10 +445,8 @@ impl PipCompileSettings { build, no_binary, only_binary, - config_setting, python_version, python_platform, - exclude_newer, no_emit_package, emit_index_url, no_emit_index_url, @@ -489,21 +484,10 @@ impl PipCompileSettings { refresh: Refresh::from_args(flag(refresh, no_refresh), refresh_package), upgrade: Upgrade::from_args(flag(upgrade, no_upgrade), upgrade_package), overrides_from_workspace, - pip: PipSettings::combine( + settings: PipSettings::combine( PipOptions { python, system: flag(system, no_system), - index_url: index_args.index_url.and_then(Maybe::into_option), - extra_index_url: index_args.extra_index_url.map(|extra_index_urls| { - extra_index_urls - .into_iter() - .filter_map(Maybe::into_option) - .collect() - }), - no_index: Some(index_args.no_index), - find_links: index_args.find_links, - index_strategy, - keyring_provider, no_build: flag(no_build, build), no_binary, only_binary, @@ -511,12 +495,6 @@ impl PipCompileSettings { extra, all_extras: flag(all_extras, no_all_extras), no_deps: flag(no_deps, deps), - resolution, - prerelease: if pre { - Some(PreReleaseMode::Allow) - } else { - prerelease - }, output_file, no_strip_extras: flag(no_strip_extras, strip_extras), no_annotate: flag(no_annotate, annotate), @@ -524,23 +502,18 @@ impl PipCompileSettings { custom_compile_command, generate_hashes: flag(generate_hashes, no_generate_hashes), legacy_setup_py: flag(legacy_setup_py, no_legacy_setup_py), - config_settings: config_setting.map(|config_settings| { - config_settings.into_iter().collect::() - }), python_version, python_platform, - exclude_newer, no_emit_package, emit_index_url: flag(emit_index_url, no_emit_index_url), emit_find_links: flag(emit_find_links, no_emit_find_links), emit_marker_expression: flag(emit_marker_expression, no_emit_marker_expression), emit_index_annotation: flag(emit_index_annotation, no_emit_index_annotation), annotation_style, - link_mode, concurrent_builds: env(env::CONCURRENT_BUILDS), concurrent_downloads: env(env::CONCURRENT_DOWNLOADS), concurrent_installs: env(env::CONCURRENT_INSTALLS), - ..PipOptions::default() + ..PipOptions::from(resolver) }, workspace, ), @@ -557,7 +530,7 @@ pub(crate) struct PipSyncSettings { pub(crate) reinstall: Reinstall, pub(crate) refresh: Refresh, pub(crate) dry_run: bool, - pub(crate) pip: PipSettings, + pub(crate) settings: PipSettings, } impl PipSyncSettings { @@ -566,18 +539,16 @@ impl PipSyncSettings { let PipSyncArgs { src_file, constraint, + installer, + exclude_newer, reinstall, no_reinstall, reinstall_package, refresh, no_refresh, refresh_package, - link_mode, - index_args, - index_strategy, require_hashes, no_require_hashes, - keyring_provider, python, system, no_system, @@ -593,14 +564,10 @@ impl PipSyncSettings { build, no_binary, only_binary, - compile_bytecode, - no_compile_bytecode, - config_setting, python_version, python_platform, strict, no_strict, - exclude_newer, dry_run, compat_args: _, } = args; @@ -614,43 +581,27 @@ impl PipSyncSettings { reinstall: Reinstall::from_args(flag(reinstall, no_reinstall), reinstall_package), refresh: Refresh::from_args(flag(refresh, no_refresh), refresh_package), dry_run, - pip: PipSettings::combine( + settings: PipSettings::combine( PipOptions { python, system: flag(system, no_system), break_system_packages: flag(break_system_packages, no_break_system_packages), + exclude_newer, target, prefix, - index_url: index_args.index_url.and_then(Maybe::into_option), - extra_index_url: index_args.extra_index_url.map(|extra_index_urls| { - extra_index_urls - .into_iter() - .filter_map(Maybe::into_option) - .collect() - }), - no_index: Some(index_args.no_index), - find_links: index_args.find_links, - index_strategy, - keyring_provider, no_build: flag(no_build, build), no_binary, only_binary, no_build_isolation: flag(no_build_isolation, build_isolation), strict: flag(strict, no_strict), legacy_setup_py: flag(legacy_setup_py, no_legacy_setup_py), - config_settings: config_setting.map(|config_settings| { - config_settings.into_iter().collect::() - }), python_version, python_platform, - exclude_newer, - link_mode, - compile_bytecode: flag(compile_bytecode, no_compile_bytecode), require_hashes: flag(require_hashes, no_require_hashes), concurrent_builds: env(env::CONCURRENT_BUILDS), concurrent_downloads: env(env::CONCURRENT_DOWNLOADS), concurrent_installs: env(env::CONCURRENT_INSTALLS), - ..PipOptions::default() + ..PipOptions::from(installer) }, workspace, ), @@ -672,7 +623,7 @@ pub(crate) struct PipInstallSettings { pub(crate) refresh: Refresh, pub(crate) dry_run: bool, pub(crate) overrides_from_workspace: Vec, - pub(crate) pip: PipSettings, + pub(crate) settings: PipSettings, } impl PipInstallSettings { @@ -698,15 +649,9 @@ impl PipInstallSettings { refresh_package, no_deps, deps, - link_mode, - resolution, - prerelease, - pre, - index_args, - index_strategy, require_hashes, no_require_hashes, - keyring_provider, + installer, python, system, no_system, @@ -722,14 +667,10 @@ impl PipInstallSettings { build, no_binary, only_binary, - compile_bytecode, - no_compile_bytecode, - config_setting, python_version, python_platform, strict, no_strict, - exclude_newer, dry_run, compat_args: _, } = args; @@ -763,24 +704,13 @@ impl PipInstallSettings { refresh: Refresh::from_args(flag(refresh, no_refresh), refresh_package), dry_run, overrides_from_workspace, - pip: PipSettings::combine( + settings: PipSettings::combine( PipOptions { python, system: flag(system, no_system), break_system_packages: flag(break_system_packages, no_break_system_packages), target, prefix, - index_url: index_args.index_url.and_then(Maybe::into_option), - extra_index_url: index_args.extra_index_url.map(|extra_index_urls| { - extra_index_urls - .into_iter() - .filter_map(Maybe::into_option) - .collect() - }), - no_index: Some(index_args.no_index), - find_links: index_args.find_links, - index_strategy, - keyring_provider, no_build: flag(no_build, build), no_binary, only_binary, @@ -789,26 +719,14 @@ impl PipInstallSettings { extra, all_extras: flag(all_extras, no_all_extras), no_deps: flag(no_deps, deps), - resolution, - prerelease: if pre { - Some(PreReleaseMode::Allow) - } else { - prerelease - }, legacy_setup_py: flag(legacy_setup_py, no_legacy_setup_py), - config_settings: config_setting.map(|config_settings| { - config_settings.into_iter().collect::() - }), python_version, python_platform, - exclude_newer, - link_mode, - compile_bytecode: flag(compile_bytecode, no_compile_bytecode), require_hashes: flag(require_hashes, no_require_hashes), concurrent_builds: env(env::CONCURRENT_BUILDS), concurrent_downloads: env(env::CONCURRENT_DOWNLOADS), concurrent_installs: env(env::CONCURRENT_INSTALLS), - ..PipOptions::default() + ..PipOptions::from(installer) }, workspace, ), @@ -822,7 +740,7 @@ impl PipInstallSettings { pub(crate) struct PipUninstallSettings { pub(crate) package: Vec, pub(crate) requirement: Vec, - pub(crate) pip: PipSettings, + pub(crate) settings: PipSettings, } impl PipUninstallSettings { @@ -844,7 +762,7 @@ impl PipUninstallSettings { Self { package, requirement, - pip: PipSettings::combine( + settings: PipSettings::combine( PipOptions { python, system: flag(system, no_system), @@ -865,7 +783,7 @@ impl PipUninstallSettings { #[derive(Debug, Clone)] pub(crate) struct PipFreezeSettings { pub(crate) exclude_editable: bool, - pub(crate) pip: PipSettings, + pub(crate) settings: PipSettings, } impl PipFreezeSettings { @@ -882,7 +800,7 @@ impl PipFreezeSettings { Self { exclude_editable, - pip: PipSettings::combine( + settings: PipSettings::combine( PipOptions { python, system: flag(system, no_system), @@ -903,7 +821,7 @@ pub(crate) struct PipListSettings { pub(crate) exclude_editable: bool, pub(crate) exclude: Vec, pub(crate) format: ListFormat, - pub(crate) pip: PipSettings, + pub(crate) settings: PipSettings, } impl PipListSettings { @@ -927,7 +845,7 @@ impl PipListSettings { exclude_editable, exclude, format, - pip: PipSettings::combine( + settings: PipSettings::combine( PipOptions { python, system: flag(system, no_system), @@ -945,7 +863,7 @@ impl PipListSettings { #[derive(Debug, Clone)] pub(crate) struct PipShowSettings { pub(crate) package: Vec, - pub(crate) pip: PipSettings, + pub(crate) settings: PipSettings, } impl PipShowSettings { @@ -962,7 +880,7 @@ impl PipShowSettings { Self { package, - pip: PipSettings::combine( + settings: PipSettings::combine( PipOptions { python, system: flag(system, no_system), @@ -979,7 +897,7 @@ impl PipShowSettings { #[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct PipCheckSettings { - pub(crate) shared: PipSettings, + pub(crate) settings: PipSettings, } impl PipCheckSettings { @@ -992,7 +910,7 @@ impl PipCheckSettings { } = args; Self { - shared: PipSettings::combine( + settings: PipSettings::combine( PipOptions { python, system: flag(system, no_system), @@ -1013,7 +931,7 @@ pub(crate) struct VenvSettings { pub(crate) name: PathBuf, pub(crate) prompt: Option, pub(crate) system_site_packages: bool, - pub(crate) pip: PipSettings, + pub(crate) settings: PipSettings, } impl VenvSettings { @@ -1028,13 +946,11 @@ impl VenvSettings { name, prompt, system_site_packages, - link_mode, - index_url, - extra_index_url, - no_index, + index_args, index_strategy, keyring_provider, exclude_newer, + link_mode, compat_args: _, } = args; @@ -1044,23 +960,15 @@ impl VenvSettings { name, prompt, system_site_packages, - pip: PipSettings::combine( + settings: PipSettings::combine( PipOptions { python, system: flag(system, no_system), - index_url: index_url.and_then(Maybe::into_option), - extra_index_url: extra_index_url.map(|extra_index_urls| { - extra_index_urls - .into_iter() - .filter_map(Maybe::into_option) - .collect() - }), - no_index: Some(no_index), index_strategy, keyring_provider, exclude_newer, link_mode, - ..PipOptions::default() + ..PipOptions::from(index_args) }, workspace, ), @@ -1068,9 +976,213 @@ impl VenvSettings { } } +/// The resolved settings to use for an invocation of the `uv` CLI when installing dependencies. +/// +/// Combines the `[tool.uv]` persistent configuration with the command-line arguments +/// ([`InstallerArgs`], represented as [`InstallerOptions`]). +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Clone, Default)] +pub(crate) struct InstallerSettings { + pub(crate) index_locations: IndexLocations, + pub(crate) index_strategy: IndexStrategy, + pub(crate) keyring_provider: KeyringProviderType, + pub(crate) config_setting: ConfigSettings, + pub(crate) link_mode: LinkMode, + pub(crate) compile_bytecode: bool, +} + +impl InstallerSettings { + /// Resolve the [`InstallerSettings`] from the CLI and workspace configuration. + pub(crate) fn combine(args: InstallerOptions, workspace: Option) -> Self { + let ResolverInstallerOptions { + index_url, + extra_index_url, + no_index, + find_links, + index_strategy, + keyring_provider, + resolution: _, + prerelease: _, + config_settings, + exclude_newer: _, + link_mode, + compile_bytecode, + } = workspace + .map(|workspace| workspace.options.top_level) + .unwrap_or_default(); + + Self { + index_locations: IndexLocations::new( + args.index_url.combine(index_url), + args.extra_index_url + .combine(extra_index_url) + .unwrap_or_default(), + args.find_links.combine(find_links).unwrap_or_default(), + args.no_index.combine(no_index).unwrap_or_default(), + ), + index_strategy: args + .index_strategy + .combine(index_strategy) + .unwrap_or_default(), + keyring_provider: args + .keyring_provider + .combine(keyring_provider) + .unwrap_or_default(), + config_setting: args + .config_settings + .combine(config_settings) + .unwrap_or_default(), + link_mode: args.link_mode.combine(link_mode).unwrap_or_default(), + compile_bytecode: args + .compile_bytecode + .combine(compile_bytecode) + .unwrap_or_default(), + } + } +} + +/// The resolved settings to use for an invocation of the `uv` CLI when resolving dependencies. +/// +/// Combines the `[tool.uv]` persistent configuration with the command-line arguments +/// ([`ResolverArgs`], represented as [`ResolverOptions`]). +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Clone, Default)] +pub(crate) struct ResolverSettings { + pub(crate) index_locations: IndexLocations, + pub(crate) index_strategy: IndexStrategy, + pub(crate) keyring_provider: KeyringProviderType, + pub(crate) resolution: ResolutionMode, + pub(crate) prerelease: PreReleaseMode, + pub(crate) config_setting: ConfigSettings, + pub(crate) exclude_newer: Option, + pub(crate) link_mode: LinkMode, +} + +impl ResolverSettings { + /// Resolve the [`ResolverSettings`] from the CLI and workspace configuration. + pub(crate) fn combine(args: ResolverOptions, workspace: Option) -> Self { + let ResolverInstallerOptions { + index_url, + extra_index_url, + no_index, + find_links, + index_strategy, + keyring_provider, + resolution, + prerelease, + config_settings, + exclude_newer, + link_mode, + compile_bytecode: _, + } = workspace + .map(|workspace| workspace.options.top_level) + .unwrap_or_default(); + + Self { + index_locations: IndexLocations::new( + args.index_url.combine(index_url), + args.extra_index_url + .combine(extra_index_url) + .unwrap_or_default(), + args.find_links.combine(find_links).unwrap_or_default(), + args.no_index.combine(no_index).unwrap_or_default(), + ), + resolution: args.resolution.combine(resolution).unwrap_or_default(), + prerelease: args.prerelease.combine(prerelease).unwrap_or_default(), + index_strategy: args + .index_strategy + .combine(index_strategy) + .unwrap_or_default(), + keyring_provider: args + .keyring_provider + .combine(keyring_provider) + .unwrap_or_default(), + config_setting: args + .config_settings + .combine(config_settings) + .unwrap_or_default(), + exclude_newer: args.exclude_newer.combine(exclude_newer), + link_mode: args.link_mode.combine(link_mode).unwrap_or_default(), + } + } +} + +/// The resolved settings to use for an invocation of the `uv` CLI with both resolver and installer +/// capabilities. +/// +/// Represents the shared settings that are used across all `uv` commands outside the `pip` API. +/// Analogous to the settings contained in the `[tool.uv]` table, combined with [`ResolverInstallerArgs`]. +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Clone, Default)] +pub(crate) struct ResolverInstallerSettings { + pub(crate) index_locations: IndexLocations, + pub(crate) index_strategy: IndexStrategy, + pub(crate) keyring_provider: KeyringProviderType, + pub(crate) resolution: ResolutionMode, + pub(crate) prerelease: PreReleaseMode, + pub(crate) config_setting: ConfigSettings, + pub(crate) exclude_newer: Option, + pub(crate) link_mode: LinkMode, + pub(crate) compile_bytecode: bool, +} + +impl ResolverInstallerSettings { + /// Resolve the [`ResolverInstallerSettings`] from the CLI and workspace configuration. + pub(crate) fn combine(args: ResolverInstallerOptions, workspace: Option) -> Self { + let ResolverInstallerOptions { + index_url, + extra_index_url, + no_index, + find_links, + index_strategy, + keyring_provider, + resolution, + prerelease, + config_settings, + exclude_newer, + link_mode, + compile_bytecode, + } = workspace + .map(|workspace| workspace.options.top_level) + .unwrap_or_default(); + + Self { + index_locations: IndexLocations::new( + args.index_url.combine(index_url), + args.extra_index_url + .combine(extra_index_url) + .unwrap_or_default(), + args.find_links.combine(find_links).unwrap_or_default(), + args.no_index.combine(no_index).unwrap_or_default(), + ), + resolution: args.resolution.combine(resolution).unwrap_or_default(), + prerelease: args.prerelease.combine(prerelease).unwrap_or_default(), + index_strategy: args + .index_strategy + .combine(index_strategy) + .unwrap_or_default(), + keyring_provider: args + .keyring_provider + .combine(keyring_provider) + .unwrap_or_default(), + config_setting: args + .config_settings + .combine(config_settings) + .unwrap_or_default(), + exclude_newer: args.exclude_newer.combine(exclude_newer), + link_mode: args.link_mode.combine(link_mode).unwrap_or_default(), + compile_bytecode: args + .compile_bytecode + .combine(compile_bytecode) + .unwrap_or_default(), + } + } +} + /// The resolved settings to use for an invocation of the `pip` CLI. /// -/// Represents the shared settings that are used across all `pip` commands. +/// Represents the shared settings that are used across all `pip` commands. Analogous to the +/// settings contained in the `[tool.uv.pip]` table. #[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct PipSettings { @@ -1162,7 +1274,7 @@ impl PipSettings { concurrent_downloads, concurrent_installs, } = workspace - .and_then(|workspace| workspace.options.pip) + .map(|workspace| workspace.options.pip()) .unwrap_or_default(); Self { @@ -1341,20 +1453,259 @@ fn flag(yes: bool, no: bool) -> Option { } } -impl From for IndexLocations { - fn from(args: IndexArgs) -> Self { - IndexLocations::new( - args.index_url.and_then(Maybe::into_option), - args.extra_index_url - .map(|extra_index_urls| { - extra_index_urls - .into_iter() - .filter_map(Maybe::into_option) - .collect() - }) - .unwrap_or_default(), - args.find_links.unwrap_or_default(), - args.no_index, - ) +impl From for PipOptions { + fn from(args: ResolverArgs) -> Self { + let ResolverArgs { + index_args, + index_strategy, + keyring_provider, + resolution, + prerelease, + pre, + config_setting, + exclude_newer, + link_mode, + } = args; + + Self { + index_url: index_args.index_url.and_then(Maybe::into_option), + extra_index_url: index_args.extra_index_url.map(|extra_index_urls| { + extra_index_urls + .into_iter() + .filter_map(Maybe::into_option) + .collect() + }), + no_index: Some(index_args.no_index), + find_links: index_args.find_links, + index_strategy, + keyring_provider, + resolution, + prerelease: if pre { + Some(PreReleaseMode::Allow) + } else { + prerelease + }, + config_settings: config_setting + .map(|config_settings| config_settings.into_iter().collect::()), + exclude_newer, + link_mode, + ..PipOptions::default() + } + } +} + +impl From for PipOptions { + fn from(args: InstallerArgs) -> Self { + let InstallerArgs { + index_args, + index_strategy, + keyring_provider, + config_setting, + link_mode, + compile_bytecode, + no_compile_bytecode, + } = args; + + Self { + index_url: index_args.index_url.and_then(Maybe::into_option), + extra_index_url: index_args.extra_index_url.map(|extra_index_urls| { + extra_index_urls + .into_iter() + .filter_map(Maybe::into_option) + .collect() + }), + no_index: Some(index_args.no_index), + find_links: index_args.find_links, + index_strategy, + keyring_provider, + config_settings: config_setting + .map(|config_settings| config_settings.into_iter().collect::()), + link_mode, + compile_bytecode: flag(compile_bytecode, no_compile_bytecode), + ..PipOptions::default() + } + } +} + +impl From for PipOptions { + fn from(args: ResolverInstallerArgs) -> Self { + let ResolverInstallerArgs { + index_args, + index_strategy, + keyring_provider, + resolution, + prerelease, + pre, + config_setting, + exclude_newer, + link_mode, + compile_bytecode, + no_compile_bytecode, + } = args; + + Self { + index_url: index_args.index_url.and_then(Maybe::into_option), + extra_index_url: index_args.extra_index_url.map(|extra_index_urls| { + extra_index_urls + .into_iter() + .filter_map(Maybe::into_option) + .collect() + }), + no_index: Some(index_args.no_index), + find_links: index_args.find_links, + index_strategy, + keyring_provider, + resolution, + prerelease: if pre { + Some(PreReleaseMode::Allow) + } else { + prerelease + }, + config_settings: config_setting + .map(|config_settings| config_settings.into_iter().collect::()), + exclude_newer, + link_mode, + compile_bytecode: flag(compile_bytecode, no_compile_bytecode), + ..PipOptions::default() + } + } +} + +impl From for InstallerOptions { + fn from(args: InstallerArgs) -> Self { + let InstallerArgs { + index_args, + index_strategy, + keyring_provider, + config_setting, + link_mode, + compile_bytecode, + no_compile_bytecode, + } = args; + + Self { + index_url: index_args.index_url.and_then(Maybe::into_option), + extra_index_url: index_args.extra_index_url.map(|extra_index_urls| { + extra_index_urls + .into_iter() + .filter_map(Maybe::into_option) + .collect() + }), + no_index: Some(index_args.no_index), + find_links: index_args.find_links, + index_strategy, + keyring_provider, + config_settings: config_setting + .map(|config_settings| config_settings.into_iter().collect::()), + link_mode, + compile_bytecode: flag(compile_bytecode, no_compile_bytecode), + } + } +} + +impl From for ResolverOptions { + fn from(args: ResolverArgs) -> Self { + let ResolverArgs { + index_args, + index_strategy, + keyring_provider, + resolution, + prerelease, + pre, + config_setting, + exclude_newer, + link_mode, + } = args; + + Self { + index_url: index_args.index_url.and_then(Maybe::into_option), + extra_index_url: index_args.extra_index_url.map(|extra_index_urls| { + extra_index_urls + .into_iter() + .filter_map(Maybe::into_option) + .collect() + }), + no_index: Some(index_args.no_index), + find_links: index_args.find_links, + index_strategy, + keyring_provider, + resolution, + prerelease: if pre { + Some(PreReleaseMode::Allow) + } else { + prerelease + }, + config_settings: config_setting + .map(|config_settings| config_settings.into_iter().collect::()), + exclude_newer, + link_mode, + } + } +} + +impl From for ResolverInstallerOptions { + fn from(args: ResolverInstallerArgs) -> Self { + let ResolverInstallerArgs { + index_args, + index_strategy, + keyring_provider, + resolution, + prerelease, + pre, + config_setting, + exclude_newer, + link_mode, + compile_bytecode, + no_compile_bytecode, + } = args; + + Self { + index_url: index_args.index_url.and_then(Maybe::into_option), + extra_index_url: index_args.extra_index_url.map(|extra_index_urls| { + extra_index_urls + .into_iter() + .filter_map(Maybe::into_option) + .collect() + }), + no_index: Some(index_args.no_index), + find_links: index_args.find_links, + index_strategy, + keyring_provider, + resolution, + prerelease: if pre { + Some(PreReleaseMode::Allow) + } else { + prerelease + }, + config_settings: config_setting + .map(|config_settings| config_settings.into_iter().collect::()), + exclude_newer, + link_mode, + compile_bytecode: flag(compile_bytecode, no_compile_bytecode), + } + } +} + +impl From for PipOptions { + fn from(args: IndexArgs) -> Self { + let IndexArgs { + index_url, + extra_index_url, + no_index, + find_links, + } = args; + + Self { + index_url: index_url.and_then(Maybe::into_option), + extra_index_url: extra_index_url.map(|extra_index_urls| { + extra_index_urls + .into_iter() + .filter_map(Maybe::into_option) + .collect() + }), + no_index: Some(no_index), + find_links, + ..PipOptions::default() + } } } diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index 343e70f24..bf2eccce4 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -9272,6 +9272,229 @@ fn resolve_configuration() -> Result<()> { "### ); + // Write out to the top-level (`tool.uv`, rather than `tool.uv.pip`). + pyproject.write_str(indoc::indoc! {r#" + [project] + name = "example" + version = "0.0.0" + + [tool.uv] + resolution = "lowest-direct" + "#})?; + + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("anyio>3.0.0")?; + + uv_snapshot!(context.compile() + .arg("requirements.in"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in + anyio==3.0.1 + # via -r requirements.in + idna==3.6 + # via anyio + sniffio==1.3.1 + # via anyio + + ----- stderr ----- + Resolved 3 packages in [TIME] + "### + ); + + // Write out to both the top-level (`tool.uv`) and the pip section (`tool.uv.pip`). The + // `tool.uv.pip` section should take precedence. + pyproject.write_str(indoc::indoc! {r#" + [project] + name = "example" + version = "0.0.0" + + [tool.uv] + resolution = "lowest-direct" + + [tool.uv.pip] + resolution = "highest" + "#})?; + + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("anyio>3.0.0")?; + + uv_snapshot!(context.compile() + .arg("requirements.in"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in + anyio==4.3.0 + # via -r requirements.in + idna==3.6 + # via anyio + sniffio==1.3.1 + # via anyio + + ----- stderr ----- + Resolved 3 packages in [TIME] + "### + ); + + // But the command-line should take precedence over both. + uv_snapshot!(context.compile() + .arg("requirements.in") + .arg("--resolution=lowest-direct"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in --resolution=lowest-direct + anyio==3.0.1 + # via -r requirements.in + idna==3.6 + # via anyio + sniffio==1.3.1 + # via anyio + + ----- stderr ----- + Resolved 3 packages in [TIME] + "### + ); + + Ok(()) +} + +/// Verify that user configuration is respected. +#[test] +#[cfg(not(windows))] +fn resolve_user_configuration() -> Result<()> { + // Create a temporary directory to store the user configuration. + let xdg = assert_fs::TempDir::new().expect("Failed to create temp dir"); + let uv = xdg.child("uv"); + let config = uv.child("uv.toml"); + config.write_str(indoc::indoc! {r#" + [pip] + resolution = "lowest-direct" + "#})?; + + let context = TestContext::new("3.12"); + + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("anyio>3.0.0")?; + + // Resolution should use the lowest direct version. + uv_snapshot!(context.compile() + .arg("requirements.in") + .env("XDG_CONFIG_HOME", xdg.path()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in + anyio==3.0.1 + # via -r requirements.in + idna==3.6 + # via anyio + sniffio==1.3.1 + # via anyio + + ----- stderr ----- + Resolved 3 packages in [TIME] + "### + ); + + // Add a local configuration to generate hashes. + let config = context.temp_dir.child("uv.toml"); + config.write_str(indoc::indoc! {r" + [pip] + generate-hashes = true + "})?; + + // Resolution should use the lowest direct version and generate hashes. + uv_snapshot!(context.compile() + .arg("requirements.in") + .env("XDG_CONFIG_HOME", xdg.path()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in + anyio==3.0.1 \ + --hash=sha256:1ef7622396ab55829d4236a6f75e2199df6d26a4ba79bea0cb942a5fd2f79a23 \ + --hash=sha256:ed71f7542ef39875b65def219794d9dcb0a48c571317b13612c12b1f292701b5 + # via -r requirements.in + idna==3.6 \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f + # via anyio + sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + # via anyio + + ----- stderr ----- + Resolved 3 packages in [TIME] + "### + ); + + // Add a local configuration to override the user configuration. + let config = context.temp_dir.child("uv.toml"); + config.write_str(indoc::indoc! {r#" + [pip] + resolution = "highest" + "#})?; + + // Resolution should use the highest version. + uv_snapshot!(context.compile() + .arg("requirements.in") + .env("XDG_CONFIG_HOME", xdg.path()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in + anyio==4.3.0 + # via -r requirements.in + idna==3.6 + # via anyio + sniffio==1.3.1 + # via anyio + + ----- stderr ----- + Resolved 3 packages in [TIME] + "### + ); + + // However, the user-level `tool.uv.pip` settings override the project-level `tool.uv` settings. + // This is awkward, but we merge the user configuration into the workspace configuration, so + // the resulting configuration has both `tool.uv.pip.resolution` (from the user configuration) + // and `tool.uv.resolution` (from the workspace settings), so we choose the former. + let config = context.temp_dir.child("uv.toml"); + config.write_str(indoc::indoc! {r#" + resolution = "highest" + "#})?; + + // Resolution should use the highest version. + uv_snapshot!(context.compile() + .arg("requirements.in") + .env("XDG_CONFIG_HOME", xdg.path()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in + anyio==3.0.1 + # via -r requirements.in + idna==3.6 + # via anyio + sniffio==1.3.1 + # via anyio + + ----- stderr ----- + Resolved 3 packages in [TIME] + "### + ); + Ok(()) } diff --git a/uv.schema.json b/uv.schema.json index 3d58d690b..011b8a974 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -10,6 +10,22 @@ "null" ] }, + "compile-bytecode": { + "type": [ + "boolean", + "null" + ] + }, + "config-settings": { + "anyOf": [ + { + "$ref": "#/definitions/ConfigSettings" + }, + { + "type": "null" + } + ] + }, "dev-dependencies": { "description": "PEP 508-style requirements, e.g., `flask==3.0.0`, or `black @ https://...`.", "type": [ @@ -20,6 +36,74 @@ "type": "string" } }, + "exclude-newer": { + "anyOf": [ + { + "$ref": "#/definitions/ExcludeNewer" + }, + { + "type": "null" + } + ] + }, + "extra-index-url": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/IndexUrl" + } + }, + "find-links": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/FlatIndexLocation" + } + }, + "index-strategy": { + "anyOf": [ + { + "$ref": "#/definitions/IndexStrategy" + }, + { + "type": "null" + } + ] + }, + "index-url": { + "anyOf": [ + { + "$ref": "#/definitions/IndexUrl" + }, + { + "type": "null" + } + ] + }, + "keyring-provider": { + "anyOf": [ + { + "$ref": "#/definitions/KeyringProviderType" + }, + { + "type": "null" + } + ] + }, + "link-mode": { + "anyOf": [ + { + "$ref": "#/definitions/LinkMode" + }, + { + "type": "null" + } + ] + }, "native-tls": { "type": [ "boolean", @@ -32,6 +116,12 @@ "null" ] }, + "no-index": { + "type": [ + "boolean", + "null" + ] + }, "offline": { "type": [ "boolean", @@ -58,12 +148,32 @@ } ] }, + "prerelease": { + "anyOf": [ + { + "$ref": "#/definitions/PreReleaseMode" + }, + { + "type": "null" + } + ] + }, "preview": { "type": [ "boolean", "null" ] }, + "resolution": { + "anyOf": [ + { + "$ref": "#/definitions/ResolutionMode" + }, + { + "type": "null" + } + ] + }, "sources": { "type": [ "object",